use oxc_syntax::{identifier::is_white_space, line_terminator::is_line_terminator};
pub trait StringToNumber {
fn string_to_number(&self) -> f64;
}
impl StringToNumber for &str {
fn string_to_number(&self) -> f64 {
let s = self.trim_matches(is_str_white_space_char);
match s {
"" => return 0.0,
"-Infinity" => return f64::NEG_INFINITY,
"Infinity" | "+Infinity" => return f64::INFINITY,
_ => {
let mut bytes = s.trim_start_matches(['-', '+']).bytes();
if bytes
.next()
.filter(|c| c.eq_ignore_ascii_case(&b'i'))
.and_then(|_| bytes.next().filter(|c| c.eq_ignore_ascii_case(&b'n')))
.and_then(|_| bytes.next().filter(|c| c.eq_ignore_ascii_case(&b'f')))
.is_some()
{
return f64::NAN;
}
}
}
let mut bytes = s.bytes();
if s.len() > 2 && bytes.next() == Some(b'0') {
let radix: u32 = match bytes.next().unwrap() | 32 {
b'x' => 16,
b'o' => 8,
b'b' => 2,
_ => 0,
};
if radix != 0 {
let s = &s[2..];
if let Ok(value) = u32::from_str_radix(s, radix) {
return f64::from(value);
}
let mut value: f64 = 0.0;
for c in bytes {
if let Some(digit) = char::from(c).to_digit(radix) {
value = value.mul_add(f64::from(radix), f64::from(digit));
} else {
return f64::NAN;
}
}
return value;
}
}
s.parse::<f64>().unwrap_or(f64::NAN)
}
}
fn is_str_white_space_char(c: char) -> bool {
is_white_space(c) || is_line_terminator(c)
}
#[cfg(test)]
mod tests {
use super::StringToNumber;
#[expect(clippy::float_cmp)]
#[test]
fn trims_ecmascript_whitespace_from_both_ends() {
const WHITESPACE: &str = "\u{0009}\u{000B}\u{000C}\u{0020}\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{202F}\u{205F}\u{3000}\u{FEFF}\u{000A}\u{000D}\u{2028}\u{2029}";
assert_eq!(format!("{WHITESPACE}1{WHITESPACE}").as_str().string_to_number(), 1.0);
assert_eq!(format!("{WHITESPACE}0x10{WHITESPACE}").as_str().string_to_number(), 16.0);
assert_eq!(
format!("{WHITESPACE}Infinity{WHITESPACE}").as_str().string_to_number(),
f64::INFINITY
);
assert_eq!(format!("{WHITESPACE}{WHITESPACE}").as_str().string_to_number(), 0.0);
}
#[test]
fn does_not_trim_non_ecmascript_whitespace() {
assert!("1\u{0085}".string_to_number().is_nan());
}
}