use nom::{
IResult, Parser,
branch::alt,
bytes::complete::{tag, tag_no_case, take_while1},
character::complete::{char, one_of},
combinator::{map, map_res, opt, recognize, value},
sequence::{pair, preceded},
};
pub fn number(input: &str) -> IResult<&str, f64> {
alt((special_float, signed_number)).parse(input)
}
fn is_ident_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn special_float(input: &str) -> IResult<&str, f64> {
let (rest, val) = alt((
value(f64::INFINITY, preceded(opt(char('+')), tag_no_case("Inf"))),
value(f64::NEG_INFINITY, preceded(char('-'), tag_no_case("Inf"))),
value(f64::NAN, preceded(opt(one_of("+-")), tag_no_case("NaN"))),
))
.parse(input)?;
if rest.chars().next().is_some_and(is_ident_char) {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
}
Ok((rest, val))
}
fn signed_number(input: &str) -> IResult<&str, f64> {
map(
pair(opt(one_of("+-")), unsigned_number),
|(sign, value)| match sign {
Some('-') => -value,
_ => value,
},
)
.parse(input)
}
fn unsigned_number(input: &str) -> IResult<&str, f64> {
alt((
hexadecimal,
octal_prefixed,
octal_legacy,
decimal_float,
))
.parse(input)
}
fn hexadecimal(input: &str) -> IResult<&str, f64> {
map_res(
preceded(
alt((tag("0x"), tag("0X"))),
take_while1(|c: char| c.is_ascii_hexdigit()),
),
|digits: &str| i64::from_str_radix(digits, 16).map(|v| v as f64),
)
.parse(input)
}
fn octal_prefixed(input: &str) -> IResult<&str, f64> {
map_res(
preceded(
alt((tag("0o"), tag("0O"))),
take_while1(|c: char| matches!(c, '0'..='7')),
),
|digits: &str| i64::from_str_radix(digits, 8).map(|v| v as f64),
)
.parse(input)
}
fn octal_legacy(input: &str) -> IResult<&str, f64> {
let (remaining, _) = char('0')(input)?;
if remaining.is_empty() {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
}
let next_char = remaining.chars().next().unwrap();
if !matches!(next_char, '0'..='7') {
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
}
let (remaining, octal_digits) = take_while1(|c: char| matches!(c, '0'..='7'))(remaining)?;
if let Some(c) = remaining.chars().next()
&& (c == '.' || c == 'e' || c == 'E')
{
return Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Tag,
)));
}
let full_octal = format!("0{}", octal_digits);
match i64::from_str_radix(&full_octal, 8) {
Ok(v) => Ok((remaining, v as f64)),
Err(_) => Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::MapRes,
))),
}
}
fn decimal_float(input: &str) -> IResult<&str, f64> {
map_res(recognize(decimal_float_inner), |s: &str| s.parse::<f64>()).parse(input)
}
fn decimal_float_inner(input: &str) -> IResult<&str, &str> {
alt((
recognize((char('.'), decimal_digits, opt(exponent))),
recognize((
decimal_digits,
opt(pair(char('.'), opt(decimal_digits))),
exponent,
)),
recognize((decimal_digits, char('.'), opt(decimal_digits))),
recognize(decimal_digits),
))
.parse(input)
}
fn decimal_digits(input: &str) -> IResult<&str, &str> {
take_while1(|c: char| c.is_ascii_digit())(input)
}
fn exponent(input: &str) -> IResult<&str, &str> {
recognize((one_of("eE"), opt(one_of("+-")), decimal_digits)).parse(input)
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_number(input: &str, expected: f64) {
let result = number(input);
match result {
Ok((remaining, value)) => {
assert!(
remaining.is_empty(),
"Parser did not consume entire input '{}', remaining: '{}'",
input,
remaining
);
if expected.is_nan() {
assert!(
value.is_nan(),
"Expected NaN for input '{}', got {}",
input,
value
);
} else {
assert!(
(value - expected).abs() < f64::EPSILON || value == expected,
"For input '{}', expected {}, got {}",
input,
expected,
value
);
}
}
Err(e) => panic!("Failed to parse '{}': {:?}", input, e),
}
}
fn assert_number_partial(input: &str, expected: f64, expected_remaining: &str) {
let result = number(input);
match result {
Ok((remaining, value)) => {
assert_eq!(
remaining, expected_remaining,
"For input '{}', expected remaining '{}', got '{}'",
input, expected_remaining, remaining
);
if expected.is_nan() {
assert!(
value.is_nan(),
"Expected NaN for input '{}', got {}",
input,
value
);
} else {
assert!(
(value - expected).abs() < f64::EPSILON || value == expected,
"For input '{}', expected {}, got {}",
input,
expected,
value
);
}
}
Err(e) => panic!("Failed to parse '{}': {:?}", input, e),
}
}
fn assert_not_number(input: &str) {
let result = number(input);
assert!(
result.is_err(),
"Expected '{}' to fail parsing, but got {:?}",
input,
result
);
}
#[test]
fn test_integer() {
assert_number("1", 1.0);
assert_number("0", 0.0);
assert_number("42", 42.0);
assert_number("123", 123.0);
}
#[test]
fn test_float() {
assert_number(".5", 0.5);
assert_number("5.", 5.0);
assert_number("123.4567", 123.4567);
assert_number("4.23", 4.23);
assert_number(".3", 0.3);
}
#[test]
fn test_scientific() {
assert_number("5e-3", 0.005);
assert_number("5e3", 5000.0);
assert_number("5e+3", 5000.0);
assert_number("1e10", 1e10);
assert_number("2.5E-3", 0.0025);
assert_number("1e1", 10.0);
assert_number("1e-1", 0.1);
assert_number("1.0e1", 10.0);
assert_number("1e01", 10.0);
assert_number("1E01", 10.0);
assert_number("1.e2", 100.0);
}
#[test]
fn test_hex() {
assert_number("0xc", 12.0);
assert_number("0x123", 291.0);
assert_number("0X2A", 42.0);
assert_number("0x1F", 31.0);
assert_number("0xA", 10.0);
}
#[test]
fn test_octal() {
assert_number("0755", 493.0); assert_number("0644", 420.0); assert_number("07", 7.0);
assert_number("010", 8.0); assert_number("0777", 511.0); }
#[test]
fn test_octal_prefixed() {
assert_number("0o0", 0.0);
assert_number("0O0", 0.0);
assert_number("0o7", 7.0);
assert_number("0o10", 8.0);
assert_number("0o755", 493.0);
assert_number("0o777", 511.0);
assert_number("0O755", 493.0);
}
#[test]
fn test_octal_signed() {
assert_number("-0755", -493.0);
assert_number("+0755", 493.0);
assert_number("-0o755", -493.0);
assert_number("+0o755", 493.0);
}
#[test]
fn test_signed() {
assert_number("-0755", -493.0);
assert_number("-1", -1.0);
assert_number("+1", 1.0);
assert_number("-1e1", -10.0);
assert_number("-1e-1", -0.1);
assert_number("+5.5e-3", 0.0055);
}
#[test]
fn test_special_floats() {
assert_number("NaN", f64::NAN);
assert_number("nAN", f64::NAN);
assert_number("Inf", f64::INFINITY);
assert_number("iNf", f64::INFINITY);
assert_number("+Inf", f64::INFINITY);
assert_number("-Inf", f64::NEG_INFINITY);
}
#[test]
fn test_partial_parse() {
assert_number_partial("NaN 123", f64::NAN, " 123");
assert_number_partial("123abc", 123.0, "abc");
}
#[test]
fn test_not_numbers() {
assert_not_number("."); assert_not_number(""); }
#[test]
fn test_edge_cases() {
assert_number("0", 0.0);
assert_number("0.5", 0.5);
assert_number("0e5", 0.0);
}
}