use crate::{Error, Result};
pub const MAX_SAFE_INTEGER: i64 = (1 << 53) - 1;
const MAX_SAFE_F64: f64 = MAX_SAFE_INTEGER as f64;
#[derive(Debug, Clone, Copy)]
pub struct Number {
value: f64,
int: Option<i64>,
}
impl Number {
pub fn from_i64(i: i64) -> Option<Number> {
(i.unsigned_abs() <= MAX_SAFE_INTEGER.unsigned_abs()).then_some(Number {
value: i as f64,
int: Some(i),
})
}
pub fn from_u64(u: u64) -> Option<Number> {
i64::try_from(u).ok().and_then(Number::from_i64)
}
pub fn from_f64(v: f64) -> Option<Number> {
if !v.is_finite() {
return None;
}
let value = v + 0.0;
let int = (value.fract() == 0.0 && value.abs() <= MAX_SAFE_F64).then_some(value as i64);
Some(Number { value, int })
}
pub(crate) fn from_lexeme(lexeme: &str) -> Result<Number> {
let parsed: f64 = lexeme
.parse()
.map_err(|_| Error::InvalidJson("invalid number"))?;
if !parsed.is_finite() {
return Err(Error::InvalidJson("number out of range"));
}
let value = parsed + 0.0;
let int =
(lexeme_is_integer(lexeme) && value.abs() <= MAX_SAFE_F64).then_some(value as i64);
Ok(Number { value, int })
}
pub fn as_i64(&self) -> Option<i64> {
self.int
}
pub fn as_u64(&self) -> Option<u64> {
self.int.and_then(|i| u64::try_from(i).ok())
}
pub fn as_f64(&self) -> f64 {
self.value
}
}
impl PartialEq for Number {
fn eq(&self, other: &Number) -> bool {
self.value == other.value
}
}
fn lexeme_is_integer(lexeme: &str) -> bool {
let body = lexeme.strip_prefix('-').unwrap_or(lexeme);
let (mantissa, exp) = match body.split_once(['e', 'E']) {
Some((m, e)) => (m, parse_exponent(e)),
None => (body, 0),
};
let (int_part, frac) = mantissa.split_once('.').unwrap_or((mantissa, ""));
let frac_significant = frac.trim_end_matches('0');
if !frac_significant.is_empty() {
let places = i64::try_from(frac_significant.len()).unwrap_or(i64::MAX);
return exp >= places;
}
let int_significant = int_part.trim_end_matches('0');
if int_significant.is_empty() {
return true; }
let trailing_zeros = i64::try_from(int_part.len() - int_significant.len()).unwrap_or(i64::MAX);
exp.saturating_add(trailing_zeros) >= 0
}
fn parse_exponent(e: &str) -> i64 {
let (negative, digits) = match e.as_bytes().first() {
Some(b'-') => (true, e.get(1..).unwrap_or("")),
Some(b'+') => (false, e.get(1..).unwrap_or("")),
_ => (false, e),
};
let magnitude = digits.bytes().fold(0i64, |acc, d| {
acc.saturating_mul(10)
.saturating_add(i64::from(d.wrapping_sub(b'0')))
});
if negative { -magnitude } else { magnitude }
}
#[cfg(test)]
mod tests {
use super::*;
fn lex(s: &str) -> Number {
Number::from_lexeme(s).unwrap_or_else(|e| unreachable!("{s}: {e}"))
}
#[test]
fn integer_lexemes_are_exact() {
for (s, i) in [
("0", 0),
("-0", 0),
("7", 7),
("-7", -7),
("1e2", 100),
("1E+2", 100),
("1.0", 1),
("1.500e1", 15),
("100e-2", 1),
("0.0e-99999999999999999999999", 0),
("9007199254740991", MAX_SAFE_INTEGER),
("-9007199254740991", -MAX_SAFE_INTEGER),
] {
assert_eq!(lex(s).as_i64(), Some(i), "{s}");
}
}
#[test]
fn non_integers_and_out_of_range_integers_have_no_integer_value() {
for s in [
"1.5",
"1e-1",
"10e-2",
"1.00000000000000001", "1e-400", "9007199254740992", "-9007199254740992",
"9007199254740993",
"1e16",
"123123123123123123123123123123",
] {
assert_eq!(lex(s).as_i64(), None, "{s}");
}
}
#[test]
fn non_finite_values_are_rejected() {
for s in [
"1e400",
"-1e400",
"1.5e+9999",
"0.4e0066999999999999999999999",
] {
assert_eq!(
Number::from_lexeme(s),
Err(Error::InvalidJson("number out of range")),
"{s}"
);
}
}
#[test]
fn values_are_correctly_rounded_doubles() {
assert_eq!(lex("0.1").as_f64(), 0.1);
assert_eq!(lex("-2.5e-3").as_f64(), -0.0025);
assert_eq!(lex("1.7976931348623157e308").as_f64(), f64::MAX);
assert_eq!(lex("5e-324").as_f64(), 5e-324);
assert_eq!(lex("9007199254740993").as_f64(), 9007199254740992.0);
}
#[test]
fn negative_zero_is_stored_as_zero() {
for s in ["-0", "-0.0", "-0e5", "-1e-400"] {
assert!(lex(s).as_f64().is_sign_positive(), "{s}");
}
let from_f64 = Number::from_f64(-0.0).map(|n| n.as_f64());
assert!(from_f64.is_some_and(f64::is_sign_positive));
}
#[test]
fn u64_accessor_is_non_negative_exact_integers_only() {
assert_eq!(lex("42").as_u64(), Some(42));
assert_eq!(lex("-1").as_u64(), None);
assert_eq!(lex("4.2").as_u64(), None);
}
#[test]
fn constructors_enforce_the_safe_range_and_finiteness() {
assert!(Number::from_i64(MAX_SAFE_INTEGER).is_some());
assert!(Number::from_i64(-MAX_SAFE_INTEGER).is_some());
assert!(Number::from_i64(MAX_SAFE_INTEGER + 1).is_none());
assert!(Number::from_i64(i64::MIN).is_none());
assert!(Number::from_u64(u64::MAX).is_none());
assert_eq!(Number::from_u64(5).and_then(|n| n.as_u64()), Some(5));
assert!(Number::from_f64(f64::INFINITY).is_none());
assert!(Number::from_f64(f64::NEG_INFINITY).is_none());
assert_eq!(Number::from_f64(2.0e20).and_then(|n| n.as_i64()), None);
assert_eq!(Number::from_f64(-0.0).and_then(|n| n.as_i64()), Some(0));
}
#[test]
fn equality_compares_doubles() {
assert_eq!(lex("1"), lex("1.0"));
assert_eq!(lex("1"), lex("1e0"));
assert_eq!(lex("0"), lex("-0"));
assert_ne!(lex("1"), lex("1.5"));
assert_eq!(Number::from_i64(3), Number::from_f64(3.0));
}
}