Skip to main content

meta_language/translation/
js_number.rs

1//! ECMAScript `Number::toString` for binary64.
2//!
3//! The JavaScript frontend keeps a Number literal as `String(value)`, the
4//! text JavaScript prints, so both runtimes agree on it. Rust's `{:e}` gives
5//! the shortest digits that read back as the value; ECMAScript picks the
6//! even neighbour where Rust rounds a tie away from zero.
7//!
8//! The emitted Rust prelude (`ml_number::js_number`) is the same algorithm.
9
10/// `String(x)` for a JavaScript Number `x`.
11#[must_use]
12pub fn js_number(x: f64) -> String {
13    if x.is_nan() {
14        return String::from("NaN");
15    }
16    if x.is_infinite() {
17        return String::from(if x > 0.0 { "Infinity" } else { "-Infinity" });
18    }
19    if x == 0.0 {
20        return String::from("0");
21    }
22    let (mut digits, n) = decimal(&format!("{:e}", x.abs()));
23    let (exact, _) = decimal(&format!("{:.800e}", x.abs()));
24    let rest = &exact[digits.len().min(exact.len())..];
25    if digits.len() > 1 && rest.starts_with('5') && rest[1..].bytes().all(|b| b == b'0') {
26        let last = digits.as_bytes()[digits.len() - 1] - b'0';
27        let down = exact[..digits.len()].to_string();
28        let other = if digits == down {
29            increment(&down)
30        } else {
31            Some(down)
32        };
33        if let Some(other) = other {
34            let back = format!("{}.{}e{}", &other[..1], &other[1..], n - 1);
35            if last % 2 == 1 && back.parse::<f64>() == Ok(x.abs()) {
36                digits = other.trim_end_matches('0').to_string();
37            }
38        }
39    }
40    let k = i32::try_from(digits.len()).unwrap_or(i32::MAX);
41    let width = |count: i32| usize::try_from(count).unwrap_or(0);
42    let body = if k <= n && n <= 21 {
43        format!("{digits}{}", "0".repeat(width(n - k)))
44    } else if 0 < n && n <= 21 {
45        format!("{}.{}", &digits[..width(n)], &digits[width(n)..])
46    } else if -6 < n && n <= 0 {
47        format!("0.{}{digits}", "0".repeat(width(-n)))
48    } else {
49        let sign = if n - 1 < 0 { '-' } else { '+' };
50        let power = (n - 1).abs();
51        if k == 1 {
52            format!("{digits}e{sign}{power}")
53        } else {
54            format!("{}.{}e{sign}{power}", &digits[..1], &digits[1..])
55        }
56    };
57    if x < 0.0 { format!("-{body}") } else { body }
58}
59
60/// `String(Number(text))` for the text of a JavaScript numeric literal
61/// without separators or suffix: decimal, or `0x`, `0o`, `0b` integers.
62#[must_use]
63pub fn js_number_literal(text: &str) -> Option<String> {
64    let lower = text.to_ascii_lowercase();
65    let value = if ["0x", "0o", "0b"]
66        .iter()
67        .any(|prefix| lower.starts_with(prefix))
68    {
69        super::decimal::Decimal::parse(&lower)?
70            .digits
71            .parse::<f64>()
72            .ok()?
73    } else {
74        lower.parse::<f64>().ok()?
75    };
76    Some(js_number(value))
77}
78
79/// A decimal digit string plus one, or `None` when it gains a digit.
80fn increment(digits: &str) -> Option<String> {
81    let mut bytes = digits.as_bytes().to_vec();
82    for index in (0..bytes.len()).rev() {
83        if bytes[index] == b'9' {
84            bytes[index] = b'0';
85        } else {
86            bytes[index] += 1;
87            return String::from_utf8(bytes).ok();
88        }
89    }
90    None
91}
92
93/// The significant digits without trailing zeros, and the exponent `n` with
94/// value `0.digits × 10^n`, of Rust's `{:e}` text.
95fn decimal(text: &str) -> (String, i32) {
96    let (mantissa, exponent) = text.split_once('e').unwrap_or((text, "0"));
97    let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
98    let digits = digits.trim_end_matches('0').to_string();
99    (
100        if digits.is_empty() {
101            String::from("0")
102        } else {
103            digits
104        },
105        exponent.parse::<i32>().unwrap_or(0) + 1,
106    )
107}