use std::fmt::Write as _;
pub fn to_ecma_string(x: f64) -> String {
if x.is_nan() {
return "NaN".to_string();
}
if x == 0.0 {
return "0".to_string();
}
if x < 0.0 {
return format!("-{}", to_ecma_string(-x));
}
if x.is_infinite() {
return "Infinity".to_string();
}
let (digits, n) = shortest_digits(x);
let k = digits.len() as i32;
render(&digits, k, n)
}
fn shortest_digits(x: f64) -> (String, i32) {
let mut buf = ryu::Buffer::new();
let s = buf.format_finite(x);
let (mantissa, exp10) = match s.split_once('e') {
Some((m, e)) => (m, e.parse::<i32>().unwrap_or(0)),
None => (s, 0),
};
let (int_part, frac_part) = mantissa.split_once('.').unwrap_or((mantissa, ""));
let (digits, n) = if int_part == "0" {
let lead_zeros = frac_part.len() - frac_part.trim_start_matches('0').len();
(frac_part[lead_zeros..].to_string(), -(lead_zeros as i32))
} else {
let mut d = String::with_capacity(int_part.len() + frac_part.len());
d.push_str(int_part);
d.push_str(frac_part);
(d, int_part.len() as i32)
};
let trimmed = digits.trim_end_matches('0');
let digits = if trimmed.is_empty() {
"0".to_string()
} else {
trimmed.to_string()
};
(digits, n + exp10)
}
fn render(digits: &str, k: i32, n: i32) -> String {
if k <= n && n <= 21 {
let mut s = String::with_capacity(n as usize);
s.push_str(digits);
for _ in 0..(n - k) {
s.push('0');
}
return s;
}
if 0 < n && n <= 21 {
let (int_part, frac_part) = digits.split_at(n as usize);
return format!("{int_part}.{frac_part}");
}
if -6 < n && n <= 0 {
let mut s = String::with_capacity((2 - n) as usize + digits.len());
s.push_str("0.");
for _ in 0..(-n) {
s.push('0');
}
s.push_str(digits);
return s;
}
let e = n - 1;
let sign = if e >= 0 { '+' } else { '-' };
let mut s = String::new();
if k == 1 {
let _ = write!(s, "{digits}e{sign}{}", e.abs());
} else {
let (first, rest) = digits.split_at(1);
let _ = write!(s, "{first}.{rest}e{sign}{}", e.abs());
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn matches_node_on_the_known_divergences() {
assert_eq!(to_ecma_string(100.0), "100"); assert_eq!(to_ecma_string(3_000_000_000.0), "3000000000"); assert_eq!(to_ecma_string(1e20), "100000000000000000000"); assert_eq!(to_ecma_string(1e-6), "0.000001"); assert_eq!(to_ecma_string(-0.0), "0"); }
#[test]
fn agrees_with_ryu_where_it_already_agreed() {
assert_eq!(to_ecma_string(1.5e300), "1.5e+300");
assert_eq!(to_ecma_string(1e21), "1e+21");
assert_eq!(to_ecma_string(1e-7), "1e-7");
assert_eq!(to_ecma_string(0.1), "0.1");
assert_eq!(to_ecma_string(5e-324), "5e-324");
assert_eq!(to_ecma_string(f64::MAX), "1.7976931348623157e+308");
}
#[test]
fn boundaries_are_exact() {
assert_eq!(to_ecma_string(1e20), "100000000000000000000");
assert_eq!(to_ecma_string(1e21), "1e+21");
assert_eq!(to_ecma_string(1e-6), "0.000001");
assert_eq!(to_ecma_string(1e-7), "1e-7");
assert_eq!(
to_ecma_string(123456789012345678901.0),
"123456789012345680000"
);
}
#[test]
fn integers_and_fractions() {
assert_eq!(to_ecma_string(0.0), "0");
assert_eq!(to_ecma_string(1.0), "1");
assert_eq!(to_ecma_string(-1.0), "-1");
assert_eq!(to_ecma_string(1.5), "1.5");
assert_eq!(to_ecma_string(-1.5), "-1.5");
assert_eq!(to_ecma_string(9007199254740992.0), "9007199254740992"); assert_eq!(to_ecma_string(-1e21), "-1e+21");
}
#[test]
fn non_finite_are_ecmascript_strings_not_json() {
assert_eq!(to_ecma_string(f64::NAN), "NaN");
assert_eq!(to_ecma_string(f64::INFINITY), "Infinity");
assert_eq!(to_ecma_string(f64::NEG_INFINITY), "-Infinity");
}
#[test]
fn round_trips_through_parse() {
let vals = [
1.0,
100.0,
0.1,
1e20,
1e21,
1e-6,
1e-7,
1.5e300,
5e-324,
f64::MAX,
9007199254740992.0,
123456789012345678901.0,
-42.75,
2.2250738585072014e-308,
];
for v in vals {
let s = to_ecma_string(v);
let back: f64 = s
.parse()
.unwrap_or_else(|e| panic!("{s} did not reparse: {e}"));
assert_eq!(back.to_bits(), v.to_bits(), "round trip failed for {s}");
}
}
}