fix: Number::toString 符合 ECMA-262(1e21 指数、radix RangeError、parseInt/Number 字符串解析)

This commit is contained in:
2026-08-08 20:39:55 +08:00
parent 71c9f78338
commit 32c0474959
4 changed files with 325 additions and 80 deletions
+198 -35
View File
@@ -182,7 +182,7 @@ pub fn to_number(val: JsValue) -> f64 {
}
if val.is_string() {
let s = unsafe { string_data(val) };
return s.parse::<f64>().unwrap_or(f64::NAN);
return parse_js_number(s);
}
if val.is_object() {
return f64::NAN;
@@ -190,6 +190,149 @@ pub fn to_number(val: JsValue) -> f64 {
f64::NAN
}
/// 按 ECMA-262 ToNumber 的 StringNumericLiteral 语法解析字符串。
///
/// 在 Rust `parse::<f64>` 之上补齐 JS 特有规则:空串为 0、`0x/0o/0b` 前缀按
/// 对应进制解析、精确匹配 `±Infinity`。Rust parse 对 inf/nan 大小写不敏感,
/// 需先按十进制字符集排除这些令牌(JS 只接受精确的 "Infinity")。
///
/// # 边界与前提
/// - trim 后空串 → 0
/// - 十六/八/二进制要求全部字符为有效数字,含非法字符 → NaN
/// - 十进制结果与 Rust parse 一致,溢出时归 ±inf / 0
fn parse_js_number(s: &str) -> f64 {
let t = s.trim();
if t.is_empty() {
return 0.0;
}
let lower = t.to_ascii_lowercase();
if let Some(digits) = lower.strip_prefix("0x") {
return parse_radix_int(digits, 16);
}
if let Some(digits) = lower.strip_prefix("0o") {
return parse_radix_int(digits, 8);
}
if let Some(digits) = lower.strip_prefix("0b") {
return parse_radix_int(digits, 2);
}
if t == "Infinity" || t == "+Infinity" {
return f64::INFINITY;
}
if t == "-Infinity" {
return f64::NEG_INFINITY;
}
// 十进制语法仅允许数字、符号、小数点与指数 e;含其它字符的令牌(如
// inf/nan 变体)不是合法 StringNumericLiteral,一律 NaN。
if !t
.chars()
.all(|c| c.is_ascii_digit() || c == '+' || c == '-' || c == '.' || c == 'e' || c == 'E')
{
return f64::NAN;
}
t.parse::<f64>().unwrap_or(f64::NAN)
}
/// 解析指定进制的无符号整数;全部字符必须是有效数字,否则返回 NaN。
///
/// 累积在 f64 中:2/8/16 是 2 的幂,逐位乘加即为正确舍入结果;十进制不适用。
fn parse_radix_int(s: &str, radix: u32) -> f64 {
if s.is_empty() {
return f64::NAN;
}
let mut v = 0.0f64;
for c in s.chars() {
match c.to_digit(radix) {
Some(d) => v = v * radix as f64 + d as f64,
None => return f64::NAN,
}
}
v
}
/// 把 f64 格式化为 ECMA-262 Number::toString 的字符串(含 NaN/±Infinity 专名)。
///
/// 对有限非零数,先经 ryu 最短表示取出有效数字与十进制指数(`format_finite`
/// 输出形如 `"123.45"` / `"1e21"` / `"0.001"`),再按规范分段重建:指数
/// `-6 < n <= 21` 时用定点表示,其余用科学计数法 `d.ddde±e`。替代原先
/// `(d as i64)` 直接转换——后者对 `>= 2^63` 的数值溢出。
///
/// # 步骤
/// 1. 取绝对值经 ryu `format_finite` 得最短表示 `s`
/// 2. 含 `e` 时解析出有效数字与指数,按四段规则重建(整数/带小数点/纯小数/科学计数)
/// 3. 不含 `e` 的定点形式仅需去掉整数末尾的 `.0` 后缀
///
/// # 边界与前提
/// - NaN → "NaN",±∞ → "±Infinity",±0 → "0"
/// - 负数先格式化绝对值再加 "-" 前缀
pub fn js_number_to_string(d: f64) -> String {
if d.is_nan() {
return "NaN".to_string();
}
if d.is_infinite() {
return if d.is_sign_positive() {
"Infinity".to_string()
} else {
"-Infinity".to_string()
};
}
if d == 0.0 {
return "0".to_string();
}
let neg = d.is_sign_negative();
let mut buf = ryu::Buffer::new();
let s = buf.format_finite(d.abs());
let mut out = String::new();
if let Some(e_pos) = s.find('e') {
let digits: Vec<char> = s[..e_pos].chars().filter(|c| *c != '.').collect();
let exp: i32 = s[e_pos + 1..].parse().unwrap_or(0);
let k = digits.len() as i32;
let n = exp + 1;
if n > -6 && n <= 21 {
// 定点:ryu 在此范围输出指数形式但 JS 要求十进制。
if n <= 0 {
out.push_str("0.");
for _ in 0..(-n) {
out.push('0');
}
out.extend(digits.iter());
} else if k <= n {
out.extend(digits.iter());
for _ in 0..(n - k) {
out.push('0');
}
} else {
out.extend(digits[..n as usize].iter());
out.push('.');
out.extend(digits[n as usize..].iter());
}
} else {
// 科学计数:首位 + (可选 "." + 剩余) + e[+/-]指数。
out.push(digits[0]);
if k > 1 {
out.push('.');
out.extend(digits[1..].iter());
}
out.push('e');
if n > 1 {
out.push('+');
}
out.push_str(&(n - 1).to_string());
}
} else if let Some(stripped) = s.strip_suffix(".0") {
out.push_str(stripped);
} else {
out.push_str(s);
}
if neg {
let mut res = String::with_capacity(out.len() + 1);
res.push('-');
res.push_str(&out);
res
} else {
out
}
}
/// ToUint32(ECMA-262 §7.1.6):对数值取模 2^32。NaN/±0/Infinity 归零。
pub fn to_uint32(val: JsValue) -> u32 {
let n = to_number(val);
@@ -218,22 +361,7 @@ pub fn push_to_string(val: JsValue, buf: &mut String) {
return;
}
if val.is_double() {
let d = val.as_double();
if d.is_nan() {
buf.push_str("NaN");
return;
}
if d.is_infinite() {
buf.push_str(if d.is_sign_positive() { "Infinity" } else { "-Infinity" });
return;
}
if d.is_finite() && d.fract() == 0.0 {
use std::fmt::Write;
let _ = write!(buf, "{}", d as i64);
return;
}
let mut ryubuf = ryu::Buffer::new();
buf.push_str(ryubuf.format(d));
buf.push_str(&js_number_to_string(val.as_double()));
return;
}
if val.is_bool() {
@@ -259,29 +387,14 @@ pub fn push_to_string(val: JsValue, buf: &mut String) {
/// 把原始值转成字符串(ToString 的原始值路径)。
///
/// 数值使用与 V8 一致的格式化(整数直接打印、有限数用 ryu 最短表示);
/// Object 在此返回占位符 `[object]`,完整路径见 [`to_string_full`]。
/// 数值走 ECMA-262 Number::toString 格式化;Object 在此返回占位符 `[object]`,
/// 完整路径见 [`to_string_full`]。
pub fn to_string(val: JsValue) -> String {
if val.is_int() {
return val.as_int().to_string();
}
if val.is_double() {
let d = val.as_double();
if d.is_nan() {
return "NaN".to_string();
}
if d.is_infinite() {
return if d.is_sign_positive() {
"Infinity".to_string()
} else {
"-Infinity".to_string()
};
}
if d.is_finite() && d.fract() == 0.0 {
return (d as i64).to_string();
}
let mut buf = ryu::Buffer::new();
return buf.format(d).to_string();
return js_number_to_string(val.as_double());
}
if val.is_bool() {
return val.as_bool().to_string();
@@ -638,3 +751,53 @@ pub fn to_string_full<H: VmHost>(val: JsValue, host: &mut H) -> Result<String, S
let primitive = to_primitive(val, ToPrimitiveHint::String, host)?;
Ok(to_string(primitive))
}
#[cfg(test)]
mod tests {
use super::*;
fn fmt(d: f64) -> String {
js_number_to_string(d)
}
#[test]
fn number_to_string_boundaries() {
assert_eq!(fmt(1e21), "1e+21");
assert_eq!(fmt(1e20), "100000000000000000000");
assert_eq!(fmt(123.45), "123.45");
assert_eq!(fmt(0.000001), "0.000001");
assert_eq!(fmt(0.0000001), "1e-7");
assert_eq!(fmt(1e15), "1000000000000000");
assert_eq!(fmt(0.1 + 0.2), "0.30000000000000004");
assert_eq!(fmt(0.0), "0");
assert_eq!(fmt(-0.0), "0");
assert_eq!(fmt(-123.45), "-123.45");
assert_eq!(fmt(-1e21), "-1e+21");
assert_eq!(fmt(1e16), "10000000000000000");
assert_eq!(fmt(f64::MAX), "1.7976931348623157e+308");
assert_eq!(fmt(5e-324), "5e-324");
assert_eq!(fmt(1.5e20), "150000000000000000000");
assert_eq!(fmt(f64::NAN), "NaN");
assert_eq!(fmt(f64::INFINITY), "Infinity");
assert_eq!(fmt(f64::NEG_INFINITY), "-Infinity");
}
#[test]
fn parse_js_number_rules() {
assert_eq!(parse_js_number(""), 0.0);
assert_eq!(parse_js_number(" "), 0.0);
assert_eq!(parse_js_number("0xa"), 10.0);
assert_eq!(parse_js_number("0X1f"), 31.0);
assert_eq!(parse_js_number("0b101"), 5.0);
assert_eq!(parse_js_number("0o17"), 15.0);
assert_eq!(parse_js_number("Infinity"), f64::INFINITY);
assert_eq!(parse_js_number("-Infinity"), f64::NEG_INFINITY);
assert!(parse_js_number("INFINITY").is_nan());
assert!(parse_js_number("infinity").is_nan());
assert!(parse_js_number("0x1g").is_nan());
assert!(parse_js_number("-0x1").is_nan());
assert_eq!(parse_js_number("1.5e3"), 1500.0);
assert_eq!(parse_js_number("-0"), -0.0);
assert!(parse_js_number("1abc").is_nan());
}
}