A clear guide to validating whether a string is a valid number using grammar rules and one left-to-right scan.
Problem Restatement
We are given a string s.
We need to return true if s represents a valid number. Otherwise, return false.
A valid number can have:
- A decimal number or an integer
- Optionally, an exponent part using
eorE, followed by an integer
The official examples include valid strings such as:
"2"
"0089"
"-0.1"
"+3.14"
"4."
"-.9"
"2e10"
"-90E3"
"3e+7"
"+6e-1"
"53.5e93"
"-123.456e789"And invalid strings such as:
"abc"
"1a"
"1e"
"e3"
"99e2.5"
"--6"
"-+3"
"95a54e53"The official constraints are 1 <= s.length <= 20, and s contains only English letters, digits, +, -, or ..
Input and Output
| Item | Meaning |
|---|---|
| Input | A string s |
| Output | true if s is a valid number, otherwise false |
| Exponent | e or E, followed by an integer |
| Sign | + or -, allowed only at the start or right after exponent |
| Dot | . allowed only before exponent |
Function shape:
def isNumber(s: str) -> bool:
...Examples
For:
s = "0"The answer is:
True"0" is an integer.
For:
s = "e"The answer is:
FalseAn exponent marker cannot stand alone.
For:
s = "."The answer is:
FalseA decimal point alone has no digits.
For:
s = ".1"The answer is:
TrueA decimal number may start with a dot if at least one digit follows it.
First Thought: Split by Cases
We could try to write many special cases:
integer
signed integer
decimal
signed decimal
decimal with exponent
integer with exponent
signed exponentThis quickly becomes messy.
A better approach is to scan the string once and track what we have already seen.
Key Insight
There are only a few important facts to remember while scanning:
| Flag | Meaning |
|---|---|
seen_digit | We have seen at least one digit in the current numeric part |
seen_dot | We have seen a decimal point |
seen_exp | We have seen e or E |
The exponent is special.
After e or E, the string must contain an integer. That means:
- No dot after exponent
- Optional sign after exponent
- At least one digit after exponent
To enforce the third rule, when we see e or E, we reset:
seen_digit = FalseThen the final answer is valid only if seen_digit is true at the end.
Character Rules
For each character:
| Character | Rule |
|---|---|
| Digit | Always allowed, set seen_digit = True |
+ or - | Allowed only at index 0 or immediately after e or E |
. | Allowed only if no dot has appeared and no exponent has appeared |
e or E | Allowed only if no exponent has appeared and there was a digit before it |
| Anything else | Invalid |
The important subtle case is exponent.
This is invalid:
"1e"When we see e, we reset seen_digit to False. Since no digit appears after it, the final result is False.
This is valid:
"1e-3"The sign is allowed because it appears immediately after e.
Then 3 makes the exponent integer valid.
Algorithm
Initialize:
seen_digit = False
seen_dot = False
seen_exp = FalseScan each character ch at index i.
If ch is a digit:
seen_digit = TrueIf ch is + or -, it is valid only when:
i == 0 or s[i - 1] in "eE"If ch is ., it is invalid if:
seen_dot or seen_expOtherwise mark seen_dot = True.
If ch is e or E, it is invalid if:
seen_exp or not seen_digitOtherwise:
seen_exp = True
seen_digit = FalseAt the end, return seen_digit.
Correctness
The algorithm accepts signs only at the beginning of the string or immediately after an exponent marker. These are exactly the two places where the grammar allows signs.
The algorithm accepts a decimal point only before any exponent and only once. This matches the rule that a decimal number may contain one dot, while the exponent part must be an integer.
The algorithm accepts e or E only after at least one digit has appeared and only once. This ensures the base part exists and prevents multiple exponents.
After reading an exponent marker, the algorithm resets seen_digit to False. Therefore the final return requires at least one digit after the exponent. This rejects strings like "1e" and "1e+".
At the end, seen_digit is true exactly when the current required numeric part contains at least one digit. Therefore the algorithm returns true exactly for valid numbers.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | We scan the string once |
| Space | O(1) | We store only three boolean flags |
Implementation
class Solution:
def isNumber(self, s: str) -> bool:
seen_digit = False
seen_dot = False
seen_exp = False
for i, ch in enumerate(s):
if ch.isdigit():
seen_digit = True
elif ch in "+-":
if i != 0 and s[i - 1] not in "eE":
return False
elif ch == ".":
if seen_dot or seen_exp:
return False
seen_dot = True
elif ch in "eE":
if seen_exp or not seen_digit:
return False
seen_exp = True
seen_digit = False
else:
return False
return seen_digitCode Explanation
We start with three flags:
seen_digit = False
seen_dot = False
seen_exp = FalseA digit is always allowed:
if ch.isdigit():
seen_digit = TrueA sign is allowed only at the start or right after exponent:
elif ch in "+-":
if i != 0 and s[i - 1] not in "eE":
return FalseA dot is allowed only once and only before exponent:
elif ch == ".":
if seen_dot or seen_exp:
return False
seen_dot = TrueAn exponent is allowed only once and only after a valid numeric base:
elif ch in "eE":
if seen_exp or not seen_digit:
return False
seen_exp = True
seen_digit = FalseThe reset is necessary because the exponent must have digits after it.
Any other character makes the string invalid:
else:
return FalseFinally:
return seen_digitThis rejects ".", "e", "1e", and "1e+".
Testing
def run_tests():
s = Solution()
valid = [
"2",
"0089",
"-0.1",
"+3.14",
"4.",
"-.9",
"2e10",
"-90E3",
"3e+7",
"+6e-1",
"53.5e93",
"-123.456e789",
"0",
".1",
]
invalid = [
"abc",
"1a",
"1e",
"e3",
"99e2.5",
"--6",
"-+3",
"95a54e53",
".",
"+.",
"1e+",
"1e-",
"1..2",
"1e2e3",
]
for text in valid:
assert s.isNumber(text) is True, text
for text in invalid:
assert s.isNumber(text) is False, text
print("all tests passed")
run_tests()| Test | Why |
|---|---|
"2", "0089" | Integer forms |
"-0.1", "+3.14", "4.", "-.9" | Decimal forms |
"2e10", "3e+7", "+6e-1" | Exponent forms |
".", "+." | Dot without digits |
"1e", "1e+", "1e-" | Incomplete exponent |
"99e2.5" | Dot after exponent |
"--6", "-+3" | Invalid signs |
"1e2e3" | Multiple exponents |