Strings · String Fundamentals

String Parsing State Machine

Manually parsing a string character by character through a sequence of stages (skip whitespace, read an optional sign, read digits) - each stage consuming exactly the characters it's responsible for and leaving the rest for later stages or ignoring them entirely.

  • 6 min read
  • 7 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The approach
  3. A second flavor: an explicit state machine
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

Turning text into a value means applying rules about what may appear where. Examples are reading " -42 apples" as -42, or deciding that "3.5e2" is a valid number. Hand-written parsers move one index through the input in stages (whitespace, sign, digits) or track a state that says which characters are allowed next. Their bugs live at the boundaries between stages, in the arithmetic that builds the value, and in what counts as a "digit".

We'll go from simple to more technical:

  1. The pattern: staged parsing (atoi) with an overflow check before each step.
  2. A second flavor: an explicit state machine for "is this a valid number?".
  3. Where hand-written parsers run in production (protocols, configs, input validation).
  4. What breaks it: late overflow checks, non-ASCII digits, sign rules, empty stages.

The approach

Java

int myAtoi(String s) {
    int i = 0, n = s.length();
    while (i < n && s.charAt(i) == ' ') i++;                    // stage 1: leading spaces
    int sign = 1;
    if (i < n && (s.charAt(i) == '+' || s.charAt(i) == '-')) {  // stage 2: at most one sign
        if (s.charAt(i) == '-') sign = -1;
        i++;
    }
    int result = 0;
    while (i < n && s.charAt(i) >= '0' && s.charAt(i) <= '9') { // stage 3: ASCII digits only
        int d = s.charAt(i) - '0';
        if (result > (Integer.MAX_VALUE - d) / 10) {            // result * 10 + d would overflow
            return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
        }
        result = result * 10 + d;
        i++;
    }
    return sign * result;
}

Python

def my_atoi(s: str) -> int:
    i, n = 0, len(s)
    while i < n and s[i] == " ":                     # stage 1: leading spaces
        i += 1
    sign = 1
    if i < n and s[i] in "+-":                       # stage 2: at most one sign
        sign = -1 if s[i] == "-" else 1
        i += 1
    result = 0
    while i < n and "0" <= s[i] <= "9":              # stage 3: ASCII digits only
        result = result * 10 + (ord(s[i]) - ord("0"))
        i += 1
    return max(-2**31, min(2**31 - 1, sign * result))   # ints never wrap in Python - but still clamp

Each stage consumes only the characters it owns and stops at the first one it doesn't - so "8080 is the port" gives 8080 and "port 8080" gives 0. The overflow check runs before the multiply: afterwards, the int has already wrapped. Clamping a too-large magnitude to Integer.MIN_VALUE is exactly right on the negative side, because the negative range holds one more value (2147483648).

A long accumulator works too, but only if it's clamped inside the loop. Clamped once at the end, the long itself overflows on long inputs: "18446744073709551617" (2^64 + 1) wraps to 1. Python's ints never overflow, so there the accumulator never wraps and clamping once at the end is correct - but it still has to happen: without it, that input comes back unchanged.

A second flavor: an explicit state machine

"Is this a valid number?" - an optional sign, digits with at most one dot, then optionally e/E and a signed integer - has too many rules for three stages. Track what has been seen and reject any character that isn't allowed at that point:

Java

boolean isNumber(String s) {
    boolean digit = false, dot = false, exp = false;
    for (int i = 0; i < s.length(); i++) {
        char c = s.charAt(i);
        if (c >= '0' && c <= '9') digit = true;
        else if (c == '+' || c == '-') {
            if (i > 0 && s.charAt(i - 1) != 'e' && s.charAt(i - 1) != 'E') return false;  // first, or right after e
        } else if (c == 'e' || c == 'E') {
            if (exp || !digit) return false;       // one exponent, after some digits
            exp = true;
            digit = false;                         // the exponent needs its own digits
        } else if (c == '.') {
            if (dot || exp) return false;          // one dot, never in the exponent
            dot = true;
        } else return false;
    }
    return digit;
}

Python

def is_number(s: str) -> bool:
    digit = dot = exp = False
    for i, c in enumerate(s):
        if "0" <= c <= "9":
            digit = True
        elif c in "+-":
            if i > 0 and s[i - 1] not in "eE":
                return False                         # first, or right after e
        elif c in "eE":
            if exp or not digit:
                return False                         # one exponent, after some digits
            exp = True
            digit = False                            # the exponent needs its own digits
        elif c == ".":
            if dot or exp:
                return False                         # one dot, never in the exponent
            dot = True
        else:
            return False
    return digit

"-.9", "4." and "+6e-1" are numbers; ".", "1e", "-+3" and "99e2.5" aren't. Forgetting to reset digit after e accepts "1e"; allowing a dot after e accepts "99e2.5".

Where you'll see it

Config values, HTTP headers such as Content-Length, log fields and CSV cells all get parsed this way. The JSON number grammar is a state machine like the one above but stricter: it rejects "0089", "4." and ".5", which Java's Double.parseDouble accepts. Decide deliberately between lenient parsing (atoi: take what makes sense, default to 0) and strict parsing (Integer.parseInt throws on " 42" and on "2147483648"). Python's int() sits in between - it accepts " 42 ", "+42" and "1_000" and never overflows, but raises on "8080 is the port" - and float() also accepts "inf", "nan" and "Infinity".

Which tool for which job?

You needUse
Lenient integer parsing (atoi)stages: whitespace, sign, digits, with a clamp checked before each step
Is the whole string a valid number?a state machine, as flags or as a transition table
A shape check, no valuesa regex anchored at both ends
Strict validation of untrusted inputyour own parser, not Double.parseDouble or float()
A larger format (config, query)a transition table, or a parser generator

Common mistakes

MistakeWhat happensDo this instead
Checking overflow after result * 10 + dthe value has already wrappedcheck against (MAX - d) / 10 first
Character.isDigit / str.isdigitaccepts Arabic-Indic and other digitstest '0' <= c <= '9'
Using the language's parser as a validatoraccepts "1e5f", "Infinity", "1_000"a strict parser of your own
Nested quantifiers in a regex(a+)+$ takes exponential timesimple patterns, or a hand-written scanner
Not resetting "seen digit" after e"1e" is acceptedthe exponent needs its own digits

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Why check result > (Integer.MAX_VALUE - d) / 10 before computing result * 10 + d?

Show answer

Once the multiply overflows, the int has wrapped and the information is gone - no check afterwards can recover it. The rearranged inequality asks "would the next step exceed MAX_VALUE?" using only values that fit.

2. atoi accumulates in a long and clamps once, after the loop. What does it return for "18446744073709551617"?

Show answer
  1. The value is 2^64 + 1, which wraps the long itself to 1 - inside the int range, so the final clamp doesn't fire. Clamp inside the loop, as soon as the value passes Integer.MAX_VALUE.

3. In the valid-number checker, why reset digit to false when an e is seen?

Show answer

The exponent must have digits of its own. Without the reset, "1e" ends with digit still true from the mantissa and is wrongly accepted.

Key takeaways

  • Parse in stages over one index; each stage stops at the first character it doesn't own.
  • Guard overflow before the multiply, or clamp a long inside the loop - never only at the end.
  • Test ASCII digits explicitly; Character.isDigit accepts other scripts.
  • For richer grammars, track state explicitly (flags or a transition table).
  • Choose lenient or strict parsing on purpose, and use the same rules everywhere.

Quick revise

The whole skill on one page, for the night before an interview.

This page puts the lesson and the deep dive together. Skim it before practice or an interview, and go back to the full lesson for anything you don't recognize.

Pick the tool

You needUse
atoistages plus overflow checks before each step
Valid number?state machine / transition table
Shape onlysimple anchored regex
Untrusted inputyour own strict parser

Snippets worth knowing by heart

Java

for (; i < s.length() && s.charAt(i) >= '0' && s.charAt(i) <= '9'; i++) {
    int d = s.charAt(i) - '0';
    if (result > (Integer.MAX_VALUE - d) / 10) return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
    result = result * 10 + d;
}

Python

NEXT = {"start": {"s": "sign", "d": "int", ".": "dot0"}, "sign": {"d": "int", ".": "dot0"},
        "int": {"d": "int", ".": "intdot", "e": "exp"}, "intdot": {"d": "frac", "e": "exp"},
        "dot0": {"d": "frac"}, "frac": {"d": "frac", "e": "exp"},
        "exp": {"s": "esign", "d": "edig"}, "esign": {"d": "edig"}, "edig": {"d": "edig"}}
ACCEPT = {"int", "intdot", "frac", "edig"}

The traps

  • Late overflow checks: after result * 10 + d the value has already wrapped. Compare with (MAX - d) / 10 first.
  • isDigit / isdigit accept Arabic-Indic and other digits. Test '0' <= c <= '9'.
  • Library parsers as validators: they accept "1e5f", "Infinity" or "1_000". Write a strict parser of your own.
  • Nested regex quantifiers: (a+)+$ takes exponential time on a near miss. Use simple patterns or a hand-written scanner.
  • "1e" accepted: "seen digit" wasn't reset after e. The exponent needs its own digits.

Staff-level points, one line each

  • A transition table agreed with the flag version and the regex on all 19,531 strings of length 0-6.
  • Double.parseDouble accepts "1e5f", "0x1p3", " 7 ", "Infinity", "NaN"; Python's float() accepts "1_000", "inf", "٣".
  • Integer.parseInt of the Arabic-Indic three is 3, but that char minus '0' is 1587.
  • (a+)+$ was over 20x slower at n = 24 than at n = 18 (ReDoS).

Say it in an interview

  • "I parse in stages and check overflow before each multiply."
  • "For validation, a small state machine: each state lists what's allowed next."
  • "I wouldn't use the library parser to validate, because it accepts too much."
  • "No nested quantifiers in regexes on untrusted input."

Practice

This skill comes with 7 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice String Parsing State Machine free

String to Integer (atoi)

MediumProCoding problemJava · Python

Turn the start of a string into a 32-bit signed integer, the way C's atoi does. Skip any spaces at the front, accept one optional + or -, then read digits until a character that isn't a digit. No digits at all means the result is 0.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Valid Number

HardProCoding problemJava · Python

Decide whether a string spells a number. We accept an integer or a decimal, optionally followed by an exponent: e or E and then an integer.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice String Parsing State Machine free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime