use crate::ast::{Number, Value};
use crate::error::{Error, Result, Span};
#[inline]
pub(super) fn parse_number_token(original_input: &str, span: Span) -> Result<Value> {
let number_slice = &original_input[span.start..span.end];
let has_trailing_decimal = number_slice.ends_with('.');
if let Some(parsed_int) = parse_alternative_number_format(number_slice, span)? {
return Ok(Value::Number(Number::Integer(parsed_int)));
}
let normalized_number = if has_trailing_decimal {
format!("{number_slice}0")
} else {
number_slice.to_string()
};
let number_value = match normalized_number.parse::<f64>() {
Ok(f) => {
let has_no_fract = f.fract() == 0.0;
let is_finite = f.is_finite();
let in_range = f.is_finite()
&& f >= (i64::MIN as f64)
&& f <= (i64::MAX as f64)
&& (f as i64) as f64 == f;
let is_neg_zero = f.is_sign_negative() && f == 0.0;
let has_decimal_point = number_slice.contains('.');
if is_neg_zero && !has_decimal_point {
return Ok(Value::Number(Number::Integer(0)));
}
if has_no_fract
&& is_finite
&& in_range
&& !is_neg_zero
&& (!has_decimal_point || has_trailing_decimal)
{
Number::Integer(f as i64)
} else {
Number::Float(f)
}
}
Err(_) => {
return Err(Error::InvalidNumber(span.start));
}
};
Ok(Value::Number(number_value))
}
fn parse_alternative_number_format(input: &str, span: Span) -> Result<Option<i64>> {
let cleaned = input.replace('_', "");
let (sign, number_str) = if let Some(rest) = cleaned.strip_prefix('-') {
(-1i64, rest)
} else if let Some(rest) = cleaned.strip_prefix('+') {
(1i64, rest)
} else {
(1i64, cleaned.as_str())
};
let parsed_value = if number_str.starts_with("0x") || number_str.starts_with("0X") {
let hex_str = &number_str[2..];
if hex_str.is_empty() {
return Err(Error::InvalidNumber(span.start));
}
match i64::from_str_radix(hex_str, 16) {
Ok(val) => val,
Err(_) => return Err(Error::InvalidNumber(span.start)),
}
} else if number_str.starts_with("0o") || number_str.starts_with("0O") {
let octal_str = &number_str[2..];
if octal_str.is_empty() {
return Err(Error::InvalidNumber(span.start));
}
match i64::from_str_radix(octal_str, 8) {
Ok(val) => val,
Err(_) => return Err(Error::InvalidNumber(span.start)),
}
} else if number_str.starts_with("0b") || number_str.starts_with("0B") {
let binary_str = &number_str[2..];
if binary_str.is_empty() {
return Err(Error::InvalidNumber(span.start));
}
match i64::from_str_radix(binary_str, 2) {
Ok(val) => val,
Err(_) => return Err(Error::InvalidNumber(span.start)),
}
} else if input.contains('_')
&& !input.contains('.')
&& !input.contains('e')
&& !input.contains('E')
{
match number_str.parse::<i64>() {
Ok(val) => val,
Err(_) => return Err(Error::InvalidNumber(span.start)),
}
} else {
return Ok(None);
};
let result = if sign == -1 {
parsed_value
.checked_neg()
.ok_or(Error::InvalidNumber(span.start))?
} else {
parsed_value
};
Ok(Some(result))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Span;
#[test]
fn test_parse_number_token_integers() {
assert_eq!(parse_number_token("42", Span::new(0, 2)).unwrap(), Value::Number(Number::Integer(42)));
assert_eq!(parse_number_token("0", Span::new(0, 1)).unwrap(), Value::Number(Number::Integer(0)));
assert_eq!(parse_number_token("123", Span::new(0, 3)).unwrap(), Value::Number(Number::Integer(123)));
assert_eq!(parse_number_token("-42", Span::new(0, 3)).unwrap(), Value::Number(Number::Integer(-42)));
assert_eq!(parse_number_token("-0", Span::new(0, 2)).unwrap(), Value::Number(Number::Integer(0)));
}
#[test]
fn test_parse_number_token_floats() {
assert_eq!(parse_number_token("3.14", Span::new(0, 4)).unwrap(), Value::Number(Number::Float(3.14)));
assert_eq!(parse_number_token("0.5", Span::new(0, 3)).unwrap(), Value::Number(Number::Float(0.5)));
assert_eq!(parse_number_token("-1.5", Span::new(0, 4)).unwrap(), Value::Number(Number::Float(-1.5)));
assert_eq!(parse_number_token("1e10", Span::new(0, 4)).unwrap(), Value::Number(Number::Integer(10_000_000_000)));
assert_eq!(parse_number_token("1E10", Span::new(0, 4)).unwrap(), Value::Number(Number::Integer(10_000_000_000)));
assert_eq!(parse_number_token("1e-5", Span::new(0, 4)).unwrap(), Value::Number(Number::Float(1e-5)));
assert_eq!(parse_number_token("2.5e3", Span::new(0, 5)).unwrap(), Value::Number(Number::Float(2500.0)));
}
#[test]
fn test_parse_number_token_trailing_decimal() {
assert_eq!(parse_number_token("1.", Span::new(0, 2)).unwrap(), Value::Number(Number::Integer(1)));
assert_eq!(parse_number_token("42.", Span::new(0, 3)).unwrap(), Value::Number(Number::Integer(42)));
assert_eq!(parse_number_token("-5.", Span::new(0, 3)).unwrap(), Value::Number(Number::Integer(-5)));
}
#[test]
fn test_parse_alternative_number_format_hex() {
assert_eq!(parse_alternative_number_format("0x10", Span::new(0, 4)).unwrap(), Some(16));
assert_eq!(parse_alternative_number_format("0X10", Span::new(0, 4)).unwrap(), Some(16));
assert_eq!(parse_alternative_number_format("0xff", Span::new(0, 4)).unwrap(), Some(255));
assert_eq!(parse_alternative_number_format("0xFF", Span::new(0, 4)).unwrap(), Some(255));
assert_eq!(parse_alternative_number_format("-0x10", Span::new(0, 5)).unwrap(), Some(-16));
assert_eq!(parse_alternative_number_format("+0x10", Span::new(0, 5)).unwrap(), Some(16));
assert!(parse_alternative_number_format("0x", Span::new(0, 2)).is_err());
assert!(parse_alternative_number_format("0xg", Span::new(0, 3)).is_err());
}
#[test]
fn test_parse_alternative_number_format_octal() {
let span = Span::new(0, 4);
assert_eq!(parse_alternative_number_format("0o10", span).unwrap(), Some(8));
assert_eq!(parse_alternative_number_format("0O10", span).unwrap(), Some(8));
assert_eq!(parse_alternative_number_format("0o777", span).unwrap(), Some(511));
assert_eq!(parse_alternative_number_format("-0o10", span).unwrap(), Some(-8));
assert_eq!(parse_alternative_number_format("+0o10", span).unwrap(), Some(8));
assert!(parse_alternative_number_format("0o", span).is_err());
assert!(parse_alternative_number_format("0o8", span).is_err());
}
#[test]
fn test_parse_alternative_number_format_binary() {
let span = Span::new(0, 6);
assert_eq!(parse_alternative_number_format("0b1010", span).unwrap(), Some(10));
assert_eq!(parse_alternative_number_format("0B1010", span).unwrap(), Some(10));
assert_eq!(parse_alternative_number_format("0b1111", span).unwrap(), Some(15));
assert_eq!(parse_alternative_number_format("-0b1010", span).unwrap(), Some(-10));
assert_eq!(parse_alternative_number_format("+0b1010", span).unwrap(), Some(10));
assert!(parse_alternative_number_format("0b", span).is_err());
assert!(parse_alternative_number_format("0b2", span).is_err());
}
#[test]
fn test_parse_alternative_number_format_underscore_separators() {
assert_eq!(parse_alternative_number_format("1_000", Span::new(0, 5)).unwrap(), Some(1000));
assert_eq!(parse_alternative_number_format("1_000_000", Span::new(0, 9)).unwrap(), Some(1000000));
assert_eq!(parse_alternative_number_format("-1_000", Span::new(0, 6)).unwrap(), Some(-1000));
assert_eq!(parse_alternative_number_format("+1_000", Span::new(0, 6)).unwrap(), Some(1000));
assert_eq!(parse_alternative_number_format("1.5", Span::new(0, 3)).unwrap(), None);
assert_eq!(parse_alternative_number_format("1e5", Span::new(0, 3)).unwrap(), None);
assert_eq!(parse_alternative_number_format("1E5", Span::new(0, 3)).unwrap(), None);
assert_eq!(parse_alternative_number_format("1.5_0", Span::new(0, 5)).unwrap(), None);
}
#[test]
fn test_parse_alternative_number_format_not_alternative() {
let span = Span::new(0, 3);
assert_eq!(parse_alternative_number_format("123", span).unwrap(), None);
assert_eq!(parse_alternative_number_format("3.14", span).unwrap(), None);
assert_eq!(parse_alternative_number_format("1e10", span).unwrap(), None);
assert_eq!(parse_alternative_number_format("-42", span).unwrap(), None);
}
#[test]
fn test_parse_alternative_number_format_overflow() {
let span = Span::new(0, 20);
let max_hex = "0x7FFFFFFFFFFFFFFF"; assert_eq!(parse_alternative_number_format(max_hex, span).unwrap(), Some(i64::MAX));
let overflow_hex = "0x8000000000000000"; assert!(parse_alternative_number_format(overflow_hex, span).is_err());
}
#[test]
fn test_parse_number_token_edge_cases() {
let large_num = "999999999999999999999";
assert!(matches!(
parse_number_token(large_num, Span::new(0, large_num.len())).unwrap(),
Value::Number(Number::Float(_))
));
}
#[test]
fn test_parse_number_token_invalid() {
assert!(parse_number_token("abc", Span::new(0, 3)).is_err());
assert!(parse_number_token("1e", Span::new(0, 2)).is_err());
assert!(parse_number_token("", Span::new(0, 0)).is_err());
}
}