Skip to main content

submilli_engine/runtime/
number.rs

1//! JavaScript-compatible number parsers and formatters.
2
3use num_traits::FromPrimitive;
4
5/// ECMAScript ToUint32: truncate then wrap, including non-finite inputs.
6pub(crate) fn to_uint32(value: f64) -> u32 {
7    if value == 0.0 || !value.is_finite() {
8        return 0;
9    }
10    value.trunc().rem_euclid(4_294_967_296.0) as u32
11}
12
13pub(crate) fn to_int32(value: f64) -> i32 {
14    to_uint32(value) as i32
15}
16
17/// ECMAScript ToUint16, as `String.fromCharCode` applies it.
18pub(crate) fn to_uint16(value: f64) -> u16 {
19    to_uint32(value) as u16
20}
21
22/// ECMAScript ToUint8, as a `Uint8Array` byte write applies it.
23pub(crate) fn to_uint8(value: f64) -> u8 {
24    to_uint32(value) as u8
25}
26
27/// ECMAScript ToString for f64 — spells NaN/Infinity/-Infinity and
28/// normalizes -0 to "0" (Rust's default prints "inf" and keeps the sign).
29pub fn format_number_js(n: f64) -> String {
30    if n.is_nan() {
31        "NaN".to_string()
32    } else if n.is_infinite() {
33        if n > 0.0 {
34            "Infinity".to_string()
35        } else {
36            "-Infinity".to_string()
37        }
38    } else if n == 0.0 {
39        "0".to_string()
40    } else if n.abs() >= 1e21 || n.abs() < 1e-6 {
41        jsify_exponent(&format!("{n:e}"))
42    } else {
43        n.to_string()
44    }
45}
46
47/// JS-spec `Number#toFixed(digits)`.
48pub fn to_fixed_js(x: f64, digits: f64) -> Result<String, String> {
49    let d = digits.trunc();
50    if !(0.0..=100.0).contains(&d) || digits.is_nan() {
51        return Err("toFixed digits must be between 0 and 100".to_string());
52    }
53    if !x.is_finite() || x.abs() >= 1e21 {
54        return Ok(format_number_js(x));
55    }
56    let digits = d as usize;
57    let magnitude = fixed_ties_away_from_zero(x.abs(), digits);
58    // JS keeps the sign of a negative value that rounds to zero (`-0.0001`
59    // gives "-0.00") but drops it for -0 itself.
60    Ok(if x < 0.0 {
61        format!("-{magnitude}")
62    } else {
63        magnitude
64    })
65}
66
67/// Digits past the decimal point that spell any f64 exactly: the smallest
68/// subnormal has 1074 of them.
69const EXACT_FRACTION_DIGITS: usize = 1074;
70
71/// The non-negative `x` with `digits` fraction digits, an exact tie rounded
72/// up as JS's `toFixed` picks "the larger n". Rust's formatting rounds the
73/// binary value correctly but breaks an exact tie to even, so
74/// `(2.25).toFixed(1)` would be "2.2" where JS gives "2.3".
75fn fixed_ties_away_from_zero(x: f64, digits: usize) -> String {
76    let rounded = format!("{x:.digits$}");
77    // Only a value whose next digit rounds to 5 can sit on a tie; checking
78    // that first spares the exact spelling for nearly every call.
79    let one_more = digits + 1;
80    if !format!("{x:.one_more$}").ends_with('5') {
81        return rounded;
82    }
83    let exact = format!("{x:.EXACT_FRACTION_DIGITS$}");
84    let Some(point) = exact.find('.') else {
85        return rounded;
86    };
87    let fraction_end = point + 1 + digits;
88    let Some(rest) = exact.get(fraction_end..) else {
89        return rounded;
90    };
91    let is_tie = rest
92        .strip_prefix('5')
93        .is_some_and(|tail| tail.bytes().all(|b| b == b'0'));
94    if !is_tie {
95        return rounded;
96    }
97    let kept_end = if digits == 0 { point } else { fraction_end };
98    increment_last_digit(&exact[..kept_end])
99}
100
101/// `number`, a decimal spelled with ASCII digits and at most one point, plus
102/// one unit in its last digit: "2.2" gives "2.3" and "9.99" gives "10.00".
103fn increment_last_digit(number: &str) -> String {
104    let mut digits = number.to_owned().into_bytes();
105    for byte in digits.iter_mut().rev() {
106        match *byte {
107            b'.' => continue,
108            b'9' => *byte = b'0',
109            _ => {
110                *byte += 1;
111                return ascii_text(digits);
112            }
113        }
114    }
115    digits.insert(0, b'1');
116    ascii_text(digits)
117}
118
119/// `bytes`, ASCII by construction, as a `String`; nothing is replaced.
120fn ascii_text(bytes: Vec<u8>) -> String {
121    bytes.into_iter().map(char::from).collect()
122}
123
124/// JS-spec `Number#toExponential(fractionDigits)`. A NaN `fractionDigits`
125/// is the omitted-argument sentinel: use however many digits uniquely
126/// identify the value.
127pub fn to_exponential_js(x: f64, fraction_digits: f64) -> Result<String, String> {
128    if !x.is_finite() {
129        return Ok(format_number_js(x));
130    }
131    let mantissa_exp = if fraction_digits.is_nan() {
132        format!("{x:e}")
133    } else {
134        let d = fraction_digits.trunc();
135        if !(0.0..=100.0).contains(&d) {
136            return Err("toExponential digits must be between 0 and 100".to_string());
137        }
138        format!("{:.*e}", d as usize, x)
139    };
140    Ok(jsify_exponent(&mantissa_exp))
141}
142
143/// JS-spec `Number#toPrecision(precision)`. A NaN `precision` is the
144/// omitted-argument sentinel: behave like plain `toString`.
145pub fn to_precision_js(x: f64, precision: f64) -> Result<String, String> {
146    if precision.is_nan() {
147        return Ok(format_number_js(x));
148    }
149    let p = precision.trunc();
150    if !(1.0..=100.0).contains(&p) {
151        return Err("toPrecision precision must be between 1 and 100".to_string());
152    }
153    let p = p as usize;
154    if !x.is_finite() {
155        return Ok(format_number_js(x));
156    }
157    if x == 0.0 {
158        return Ok(if p == 1 {
159            "0".to_string()
160        } else {
161            format!("{:.*}", p - 1, 0.0)
162        });
163    }
164    // Round to p significant digits first; the decimal exponent decides
165    // between fixed and exponential form (JS: e < -6 or e >= p → exponential).
166    // Rust exponential formatting of finite nonzero f64 always has an integer
167    // exponent (at most three decimal digits), even after rounding.
168    let sci = format!("{:.*e}", p - 1, x);
169    let (_, exponent) = sci
170        .split_once('e')
171        .expect("finite exponential formatting includes an exponent");
172    let exp: i32 = exponent.parse().expect("an f64 decimal exponent fits i32");
173    if exp < -6 || exp >= p as i32 {
174        return Ok(jsify_exponent(&sci));
175    }
176    let fraction_digits = (p as i32 - 1 - exp).max(0) as usize;
177    Ok(format!("{x:.fraction_digits$}"))
178}
179
180/// JS-style exponent spelling: `1.5e3` → `1.5e+3` (negative exponents
181/// already carry their sign).
182fn jsify_exponent(s: &str) -> String {
183    match s.find('e') {
184        Some(i) if !s[i + 1..].starts_with('-') => format!("{}e+{}", &s[..i], &s[i + 1..]),
185        _ => s.to_string(),
186    }
187}
188
189/// JS-spec `Number#toString(radix)` for radix 2–36.
190///
191/// The integer part is exact at any magnitude; the fraction part emits up
192/// to 32 digits and stops (JS prints the shortest uniquely-identifying
193/// fraction, which can be longer or shorter).
194pub fn to_string_radix_js(x: f64, radix: f64) -> Result<String, String> {
195    let r = radix.trunc();
196    if !(2.0..=36.0).contains(&r) || radix.is_nan() {
197        return Err("toString radix must be between 2 and 36".to_string());
198    }
199    let r = r as u32;
200    if r == 10 || !x.is_finite() {
201        return Ok(format_number_js(x));
202    }
203    if x == 0.0 {
204        return Ok("0".to_string());
205    }
206
207    let negative = x < 0.0;
208    let magnitude = x.abs();
209    let integer = magnitude.trunc();
210    let mut fraction = magnitude - integer;
211
212    // f64 → BigInt is exact for any finite value, so arbitrarily large
213    // integer parts convert without drift.
214    let int_digits = num_bigint::BigInt::from_f64(integer)
215        .expect("finite integer magnitude converts to BigInt")
216        .to_str_radix(r);
217    let mut out = String::new();
218    if negative {
219        out.push('-');
220    }
221    out.push_str(&int_digits);
222
223    if fraction > 0.0 {
224        out.push('.');
225        for _ in 0..32 {
226            // fraction is in [0, 1). Even its largest f64 value times the
227            // exact integer radix is below the midpoint between r and its previous
228            // representable value, so rounding
229            // cannot produce r. Subtracting trunc below preserves [0, 1).
230            fraction *= f64::from(r);
231            let digit = fraction.trunc() as u32;
232            out.push(
233                char::from_digit(digit, r).expect("fraction digit is below the validated radix"),
234            );
235            fraction -= fraction.trunc();
236            if fraction == 0.0 {
237                break;
238            }
239        }
240    }
241    Ok(out)
242}
243
244/// `parseInt(s, radix)` with the radix argument as JavaScript reads it:
245/// `ToInt32`, where 0 (including `NaN` and an omitted radix) means "detect a
246/// `0x` prefix, else base 10". Any other value outside 2–36 makes the result
247/// `NaN`.
248pub fn parse_int_with_radix(input: &str, radix: f64) -> f64 {
249    match u32::try_from(to_int32(radix)) {
250        Ok(radix @ (0 | 2..=36)) => parse_int_js(input, radix),
251        _ => f64::NAN,
252    }
253}
254
255/// JS-spec `parseInt(s, radix)` for an already-converted radix.
256///
257/// `radix == 0` means auto-detect: `0x` / `0X` prefix → base 16,
258/// otherwise base 10. `radix == 16` also accepts the `0x` prefix.
259/// Other radices in `[2, 36]` are taken literally; out-of-range
260/// radices return `NaN`.
261pub fn parse_int_js(input: &str, radix: u32) -> f64 {
262    let s = input.trim_start_matches(is_js_whitespace);
263    if s.is_empty() {
264        return f64::NAN;
265    }
266    let (sign, rest) = strip_sign(s);
267    if rest.is_empty() {
268        return f64::NAN;
269    }
270
271    let (effective_radix, digits) = if radix == 0 || radix == 16 {
272        if let Some(after_prefix) = strip_hex_prefix(rest) {
273            (16u32, after_prefix)
274        } else if radix == 0 {
275            (10, rest)
276        } else {
277            (16, rest)
278        }
279    } else if (2..=36).contains(&radix) {
280        (radix, rest)
281    } else {
282        return f64::NAN;
283    };
284
285    let mut consumed = 0usize;
286    let mut value: f64 = 0.0;
287    for ch in digits.chars() {
288        let Some(digit) = ch.to_digit(effective_radix) else {
289            break;
290        };
291        value = value * (effective_radix as f64) + (digit as f64);
292        consumed += ch.len_utf8();
293    }
294    if consumed == 0 {
295        return f64::NAN;
296    }
297    sign * value
298}
299
300pub fn parse_float_js(input: &str) -> f64 {
301    let s = input.trim_start_matches(is_js_whitespace);
302    if s.is_empty() {
303        return f64::NAN;
304    }
305    let (sign, rest) = strip_sign(s);
306    if rest.is_empty() {
307        return f64::NAN;
308    }
309    if let Some(remainder) = rest.strip_prefix("Infinity") {
310        // prefix rule: trailing junk is fine, so "Infinity" prefix suffices
311        let _ = remainder;
312        return sign * f64::INFINITY;
313    }
314
315    let prefix = float_prefix(rest);
316    if prefix.is_empty() {
317        return f64::NAN;
318    }
319    if prefix == "." {
320        return f64::NAN;
321    }
322    match prefix.parse::<f64>() {
323        Ok(v) => sign * v,
324        Err(_) => f64::NAN,
325    }
326}
327
328/// JS-spec `Number(s)` / `ToNumber` on a string.
329///
330/// Unlike [`parse_float_js`] (prefix rule) the *entire* trimmed string must be
331/// a valid `StringNumericLiteral`; trailing junk yields `NaN`. Empty/whitespace
332/// is `0`. Handles the `0x`/`0o`/`0b` integer-literal prefixes — these take no
333/// sign and no fractional/exponent part, per the grammar.
334pub fn string_to_number_js(input: &str) -> f64 {
335    let s = input.trim_matches(is_js_whitespace);
336    if s.is_empty() {
337        return 0.0;
338    }
339
340    if let Some(rest) = strip_radix_prefix(s, b'x') {
341        return parse_radix_strict(rest, 16);
342    }
343    if let Some(rest) = strip_radix_prefix(s, b'o') {
344        return parse_radix_strict(rest, 8);
345    }
346    if let Some(rest) = strip_radix_prefix(s, b'b') {
347        return parse_radix_strict(rest, 2);
348    }
349
350    let (sign, rest) = strip_sign(s);
351    if rest == "Infinity" {
352        return sign * f64::INFINITY;
353    }
354    // Rust's `f64::from_str` also accepts `inf`/`infinity`/`nan`, which JS
355    // rejects; gate on the decimal-literal charset to exclude them.
356    if !rest.bytes().all(is_decimal_literal_byte) {
357        return f64::NAN;
358    }
359    match rest.parse::<f64>() {
360        Ok(v) => sign * v,
361        Err(_) => f64::NAN,
362    }
363}
364
365fn strip_radix_prefix(s: &str, marker: u8) -> Option<&str> {
366    let bytes = s.as_bytes();
367    if bytes.len() >= 2 && bytes[0] == b'0' && bytes[1] | 0x20 == marker {
368        Some(&s[2..])
369    } else {
370        None
371    }
372}
373
374fn parse_radix_strict(digits: &str, radix: u32) -> f64 {
375    if digits.is_empty() {
376        return f64::NAN;
377    }
378    let mut value = 0.0_f64;
379    for ch in digits.chars() {
380        let Some(d) = ch.to_digit(radix) else {
381            return f64::NAN;
382        };
383        value = value * f64::from(radix) + f64::from(d);
384    }
385    value
386}
387
388fn is_decimal_literal_byte(b: u8) -> bool {
389    b.is_ascii_digit() || matches!(b, b'.' | b'e' | b'E' | b'+' | b'-')
390}
391
392fn strip_sign(s: &str) -> (f64, &str) {
393    if let Some(rest) = s.strip_prefix('+') {
394        (1.0, rest)
395    } else if let Some(rest) = s.strip_prefix('-') {
396        (-1.0, rest)
397    } else {
398        (1.0, s)
399    }
400}
401
402/// The digits after a `0x`/`0X` prefix. A bare prefix leaves none, which
403/// `parse_int_js` reports as `NaN`, as JavaScript does.
404fn strip_hex_prefix(s: &str) -> Option<&str> {
405    s.strip_prefix("0x").or_else(|| s.strip_prefix("0X"))
406}
407
408fn float_prefix(s: &str) -> &str {
409    let bytes = s.as_bytes();
410    let mut i = 0usize;
411    let n = bytes.len();
412
413    let int_start = i;
414    while i < n && bytes[i].is_ascii_digit() {
415        i += 1;
416    }
417    let int_end = i;
418
419    let mut frac_end = int_end;
420    if i < n && bytes[i] == b'.' {
421        i += 1;
422        let frac_start = i;
423        while i < n && bytes[i].is_ascii_digit() {
424            i += 1;
425        }
426        if frac_start == i && int_end == int_start {
427            // Lone `.` with no digits on either side — not a float.
428            return "";
429        }
430        frac_end = i;
431    } else if int_end == int_start {
432        return "";
433    }
434
435    // Only consume exponent if well-formed ([eE][+-]?digit+); otherwise roll back.
436    let exp_start = i;
437    if i < n && (bytes[i] == b'e' || bytes[i] == b'E') {
438        let mut j = i + 1;
439        if j < n && (bytes[j] == b'+' || bytes[j] == b'-') {
440            j += 1;
441        }
442        let digits_start = j;
443        while j < n && bytes[j].is_ascii_digit() {
444            j += 1;
445        }
446        if j > digits_start {
447            i = j;
448        } else {
449            i = exp_start;
450        }
451    }
452
453    let end = if i > frac_end { i } else { frac_end };
454    &s[..end]
455}
456
457/// ECMAScript WhiteSpace and LineTerminator code points.
458pub(crate) fn is_js_whitespace(value: char) -> bool {
459    matches!(value, '\u{0009}'..='\u{000D}' | '\u{0020}' | '\u{00A0}' | '\u{1680}'
460        | '\u{2000}'..='\u{200A}' | '\u{2028}' | '\u{2029}' | '\u{202F}' | '\u{205F}'
461        | '\u{3000}' | '\u{FEFF}')
462}
463
464pub(crate) fn pow_js(base: f64, exponent: f64) -> f64 {
465    if exponent.is_nan() || (base.abs() == 1.0 && exponent.is_infinite()) {
466        return f64::NAN;
467    }
468    base.powf(exponent)
469}
470
471#[cfg(test)]
472mod tests {
473    use super::{parse_float_js, parse_int_js, string_to_number_js, to_fixed_js, to_uint8};
474
475    #[test]
476    fn to_fixed_matches_node_including_exact_ties() {
477        // Expected strings are Node 24's.
478        let cases: [(f64, f64, &str); 22] = [
479            (2.25, 1.0, "2.3"),
480            (0.125, 2.0, "0.13"),
481            (2.5, 0.0, "3"),
482            (-2.25, 1.0, "-2.3"),
483            (0.5, 0.0, "1"),
484            (1.5, 0.0, "2"),
485            (-0.5, 0.0, "-1"),
486            (9.5, 0.0, "10"),
487            (99.995, 2.0, "100.00"),
488            (0.0625, 3.0, "0.063"),
489            (1.005, 2.0, "1.00"),
490            (1.45, 1.0, "1.4"),
491            (8.345, 2.0, "8.35"),
492            (-0.0001, 2.0, "-0.00"),
493            (-0.0, 2.0, "0.00"),
494            (0.0, 0.0, "0"),
495            (123.456, 10.0, "123.4560000000"),
496            (0.1, 20.0, "0.10000000000000000555"),
497            (5e-324, 2.0, "0.00"),
498            (100000000000000070000.0, 1.0, "100000000000000065536.0"),
499            (0.15, 1.0, "0.1"),
500            (1e-7, 8.0, "0.00000010"),
501        ];
502        for (x, digits, expected) in cases {
503            assert_eq!(
504                to_fixed_js(x, digits).unwrap(),
505                expected,
506                "({x}).toFixed({digits})"
507            );
508        }
509    }
510
511    #[test]
512    fn to_uint8_wraps_modulo_256() {
513        assert_eq!(to_uint8(7.0), 7);
514        assert_eq!(to_uint8(256.0), 0);
515        assert_eq!(to_uint8(257.9), 1);
516        assert_eq!(to_uint8(-1.0), 255);
517        assert_eq!(to_uint8(-1.5), 255);
518        assert_eq!(to_uint8(-256.0), 0);
519        assert_eq!(to_uint8(-0.5), 0);
520        assert_eq!(to_uint8(f64::NAN), 0);
521        assert_eq!(to_uint8(f64::INFINITY), 0);
522        assert_eq!(to_uint8(f64::NEG_INFINITY), 0);
523        assert_eq!(to_uint8(2f64.powi(60) + 512.0), 0);
524    }
525
526    #[test]
527    fn formatter_invariants_hold_at_float_boundaries() {
528        let below_one = f64::from_bits(1.0_f64.to_bits() - 1);
529        for radix in 2..=36 {
530            for value in [below_one, 0.5, f64::MIN_POSITIVE, f64::from_bits(1)] {
531                let text = super::to_string_radix_js(value, f64::from(radix)).unwrap();
532                if radix != 10 {
533                    assert!(text.chars().all(|c| c == '.' || c.is_digit(radix)));
534                }
535            }
536            assert!(super::to_string_radix_js(f64::MAX, f64::from(radix)).is_ok());
537        }
538        assert_eq!(
539            super::to_precision_js(f64::from_bits(1), 1.0).unwrap(),
540            "5e-324"
541        );
542        assert_eq!(super::to_precision_js(f64::MAX, 1.0).unwrap(), "2e+308");
543        assert_eq!(super::to_precision_js(-0.0, 3.0).unwrap(), "0.00");
544    }
545
546    #[test]
547    fn parse_int_decimal_prefix() {
548        assert_eq!(parse_int_js("42", 10), 42.0);
549        assert_eq!(parse_int_js("42px", 10), 42.0);
550        assert_eq!(parse_int_js("  42  ", 10), 42.0);
551    }
552
553    #[test]
554    fn parse_int_negative_and_positive() {
555        assert_eq!(parse_int_js("-42", 10), -42.0);
556        assert_eq!(parse_int_js("+42", 10), 42.0);
557    }
558
559    #[test]
560    fn parse_int_hex_with_explicit_radix_16() {
561        assert_eq!(parse_int_js("0xff", 16), 255.0);
562        assert_eq!(parse_int_js("0XFF", 16), 255.0);
563        assert_eq!(parse_int_js("ff", 16), 255.0);
564    }
565
566    #[test]
567    fn parse_int_radix_zero_auto_detects_hex() {
568        assert_eq!(parse_int_js("0xff", 0), 255.0);
569        assert_eq!(parse_int_js("010", 0), 10.0); // ES5+: NOT octal
570        assert_eq!(parse_int_js("42", 0), 42.0);
571    }
572
573    #[test]
574    fn parse_int_no_digits_is_nan() {
575        assert!(parse_int_js("abc", 10).is_nan());
576        assert!(parse_int_js("", 10).is_nan());
577        assert!(parse_int_js("   ", 10).is_nan());
578        assert!(parse_int_js("0xZZ", 16).is_nan());
579    }
580
581    #[test]
582    fn parse_int_invalid_radix_is_nan() {
583        assert!(parse_int_js("42", 1).is_nan());
584        assert!(parse_int_js("42", 37).is_nan());
585    }
586
587    #[test]
588    fn parse_float_basic() {
589        assert_eq!(parse_float_js("1.5"), 1.5);
590        assert_eq!(parse_float_js("1.5x"), 1.5);
591        assert_eq!(parse_float_js(".5"), 0.5);
592        assert_eq!(parse_float_js("42"), 42.0);
593    }
594
595    #[test]
596    fn parse_float_negative_and_positive() {
597        assert_eq!(parse_float_js("-1.5"), -1.5);
598        assert_eq!(parse_float_js("+1.5"), 1.5);
599    }
600
601    #[test]
602    fn parse_float_exponent() {
603        assert_eq!(parse_float_js("1e3"), 1000.0);
604        assert_eq!(parse_float_js("1.5e2"), 150.0);
605        assert_eq!(parse_float_js("1.5e+2"), 150.0);
606        assert_eq!(parse_float_js("1.5e-2"), 0.015);
607    }
608
609    #[test]
610    fn parse_float_infinity() {
611        assert_eq!(parse_float_js("Infinity"), f64::INFINITY);
612        assert_eq!(parse_float_js("-Infinity"), f64::NEG_INFINITY);
613    }
614
615    #[test]
616    fn parse_float_no_digits_is_nan() {
617        assert!(parse_float_js("abc").is_nan());
618        assert!(parse_float_js(".").is_nan());
619        assert!(parse_float_js("").is_nan());
620        assert!(parse_float_js("   ").is_nan());
621    }
622
623    #[test]
624    fn parse_float_skips_whitespace() {
625        assert_eq!(parse_float_js("  1.5  "), 1.5);
626    }
627
628    #[test]
629    fn parse_float_partial_exponent_keeps_mantissa() {
630        assert_eq!(parse_float_js("1.5e"), 1.5);
631    }
632
633    #[test]
634    fn number_decimal_and_scientific() {
635        assert_eq!(string_to_number_js("123"), 123.0);
636        assert_eq!(string_to_number_js("3.5"), 3.5);
637        assert_eq!(string_to_number_js("1e3"), 1000.0);
638        assert_eq!(string_to_number_js(".5"), 0.5);
639        assert_eq!(string_to_number_js("-42"), -42.0);
640        assert_eq!(string_to_number_js("  7  "), 7.0);
641    }
642
643    #[test]
644    fn number_radix_prefixes() {
645        assert_eq!(string_to_number_js("0xff"), 255.0);
646        assert_eq!(string_to_number_js("0XFF"), 255.0);
647        assert_eq!(string_to_number_js("0o17"), 15.0);
648        assert_eq!(string_to_number_js("0O17"), 15.0);
649        assert_eq!(string_to_number_js("0b101"), 5.0);
650        assert_eq!(string_to_number_js("0B101"), 5.0);
651        assert_eq!(string_to_number_js("  0xff  "), 255.0);
652    }
653
654    #[test]
655    fn number_empty_is_zero() {
656        assert_eq!(string_to_number_js(""), 0.0);
657        assert_eq!(string_to_number_js("   "), 0.0);
658    }
659
660    #[test]
661    fn number_infinity() {
662        assert_eq!(string_to_number_js("Infinity"), f64::INFINITY);
663        assert_eq!(string_to_number_js("+Infinity"), f64::INFINITY);
664        assert_eq!(string_to_number_js("-Infinity"), f64::NEG_INFINITY);
665    }
666
667    #[test]
668    fn number_rejects_trailing_junk_and_bad_radix() {
669        assert!(string_to_number_js("12px").is_nan());
670        assert!(string_to_number_js("0xZZ").is_nan());
671        assert!(string_to_number_js("0x").is_nan());
672        assert!(string_to_number_js("0b12").is_nan());
673        assert!(string_to_number_js("0xff.5").is_nan());
674    }
675
676    #[test]
677    fn number_rejects_signed_radix_literals() {
678        // Sign is not part of the non-decimal integer-literal grammar.
679        assert!(string_to_number_js("-0xff").is_nan());
680        assert!(string_to_number_js("+0b101").is_nan());
681    }
682
683    #[test]
684    fn number_rejects_rust_only_float_spellings() {
685        // `f64::from_str` accepts these; JS `Number` does not.
686        assert!(string_to_number_js("inf").is_nan());
687        assert!(string_to_number_js("infinity").is_nan());
688        assert!(string_to_number_js("nan").is_nan());
689        assert!(string_to_number_js("NaN").is_nan());
690    }
691}