#[must_use]
pub fn trim_spaces(s: &str) -> &str {
s.trim_matches(' ')
}
fn is_c_space(b: u8) -> bool {
matches!(b, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
}
#[must_use]
pub fn is_decimal_digit(c: char) -> bool {
c.is_ascii_digit()
}
#[must_use]
pub fn is_ascii_alpha(c: char) -> bool {
c.is_ascii_alphabetic()
}
#[must_use]
pub fn is_ascii_alnum(c: char) -> bool {
c.is_ascii_alphanumeric()
}
fn parse_leading_chars(s: &str) -> &str {
let bytes = s.as_bytes();
let mut start = 0;
while start < bytes.len() {
let Some(b) = bytes.get(start).copied() else {
break;
};
if b == b' ' || b == b'+' || b == b'-' {
start = start.saturating_add(1);
} else {
break;
}
}
if start > 0
&& let Some(b) = bytes.get(start.saturating_sub(1)).copied()
&& b == b'-'
{
start = start.saturating_sub(1);
}
s.get(start..).unwrap_or_default()
}
fn scan_float_prefix(bytes: &[u8], start: usize) -> usize {
let mut i = start;
if i < bytes.len()
&& let Some(b) = bytes.get(i).copied()
&& (b == b'+' || b == b'-')
{
i = i.saturating_add(1);
}
let mut saw_digit = false;
while i < bytes.len() {
match bytes.get(i).copied() {
Some(b) if b.is_ascii_digit() => {
saw_digit = true;
i = i.saturating_add(1);
}
_ => break,
}
}
if i < bytes.len()
&& let Some(b) = bytes.get(i).copied()
&& b == b'.'
{
i = i.saturating_add(1);
while i < bytes.len() {
match bytes.get(i).copied() {
Some(b) if b.is_ascii_digit() => {
saw_digit = true;
i = i.saturating_add(1);
}
_ => break,
}
}
}
if saw_digit
&& i < bytes.len()
&& let Some(b) = bytes.get(i).copied()
&& (b == b'e' || b == b'E')
{
let mut j = i.saturating_add(1);
if j < bytes.len()
&& let Some(b) = bytes.get(j).copied()
&& (b == b'+' || b == b'-')
{
j = j.saturating_add(1);
}
if j < bytes.len()
&& let Some(b) = bytes.get(j).copied()
&& b.is_ascii_digit()
{
j = j.saturating_add(1);
while j < bytes.len() {
match bytes.get(j).copied() {
Some(b) if b.is_ascii_digit() => j = j.saturating_add(1),
_ => break,
}
}
i = j;
}
}
if saw_digit { i } else { start }
}
fn parse_scanned(bytes: &[u8], start: usize, end: usize) -> f64 {
if end <= start {
return 0.0;
}
let Some(slice) = bytes.get(start..end) else {
return 0.0;
};
let Ok(text) = std::str::from_utf8(slice) else {
return 0.0;
};
text.parse().unwrap_or(0.0)
}
#[must_use]
pub fn c_atof(input: &str) -> f64 {
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes.get(i).copied() {
Some(b) if is_c_space(b) => i = i.saturating_add(1),
_ => break,
}
}
let end = scan_float_prefix(bytes, i);
parse_scanned(bytes, i, end)
}
#[must_use]
pub fn string_to_double(input: &str) -> f64 {
let rest = parse_leading_chars(input);
let bytes = rest.as_bytes();
let end = scan_float_prefix(bytes, 0);
parse_scanned(bytes, 0, end)
}
#[must_use]
pub fn normalize_decimal_mark(s: &str) -> String {
s.replace(',', ".")
}
#[must_use]
pub fn js_to_number(input: &str) -> Option<f64> {
let trimmed = input.trim();
if trimmed.is_empty() {
return None;
}
if trimmed == "NaN" {
return Some(f64::NAN);
}
if trimmed == "Infinity" || trimmed == "+Infinity" {
return Some(f64::INFINITY);
}
if trimmed == "-Infinity" {
return Some(f64::NEG_INFINITY);
}
if let Some(hex) = trimmed
.strip_prefix("0x")
.or_else(|| trimmed.strip_prefix("0X"))
{
return i32::from_str_radix(hex, 16).ok().map(f64::from);
}
match trimmed.parse::<f64>() {
Ok(n) if n.is_finite() => Some(n),
Ok(n) if n.is_infinite() => Some(n),
_ => None,
}
}
#[must_use]
pub fn is_number(str: &str) -> bool {
let trimmed = trim_spaces(str);
if trimmed.is_empty() {
return false;
}
let chars: Vec<char> = trimmed.chars().collect();
let mut seen_mark = false;
let mut seen_exponent = false;
let mut i = 0;
while let Some(c) = chars.get(i).copied() {
match c {
'.' | ',' => {
if seen_mark {
return false;
}
seen_mark = true;
}
'-' | '+' if i == 0 => {}
'-' | '+' => return false,
'e' | 'E' => {
if seen_exponent {
return false;
}
i = i.saturating_add(1);
if !matches!(chars.get(i), Some('+' | '-')) {
return false;
}
seen_exponent = true;
}
_ if is_decimal_digit(c) => {}
_ => return false,
}
i = i.saturating_add(1);
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_number_grammar() {
let cases: &[(&str, bool)] = &[
("", false),
(" ", false),
("xyz00", false),
("1%", false),
("0x234", false),
("+123", true),
("-98765", true),
(" 345 ", true),
("-1e5", false),
("-2e", false),
("e-5", true),
("1e-9", true),
("-1.23e+23", true),
("1,000,000", false),
("560,024", true),
("0.023", true),
(".356089", true),
("0", true),
("0123", true),
("9876123", true),
];
for &(input, expected) in cases {
assert_eq!(is_number(input), expected, "is_number({input:?})");
}
}
#[test]
fn a_period_and_a_comma_are_the_same_token() {
for input in ["1.2.3", "1,2,3", "1.2,3", "1,2.3"] {
assert!(!is_number(input), "{input:?}");
}
for input in ["1.23", "1,23"] {
assert!(is_number(input), "{input:?}");
}
}
#[test]
fn c_atof_reads_a_prefix() {
assert_eq!(c_atof("1,2"), 1.0);
assert_eq!(c_atof("blooey"), 0.0);
assert_eq!(c_atof(""), 0.0);
assert_eq!(c_atof(" 12.5abc"), 12.5);
assert!((c_atof("-5.1234") - -5.1234).abs() < 1e-12);
assert_eq!(c_atof("1e+3"), 1000.0);
assert_eq!(c_atof("1e"), 1.0);
}
#[test]
fn string_to_double_skips_repeated_signs() {
assert_eq!(string_to_double("--100.0"), -100.0);
assert_eq!(string_to_double("+-100.0"), -100.0);
assert_eq!(string_to_double("++100.0"), 100.0);
assert_eq!(string_to_double("-+-100.0"), -100.0);
assert_eq!(string_to_double("invalid"), 0.0);
assert_eq!(string_to_double(" 100.0"), 100.0);
}
#[test]
fn js_to_number_rejects_trailing_junk() {
assert_eq!(js_to_number("2blooey"), None);
assert_eq!(js_to_number("1.2"), Some(1.2));
assert_eq!(js_to_number(""), None);
assert_eq!(js_to_number(" 7 "), Some(7.0));
}
#[test]
fn trimming_is_spaces_only() {
assert_eq!(trim_spaces(" a "), "a");
assert_eq!(trim_spaces("\ta\t"), "\ta\t");
assert_eq!(trim_spaces(" "), "");
}
}