On this page
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:
- The pattern: staged parsing (atoi) with an overflow check before each step.
- A second flavor: an explicit state machine for "is this a valid number?".
- Where hand-written parsers run in production (protocols, configs, input validation).
- 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 need | Use |
|---|---|
| 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 values | a regex anchored at both ends |
| Strict validation of untrusted input | your own parser, not Double.parseDouble or float() |
| A larger format (config, query) | a transition table, or a parser generator |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
Checking overflow after result * 10 + d | the value has already wrapped | check against (MAX - d) / 10 first |
Character.isDigit / str.isdigit | accepts Arabic-Indic and other digits | test '0' <= c <= '9' |
| Using the language's parser as a validator | accepts "1e5f", "Infinity", "1_000" | a strict parser of your own |
| Nested quantifiers in a regex | (a+)+$ takes exponential time | simple patterns, or a hand-written scanner |
Not resetting "seen digit" after e | "1e" is accepted | the 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
- 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.isDigitaccepts 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.