use crate::{Error, Options};
const MAX_SAFE_DIGITS: &str = "9007199254740992";
struct Decomposed {
digits: String,
exp10: i64,
}
impl Decomposed {
fn is_integer(&self) -> bool {
self.digits.is_empty() || self.exp10 >= 0
}
fn is_unsafe_magnitude(&self) -> bool {
if self.digits.is_empty() || self.exp10 < 0 {
return false;
}
let Ok(exp) = usize::try_from(self.exp10) else {
return true;
};
let Some(width) = self.digits.len().checked_add(exp) else {
return true;
};
match width.cmp(&MAX_SAFE_DIGITS.len()) {
core::cmp::Ordering::Less => false,
core::cmp::Ordering::Greater => true,
core::cmp::Ordering::Equal => {
let mut padded = self.digits.clone();
padded.push_str(&"0".repeat(exp));
padded.as_str() >= MAX_SAFE_DIGITS
}
}
}
}
fn exponent_of(text: &str) -> i64 {
let (negative, digits) = match text.as_bytes().first() {
Some(b'-') => (true, &text[1..]),
Some(b'+') => (false, &text[1..]),
_ => (false, text),
};
let mut value: i64 = 0;
for byte in digits.bytes() {
let digit = i64::from(byte.wrapping_sub(b'0'));
value = value.saturating_mul(10).saturating_add(digit);
}
if negative {
value.saturating_neg()
} else {
value
}
}
fn decompose(token: &str) -> Decomposed {
let body = token.strip_prefix('-').unwrap_or(token);
let (mantissa, exponent) = match body.split_once(['e', 'E']) {
Some((m, e)) => (m, exponent_of(e)),
None => (body, 0),
};
let (int_part, frac_part) = mantissa.split_once('.').unwrap_or((mantissa, ""));
let mut digits = String::with_capacity(int_part.len() + frac_part.len());
digits.push_str(int_part);
digits.push_str(frac_part);
let mut exp10 = exponent.saturating_sub(i64::try_from(frac_part.len()).unwrap_or(i64::MAX));
let leading = digits.len() - digits.trim_start_matches('0').len();
digits.drain(..leading);
while digits.ends_with('0') {
digits.pop();
exp10 = exp10.saturating_add(1);
}
Decomposed { digits, exp10 }
}
pub(crate) fn check(token: &str, opts: &Options) -> Result<(), Error> {
if opts.wants_safe_integers() || opts.wants_integers_only() {
let d = decompose(token);
if opts.wants_integers_only() && !d.is_integer() {
return Err(Error::NonIntegerNumber {
token: token.to_owned(),
});
}
if opts.wants_safe_integers() && d.is_integer() && d.is_unsafe_magnitude() {
return Err(Error::UnsafeInteger {
token: token.to_owned(),
});
}
}
if !token.parse::<f64>().is_ok_and(f64::is_finite) {
return Err(Error::NonFiniteNumber {
token: token.to_owned(),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn exponent_notation_decomposes_as_an_integer() {
let d = decompose("1e21");
assert_eq!(d.digits, "1");
assert_eq!(d.exp10, 21);
assert!(d.is_integer());
assert!(d.is_unsafe_magnitude());
}
#[test]
fn zero_is_a_safe_integer_in_every_notation() {
for token in ["0", "-0", "0.0", "0e100", "-0.000e-3"] {
let d = decompose(token);
assert!(d.is_integer(), "{token}");
assert!(!d.is_unsafe_magnitude(), "{token}");
}
}
#[test]
fn the_safe_integer_boundary_is_exact_on_the_digits() {
assert!(!decompose("9007199254740991").is_unsafe_magnitude());
assert!(decompose("9007199254740992").is_unsafe_magnitude());
assert!(decompose("9007199254740993").is_unsafe_magnitude());
assert!(!decompose("1000000000000000").is_unsafe_magnitude());
assert!(!decompose("9007199254740991.0").is_unsafe_magnitude());
}
#[test]
fn a_fraction_is_not_an_integer_however_it_is_written() {
assert!(!decompose("1.5").is_integer());
assert!(!decompose("15e-1").is_integer());
assert!(decompose("15e-1").exp10 == -1);
assert!(decompose("100.0").is_integer());
}
}