For problem-statement, please look at #4
A TC39 proposal to add Number.isSafeNumeric.
A simple and reliable method to validate input is numeric string and represents a valid javascript number.
Key Benefits:
- Ensure input is a valid numeric string, reducing unexpected behaviors during parsing and subsequent operations
- Avoid string's mathematical value changes during string-number conversions (developers may not aware of this)
- Reduce developer mental overhead
Stage: 0
Champion: ZiJian Liu / YiLong Li
Authors: ZiJian Liu / YiLong Li
Last Presented: (unpresented)
Slides:
In web development, validating strings that can be safely converted to JavaScript Numbers (float64) is a common requirement, particularly in scenarios like:
- API response parsing(falsy values(
null,undefined,''),Java.Longoverflow, etc.) - Form input validation(falsy values, whitespace, unexpected characters, etc.)
- Financial calculations(Mathematical value changes in conversion)
- Data processing(complex validation logic)
However, current solutions have significant limitations:
JavaScript's built-in methods (Number(), parseInt(), parseFloat(), isFinite()) have inconsistent behaviors:
- Difficult to choose which method to use
- Need handle edge cases manually, and it's hard to remember all of them
| Input String | Number() |
parseInt() |
parseFloat() |
isFinite() |
Issue |
|---|---|---|---|---|---|
'' |
0 |
NaN |
NaN |
true |
Empty string handling inconsistent |
' ' |
0 |
NaN |
NaN |
true |
Whitespace handling inconsistent |
'123.45' |
123.45 |
123 |
123.45 |
true |
parseInt truncates decimals |
'.123' |
0.123 |
NaN |
0.123 |
true |
Leading decimal point handling |
'123.' |
123 |
123 |
123 |
true |
Trailing decimal point silently accepted |
'00123' |
123 |
123 |
123 |
true |
Leading zeros silently accepted |
'1e5' |
100000 |
1 |
100000 |
true |
Scientific notation handling varies |
'0x123' |
291 |
291 |
0 |
true |
Hex string handling inconsistent |
'9007199254740993' |
9007199254740992 |
9007199254740992 |
9007199254740992 |
true |
Exceeds MAX_SAFE_INTEGER |
'0.1234567890123456789' |
0.12345678901234568 |
0 |
0.12345678901234568 |
true |
Mathematical value changes in conversion |
'Infinity' |
Infinity |
NaN |
Infinity |
false |
Infinity handling inconsistent |
'-Infinity' |
-Infinity |
NaN |
-Infinity |
false |
Negative infinity handling inconsistent |
Current validation approaches often require complex code or third-party libraries:
StackOverflow Example
3276 Votes, Link: How can I check if a string is a valid number?
// Simple but unreliable
function isNumeric(str) {
if (typeof str != 'string') return false // we only process strings!
return !isNaN(str) && !isNaN(parseFloat(str))
}
isNumeric('0.1234567890123456789') // true, but mathematical value changes when converted to NumberUsing npm libraries: `validator` and `is-number`
Using validator#isDecimal and is-number#isNumber
// Even popular libraries have limitations
const validator = require('validator')
console.log(validator.isDecimal('0.1234567890123456789')) // true, but mathematical value changes when converted to Number
const isNumber = require('is-number')
console.log(isNumber('0.1234567890123456789')) // true, but mathematical value changes when converted to NumberComplex Validation Example
// Complex but still has edge cases
function isValidNumber(str) {
// 1. Basic type checks
if (typeof str !== 'string') return false
if (str.trim() === '') return false
// 2. Format checks
const num = Number(str)
if (Number.isNaN(num)) return false
if (!Number.isFinite(num)) return false
// 3. Range checks (often overlooked)
if (num > Number.MAX_SAFE_INTEGER) return false
if (num < Number.MIN_SAFE_INTEGER) return false
// 4. Format consistency check (often overlooked)
if (String(num) !== str) return false
return true
}
// Even with such complex validation, edge cases slip through
isValidNumber('1e5') // true, but might not be desired format
isValidNumber('0x123') // true, accepts hexadecimal
isValidNumber('.123') // true, accepts non-standard decimal formatThis complex validation approach has several drawbacks:
- Increases code complexity
- Prone to missing edge cases
- High maintenance cost
- Difficult to reuse across different projects
Number.isSafeNumeric(input)This method aims to provide a simple and reliable way to validate input numeric string can be safely converted to a JavaScript Number (using float64).
A input is considered "safe numeric string" if it meets ALL of the following criteria:
-
Format Rules:
- Contains only ASCII digits (0-9) with optional single leading minus sign
- Has at most one decimal point, must have digits on both sides (e.g. "0.1", not ".1" or "1.")
- No leading zeros (except for decimal numbers < 1)
- No whitespace or other characters allowed
-
Value safety:
- Must be within the range of ±2^53-1 (
Number.MAX_SAFE_INTEGER) - The mathematical value represented by the string must remain unchanged through the string-number-string conversion process. That is:
MV(string) === MV(ToString(ToNumber(string))),MVis the mathematical value of the string(pseudo code).
This equation ensures:
- The string can be safely converted to a JavaScript Number and back without losing its original mathematical meaning.
- The displayed value will be consistent across different systems and contexts
- Aligns with developer's intuition and expectations.
For more details, see Why string to number to string may cause mathematical value changes
- Must be within the range of ±2^53-1 (
// Valid numeric strings
Number.isSafeNumeric('0') // true
Number.isSafeNumeric('123') // true
Number.isSafeNumeric('-123') // true
Number.isSafeNumeric('0.123') // true
Number.isSafeNumeric('-0.123') // true
Number.isSafeNumeric('1.1') // true
// Invalid numeric strings
Number.isSafeNumeric('.123') // false (missing leading zero)
Number.isSafeNumeric('123.') // false (trailing decimal)
Number.isSafeNumeric('00123') // false (leading zeros)
Number.isSafeNumeric('1.2.3') // false (multiple decimal points)
Number.isSafeNumeric(' 0 ') // false (whitespace)
Number.isSafeNumeric('1,000') // false (thousands separator)
Number.isSafeNumeric('abc') // false (non-numeric characters)
Number.isSafeNumeric('12a3') // false (contains letter)
Number.isSafeNumeric('1e5') // false (scientific notation)
Number.isSafeNumeric('1.23e-4') // false (scientific notation)
Number.isSafeNumeric('0x123') // false (hexadecimal)// Valid numeric strings
Number.isSafeNumeric('9007199254740991') // true (MAX_SAFE_INTEGER)
Number.isSafeNumeric('-9007199254740991') // true (-MAX_SAFE_INTEGER)
Number.isSafeNumeric('1234.5678') // true (maintains same mathematical value after conversion)
// Invalid numeric strings
Number.isSafeNumeric('9007199254740992') // false (exceeds MAX_SAFE_INTEGER)
Number.isSafeNumeric('9007199254740989.1') // false (mathematical value changes near MAX_SAFE_INTEGER)
Number.isSafeNumeric('0.1234567890123456789') // false (mathematical value changes in conversion)During the String → Number conversion process, two factors may cause the mathematical value to change:
a) RoundMVResult behavior:
- When a decimal string has more than 20 significant digits, the value will be modified during conversion
- This modification is irreversible and always changes the original mathematical value
- Example: "0.300000000000000041" becomes "0.30000000000000004"
b) IEEE-754 double precision format limitations:
- Some decimal values cannot be represented exactly in binary format
- This may lead to different decimal representations when converting back to string
- for examples:
Number('0.300000000000000041')is0.30000000000000004, but0.30000000000000004.toString()is0.30000000000000004, which is not the same as the original string.
No implementations yet
Q: Why use strict number format rules by default, and not support other formats(scientific notation, International number formats, etc.)?
A: By validating decimal strings, we:
- Focus on the fundamental programming format used in JavaScript programming
- Ensure consistent parsing across different systems (e.g.
1e5is100000in JavaScript but may be treated as string in others) - Reduce complexity in data processing and validation
Further, we could add the second parameter to support more formats and parsing options.
For example:
- Support scientific notation with
formatoption (enum:'decimal','number')
// Scientific notation format
Number.isSafeNumeric('1e5', { format: 'number' }) // true, with decimal + scientific notation supported
// Decimal format
Number.isSafeNumeric('100000', { format: 'decimal' }) // true- Support flexible parsing with
looseoption (boolean, default:false) to allow common number formats, which is more friendly to old code that already uses these formats.
Number.isSafeNumeric('00123', { loose: true }) // true, with leading zeros
Number.isSafeNumeric('+123', { loose: true }) // true, with leading plus sign
Number.isSafeNumeric('.123', { loose: true }) // true, with leading decimal point
Number.isSafeNumeric('123.', { loose: true }) // true, with trailing decimal point
Number.isSafeNumeric(' 0 ', { loose: true }) // true, with whitespaceQ: How to handle subsequent numeric calculations?
A: This proposal focuses on ensuring numeric string representation is safe to be converted to a JavaScript number (float64).
For high-precision decimal calculations, please refer to high-precision decimal libraries like decimal.js or the upcoming proposal-decimal.
Q: How does this relate to proposal-decimal?
A: These two proposals have different goals:
- proposal-decimal creates a new type of number for precise calculations
- this proposal (Number.isSafeNumeric) just checks if a string can be safely converted to a regular JavaScript number (float64)