use std::cmp::Ordering;
use std::fmt::{self, Display, Formatter};
const MAX_DIGITS: u32 = 38;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct Decimal {
mantissa: i128,
scale: i32,
}
impl Decimal {
pub(crate) fn parse(text: &str) -> Option<Self> {
let text = text.trim();
let (negative, rest) = match text.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, text.strip_prefix('+').unwrap_or(text)),
};
let (significand, exponent_text) = match rest.find(['e', 'E']) {
Some(at) => (&rest[..at], Some(&rest[at + 1..])),
None => (rest, None),
};
if let Some(text) = exponent_text {
let digits = text.strip_prefix(['+', '-']).unwrap_or(text);
if digits.is_empty() || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
}
let (whole, fraction) = match significand.split_once('.') {
Some((whole, fraction)) => (whole, fraction),
None => (significand, ""),
};
if whole.is_empty() && fraction.is_empty() {
return None;
}
if !whole
.bytes()
.chain(fraction.bytes())
.all(|b| b.is_ascii_digit())
{
return None;
}
let digit_at = |index: usize| {
if index < whole.len() {
whole.as_bytes()[index]
} else {
fraction.as_bytes()[index - whole.len()]
}
};
let mut significant = whole.len() + fraction.len();
let mut trailing = 0_usize;
while significant > 0 && digit_at(significant - 1) == b'0' {
significant -= 1;
trailing += 1;
}
if significant == 0 {
return Some(Self {
mantissa: 0,
scale: 0,
});
}
let exponent: i64 = match exponent_text {
Some(text) => text.parse().ok()?,
None => 0,
};
let scale = exponent
.checked_sub(i64::try_from(fraction.len()).ok()?)?
.checked_add(i64::try_from(trailing).ok()?)?;
let scale = i32::try_from(scale).ok()?;
let mut mantissa: i128 = 0;
for index in 0..significant {
let digit = i128::from(digit_at(index) - b'0');
mantissa = mantissa.checked_mul(10)?;
mantissa = if negative {
mantissa.checked_sub(digit)?
} else {
mantissa.checked_add(digit)?
};
}
Self::new(mantissa, scale)
}
fn new(mut mantissa: i128, mut scale: i32) -> Option<Self> {
if mantissa == 0 {
return Some(Self {
mantissa: 0,
scale: 0,
});
}
while mantissa % 10 == 0 {
mantissa /= 10;
scale = scale.checked_add(1)?;
}
Some(Self { mantissa, scale })
}
pub(crate) fn is_integer(self) -> bool {
self.scale >= 0
}
pub(crate) fn is_zero(self) -> bool {
self.mantissa == 0
}
fn at_scale(self, scale: i32) -> Option<i128> {
let steps = self.scale.checked_sub(scale)?;
if steps < 0 {
return None; }
let steps = u32::try_from(steps).ok()?;
if steps > MAX_DIGITS {
return None;
}
self.mantissa.checked_mul(10_i128.checked_pow(steps)?)
}
pub(crate) fn compare(self, other: Self) -> Option<Ordering> {
let signs = self.mantissa.signum().cmp(&other.mantissa.signum());
if signs != Ordering::Equal {
return Some(signs);
}
let scale = self.scale.min(other.scale);
Some(self.at_scale(scale)?.cmp(&other.at_scale(scale)?))
}
pub(crate) fn is_multiple_of(self, step: Self) -> Option<bool> {
if step.is_zero() {
return None;
}
if self.is_zero() {
return Some(true);
}
let shift = self.scale.checked_sub(step.scale)?;
let (numerator, denominator) = if shift >= 0 {
let steps = u32::try_from(shift).ok()?;
if steps > MAX_DIGITS {
return None;
}
(
self.mantissa.checked_mul(10_i128.checked_pow(steps)?)?,
step.mantissa,
)
} else {
let steps = shift.unsigned_abs();
if steps > MAX_DIGITS {
return None;
}
(
self.mantissa,
step.mantissa.checked_mul(10_i128.checked_pow(steps)?)?,
)
};
Some(
numerator
.checked_rem(denominator)
.is_none_or(|remainder| remainder == 0),
)
}
pub(crate) fn into_value(self) -> serde_json::Value {
serde_json::Value::Number(serde_json::Number::from_string_unchecked(self.to_string()))
}
}
impl Display for Decimal {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.scale == 0 {
return write!(f, "{}", self.mantissa);
}
let digits = self.mantissa.unsigned_abs().to_string();
let sign = if self.mantissa < 0 { "-" } else { "" };
if self.scale > 0 {
if self.scale <= 21 {
let zeros = "0".repeat(self.scale.unsigned_abs() as usize);
return write!(f, "{sign}{digits}{zeros}");
}
return write!(f, "{sign}{digits}e{}", self.scale);
}
let places = self.scale.unsigned_abs() as usize;
if places > 21 {
return write!(f, "{sign}{digits}e{}", self.scale);
}
if places >= digits.len() {
let leading = "0".repeat(places - digits.len());
write!(f, "{sign}0.{leading}{digits}")
} else {
let (whole, fraction) = digits.split_at(digits.len() - places);
write!(f, "{sign}{whole}.{fraction}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn decimal(text: &str) -> Decimal {
Decimal::parse(text).unwrap_or_else(|| panic!("{text} must parse"))
}
#[test]
fn the_same_value_written_differently_is_the_same_decimal() {
for spelling in ["1", "1.0", "1.00", "10e-1", "0.01e2", "+1", "01"] {
assert_eq!(decimal(spelling), decimal("1"), "{spelling}");
}
}
#[test]
fn a_whole_number_is_recognised_however_it_was_spelled() {
for spelling in ["1", "1.0", "100e-2", "0", "-0.0", "2e3"] {
assert!(decimal(spelling).is_integer(), "{spelling}");
}
for spelling in ["1.5", "1.0000000000000001", "2251799813685248.25", "1e-3"] {
assert!(!decimal(spelling).is_integer(), "{spelling}");
}
}
#[test]
fn comparison_is_exact_past_what_a_double_can_hold() {
assert_eq!(
decimal("9007199254740993").compare(decimal("9007199254740992")),
Some(Ordering::Greater),
);
assert_eq!(
decimal("9007199254740994").compare(decimal("9007199254740993.5")),
Some(Ordering::Greater),
);
assert_eq!(
decimal("9007199254740993").compare(decimal("9007199254740993.5")),
Some(Ordering::Less),
);
}
#[test]
fn comparison_settles_signs_without_scaling() {
assert_eq!(
decimal("-1e30").compare(decimal("1e-30")),
Some(Ordering::Less)
);
assert_eq!(decimal("0").compare(decimal("-0.0")), Some(Ordering::Equal));
}
#[test]
fn ordinary_decimals_compare_the_way_arithmetic_says() {
assert_eq!(
decimal("0.1").compare(decimal("0.1")),
Some(Ordering::Equal)
);
assert_eq!(decimal("0.1").compare(decimal("0.2")), Some(Ordering::Less));
assert_eq!(
decimal("10").compare(decimal("9.9")),
Some(Ordering::Greater)
);
}
#[test]
fn divisibility_is_decided_rather_than_divided() {
assert_eq!(decimal("0.3").is_multiple_of(decimal("0.1")), Some(true));
assert_eq!(decimal("0.35").is_multiple_of(decimal("0.1")), Some(false));
assert_eq!(decimal("1.23").is_multiple_of(decimal("0.01")), Some(true));
assert_eq!(
decimal("2814749767106564").is_multiple_of(decimal("1.25")),
Some(false),
);
assert_eq!(decimal("4").is_multiple_of(decimal("2")), Some(true));
assert_eq!(decimal("5").is_multiple_of(decimal("2")), Some(false));
assert_eq!(
decimal("1").is_multiple_of(decimal("1.0000000000000001")),
Some(false),
);
}
#[test]
fn zero_is_a_multiple_of_anything_and_nothing_is_a_multiple_of_zero() {
assert_eq!(decimal("0").is_multiple_of(decimal("7")), Some(true));
assert_eq!(decimal("0.0").is_multiple_of(decimal("1.5")), Some(true));
assert_eq!(decimal("7").is_multiple_of(decimal("0")), None);
}
#[test]
fn zero_is_zero_whatever_exponent_follows_it() {
for text in [
"0e9223372036854775808",
"0.0e-9223372036854775809",
"0e999999999999999999999999999999",
"-0.000e123456789012345678901234567890",
] {
assert_eq!(Decimal::parse(text), Decimal::parse("0"), "{text}");
}
for text in ["0e", "0eabc", "0e+", "0e1.5", "0e--1"] {
assert_eq!(Decimal::parse(text), None, "{text}");
}
}
#[test]
fn a_scale_is_worked_out_before_it_is_narrowed() {
assert_eq!(
Decimal::parse("1.0e-2147483648"),
Decimal::parse("1e-2147483648"),
);
assert!(Decimal::parse("1000e-2147483650").is_some());
assert_eq!(Decimal::parse("0.0e-2147483648"), Decimal::parse("0"));
assert_eq!(Decimal::parse("1e-2147483649"), None);
assert_eq!(Decimal::parse("1e2147483648"), None);
}
#[test]
fn trailing_zeros_are_scale_rather_than_digits() {
let one = format!("{}e-39", "1".to_owned() + &"0".repeat(39));
assert_eq!(Decimal::parse(&one), Decimal::parse("1"));
assert_eq!(decimal(&one).to_string(), "1");
assert_eq!(Decimal::parse(&format!("-{one}")), Decimal::parse("-1"));
assert_eq!(Decimal::parse(&"0".repeat(45)), Decimal::parse("0"));
assert_eq!(
Decimal::parse("1.000000000000000000000000000000000000000"),
Decimal::parse("1")
);
}
#[test]
fn the_range_that_can_be_held_is_exactly_i128s() {
let max = i128::MAX.to_string();
let min = i128::MIN.to_string();
assert!(Decimal::parse(&max).is_some(), "{max}");
assert!(Decimal::parse(&min).is_some(), "{min}");
assert_eq!(Decimal::parse(&(i128::MAX as u128 + 1).to_string()), None);
assert_eq!(
Decimal::parse(&format!("-{}", i128::MIN.unsigned_abs() + 1)),
None
);
assert_eq!(max.len(), 39);
assert!(Decimal::parse(&"9".repeat(38)).is_some());
assert_eq!(Decimal::parse(&"9".repeat(39)), None);
assert_eq!(Decimal::parse("1e999999999999"), None);
}
#[test]
fn the_most_negative_number_behaves_like_any_other() {
let min = decimal(&i128::MIN.to_string());
assert_eq!(min.compare(decimal("0")), Some(Ordering::Less));
assert_eq!(min.compare(min), Some(Ordering::Equal));
assert!(min.is_integer());
assert_eq!(min.is_multiple_of(decimal("-1")), Some(true));
assert_eq!(min.is_multiple_of(decimal("1")), Some(true));
assert_eq!(min.is_multiple_of(decimal("2")), Some(true));
assert_eq!(min.to_string(), i128::MIN.to_string());
}
#[test]
fn an_extreme_exponent_is_refused_rather_than_overflowing() {
assert_eq!(Decimal::parse("10e2147483647"), None);
assert_eq!(Decimal::parse("100e2147483646"), None);
assert!(Decimal::parse("1e2147483647").is_some());
assert!(Decimal::parse("1e-2147483648").is_some());
}
#[test]
fn divisibility_at_an_extreme_scale_does_not_overflow() {
let tiny = decimal("1e-2147483648");
assert_eq!(tiny.is_multiple_of(decimal("1")), None);
assert_eq!(decimal("1").is_multiple_of(tiny), None);
}
#[test]
fn what_is_not_a_number_does_not_parse() {
for text in ["", "abc", "1.2.3", "1e", "--1", ".", "1e1e1", "0x10"] {
assert_eq!(Decimal::parse(text), None, "{text}");
}
}
#[test]
fn a_decimal_writes_itself_the_way_it_was_meant() {
for (text, shown) in [
("1", "1"),
("1.0", "1"),
("1.5", "1.5"),
("-2.25", "-2.25"),
("0.001", "0.001"),
("100", "100"),
("1e3", "1000"),
("9007199254740993", "9007199254740993"),
("0", "0"),
] {
assert_eq!(decimal(text).to_string(), shown, "{text}");
}
}
#[test]
fn an_extreme_scale_falls_back_to_exponent_form() {
assert_eq!(decimal("1e40").to_string(), "1e40");
assert_eq!(decimal("1e-40").to_string(), "1e-40");
}
}