Valid Number
A decimal point and an exponent are easy to track. The hard part is proving that every numeric component actually contains the digits the grammar requires.

Valid Number
Given a string s, determine whether it represents a valid number. A valid number is an optionally signed integer or decimal, optionally followed by an exponent consisting of 'e' or 'E' and an optionally signed integer. Integers require one or more digits; decimals may contain a dot with digits before it, after it, or on both sides, but must contain at least one digit.
Constraints
- 1 <= s.length <= 20
- s consists only of English letters, digits 0-9, '+', '-', or '.'
Important details
- The exponent is optional and must use 'e' or 'E' followed by an integer number.
- A sign may appear before the base number and after the exponent marker.
- Decimal forms include digits followed by '.', digits followed by '.' and digits, or '.' followed by digits.
- Return a boolean indicating validity.
Key topics
A decimal point and an exponent are easy to track. The hard part is proving that every numeric component actually contains the digits the grammar requires.
Start With the Grammar
The cleanest Valid Number solution begins by treating the input as a small grammar, not as something to pass into a numeric conversion function.
A valid string has this structure:
optional sign
base number
optional exponent
The base number is either:
- an integer: one or more digits
- a decimal: a dot with at least one digit somewhere in the base
The exponent, when present, has this structure:
e or E
optional sign
one or more digits
So the complete grammar is:
number := sign? (integer | decimal) exponent?
integer := digit+
decimal := digit+ "." digit*
| "." digit+
exponent := ("e" | "E") sign? digit+
The decimal grammar permits digits on either side of the dot, but not zero digits on both sides. Therefore:
| String | Valid? | Reason |
|---|---|---|
4. | Yes | Digits appear before the dot |
-.9 | Yes | Digits appear after the dot |
3.14 | Yes | Digits appear on both sides |
. | No | The mantissa has no digits |
2e10 | Yes | The exponent contains digits |
1e | No | The exponent is incomplete |
99e2.5 | No | The exponent must be an integer |
The input contract allows English letters, decimal digits, +, -, and .. We validate that grammar exactly. We do not add whitespace trimming, hexadecimal syntax, underscores, or other formats.
The critical acceptance condition is also exact:
The entire string must be consumed, and the final component must contain at least one digit.
That last clause is what rejects a valid-looking prefix followed by an incomplete suffix.
Recognize the Parser State
This is a state-tracking problem. We scan once from left to right, and each character is legal only in the context created by earlier characters.
A first attempt often tracks only:
seen_dot
seen_exponent
That model is incomplete. It can tell us whether a dot or exponent has appeared, but it cannot enforce the digit obligations created by component boundaries.
For example:
ehas no base digits.1eenters exponent mode but provides no exponent digits.+has a sign but no number.-.has a sign and dot but no mantissa digits.1e+has an exponent sign but no exponent digits.
The missing state is not “have we seen a number somewhere?” It is:
Does the current numeric component contain at least one digit?
That component changes when we enter the exponent. The mantissa is one component; the exponent is another.
We need three flags:
seen_digit: whether the current component contains a digitseen_dot: whether the mantissa already contains a dotseen_exponent: whether the exponent has already begun
These flags implement a finite-state parser. We do not need to build a large transition table for this grammar, but the transition-table idea is useful: each character either moves the parser to a legal state or causes immediate rejection.
The important distinction is between conversion and validation. Calling float(s) asks Python whether it can interpret the string as a runtime floating-point value. This problem asks whether the string follows a specific character grammar. Conversion hides the transitions we need to reason about, and its accepted syntax need not match the problem contract exactly.
For an interview, explicit parser state is easier to inspect, explain, and prove.
Turn the Grammar Into Obligations
Each character class creates a local rule.
Digits complete the current component
A digit is always legal in the current number component.
When we see one, set:
seen_digit = True
This flag has two meanings at different points in the scan:
- before the exponent: the mantissa contains at least one digit
- after the exponent: the exponent contains at least one digit
That reuse is safe because entering the exponent resets the flag.
A dot belongs only to the mantissa
A dot is legal only when:
- no earlier dot exists
- the exponent has not started
So . is rejected if either seen_dot or seen_exponent is already true.
The dot itself does not satisfy the digit obligation. That is why . remains invalid, while 4. and -.9 are valid.
An exponent starts a new component
An e or E is legal only when:
- no earlier exponent exists
- the mantissa already contains a digit
After accepting the exponent marker, reset seen_digit:
seen_digit = False
This reset is the central transition in the parser. It says: the mantissa is complete, but the exponent still owes us a digit.
Without the reset, 1e would incorrectly inherit the digit from the mantissa and pass.
A sign appears only at a component boundary
A sign is legal in exactly two positions:
- at index
0, before the base number - immediately after
eorE, before the exponent integer
A sign does not count as a digit. It only changes the grammar state in which digits may follow.
This rejects:
--6-+31-21e--4
It accepts:
-6+3.143e+76E-1
Every other character is invalid
Letters other than e and E are invalid. So are spaces, repeated signs, and any symbol outside the contract.
Rejecting immediately keeps the parser honest. Once a transition violates the grammar, no later character can repair it.
Derive the One-Pass Scan
Initialize all state to false:
seen_digit = False
seen_dot = False
seen_exponent = False
Then process each character:
-
Digit
Mark the current component complete with respect to its digit obligation. -
Dot
Reject after another dot or after an exponent. Otherwise record the dot. -
Exponent marker
Reject if an exponent already exists or if the mantissa has no digit. Otherwise record the exponent and resetseen_digit. -
Sign
Accept only at the beginning or immediately after an exponent marker. Do not mark a digit. -
Anything else
Reject immediately.
After the loop, return seen_digit.
That final return is not a convenience check. It is the accepting state of the parser. It rejects an unfinished exponent, a bare sign, or a dot-only mantissa.
The sign rule is easiest to express with the previous character:
i == 0 or s[i - 1] in "eE"
This works because the exponent branch has already rejected malformed placement before the sign is examined.
Prove the Accepting State
Use this invariant while scanning:
After consuming any accepted prefix,
seen_dotandseen_exponentdescribe the structural markers already used, andseen_digitrecords whether the current numeric component contains at least one digit.
Now check each transition.
- A digit sets
seen_digitto true, so the current component satisfies its digit obligation. - A dot is accepted only once and only before the exponent. It changes the mantissa structure but does not pretend to be a digit.
- An exponent is accepted only after a valid mantissa digit. Resetting
seen_digittransfers the digit obligation to the new exponent component. - A sign is accepted only at the beginning of a component. Since it does not set
seen_digit, a sign cannot complete an integer or exponent by itself. - Any other character has no legal grammar transition and is rejected.
If the scan finishes with seen_digit == False, the final component is incomplete. This catches:
+
.
-.
1e
1e+
If the scan finishes with seen_digit == True, every component that was opened has supplied at least one digit, and every structural marker was used legally.
This is why accepting a valid prefix is insufficient. For 1e, the prefix 1 is valid, and the exponent marker is structurally allowed. But the complete string ends before the exponent component reaches its accepting state.
The parser must consume the whole input. A valid prefix is not a valid number.
Trace the Failure Boundaries
State traces expose the bugs that ordinary examples hide.
Valid: -6.5e+2
| Character | seen_digit | seen_dot | seen_exponent | Action |
|---|---|---|---|---|
- | False | False | False | Sign at index 0 |
6 | True | False | False | Mantissa digit |
. | True | True | False | First mantissa dot |
5 | True | True | False | Mantissa digit |
e | False | True | True | Start exponent; reset digit state |
+ | False | True | True | Sign after exponent |
2 | True | True | True | Exponent digit |
The scan ends with seen_digit == True, so the string is accepted.
Invalid: 1e
The 1 satisfies the mantissa. The e is legal because the mantissa is complete, but entering exponent mode resets seen_digit to false. The scan ends immediately, so the exponent never receives a digit.
Invalid: 1e+
This follows the same path, with one additional accepted transition for the exponent sign. The sign still does not count as a digit. The final state remains incomplete.
Valid: -.9
The initial sign is legal. The dot is legal and does not complete the mantissa. The 9 supplies the required digit after the dot.
Requiring digits on both sides of the dot would incorrectly reject this valid form.
Valid: 4.
The 4 completes the mantissa before the dot. The trailing dot is therefore legal, even though no digit follows it.
Invalid: 99e2.5
The exponent begins after valid mantissa digits. The 2 completes the exponent. The following dot is rejected because dots belong only to the mantissa.
Invalid: 1.2.3
The first dot is legal. The second dot violates the one-dot restriction and is rejected immediately.
Invalid: 1a
The 1 is valid state progress. The a has no transition, so the parser rejects it at once. No numeric conversion is needed, and no later recovery is possible.
Implement the Python Validator
Here is the interview-ready valid number parser in Python. The branch order follows the derivation: digits, dot, exponent, sign, then invalid input.
class Solution:
def isNumber(self, s: str) -> bool:
seen_digit = False
seen_dot = False
seen_exponent = False
for i, char in enumerate(s):
if char.isdigit():
seen_digit = True
elif char == ".":
# A dot is allowed only once, and only in the mantissa.
if seen_dot or seen_exponent:
return False
seen_dot = True
elif char in "eE":
# The mantissa must be complete before the exponent starts.
if seen_exponent or not seen_digit:
return False
seen_exponent = True
# The exponent is a new component with its own digit obligation.
seen_digit = False
elif char in "+-":
# A sign is allowed at the start or immediately after e/E.
if i > 0 and s[i - 1] not in "eE":
return False
else:
return False
# The final component must contain at least one digit.
return seen_digit
The code intentionally does not call float(), strip whitespace, or accept alternate numeric formats. Those operations would expand or change the contract. This validator checks the supplied grammar directly.
One subtle point deserves emphasis: char.isdigit() is broader than the problem's stated ASCII input alphabet in some Python contexts. Under the given contract, the input contains only 0 through 9, so this is sufficient. If the input contract were widened, use an explicit ASCII check such as:
"0" <= char <= "9"
That is a contract decision, not a parser-state decision.
Complexity and Interview Checks
Let n be the length of the string.
- Time:
O(n), because each character is inspected once. - Auxiliary space:
O(1), because the parser stores only three boolean flags and the loop index.
There is no repeated substring parsing, backtracking, or numeric computation. The work is proportional to the input because the parser must inspect every character before it can certify complete-string validity.
Before submitting, test the boundaries rather than only ordinary decimals:
| Category | Examples |
|---|---|
| Basic integers | 0, 0089, -6, +3 |
| Decimals | 4., -.9, 3.14 |
| Exponents | 2e10, -90E3, 3e+7, 6E-1 |
| Missing base digits | +, -, ., -. |
| Missing exponent digits | 1e, 1e+, 1e- |
| Repeated markers | 1..2, 1e2e3, 1e2.5 |
| Bad signs | --6, -+3, 1-2, 1e--4 |
| Invalid symbols | abc, 1a, 95a54e53 |
The common implementation failures are predictable:
- returning true after a valid prefix
- allowing signs anywhere
- allowing a dot after exponent entry
- forgetting to reset
seen_digitaftereorE - requiring digits on both sides of the dot
- treating a sign or dot as a digit
- silently adding whitespace or alternate syntax rules
The technique has a clear boundary. It fits a restricted numeric grammar with bounded parser state. It does not decide whether a value is mathematically finite, whether a runtime conversion will overflow, or whether a locale-specific format should be accepted. Those are different contracts with different state and policy requirements.
The transferable rule is simple:
When each symbol changes what may legally follow, derive one state variable per grammar obligation. Reset completion state when a new component begins, reject illegal transitions immediately, and require an accepting state after the entire input is consumed.
Do not memorize seen_digit, seen_dot, and `seen_exponent as magic flag names. Derive them from the grammar. Read the error, trace the state, fix the assumption.
References
Research updated Sep 7, 2026


