use std::cmp::Ordering;
use rust_decimal::Decimal;
use rust_decimal::prelude::ToPrimitive;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Numeric {
Int(i128),
Decimal(Decimal),
Float(f64),
}
pub fn cmp_f64(a: f64, b: f64) -> Ordering {
match a.partial_cmp(&b) {
Some(order) => order,
None => a.is_nan().cmp(&b.is_nan()),
}
}
pub fn parse_numeric_str(s: &str) -> Option<Numeric> {
if let Ok(i) = s.parse::<i128>() {
return Some(Numeric::Int(i));
}
let float = s.parse::<f64>().ok()?;
Some(match Decimal::from_str_exact(s) {
Ok(d) => decimal_reading(&d),
Err(_) => Numeric::Float(float),
})
}
pub fn decimal_reading(d: &Decimal) -> Numeric {
if d.is_integer()
&& let Some(i) = d.to_i128()
{
return Numeric::Int(i);
}
Numeric::Decimal(*d)
}
const TWO_POW_127: f64 = 170_141_183_460_469_231_731_687_303_715_884_105_728.0;
fn cmp_int_float(i: i128, f: f64) -> Ordering {
if f.is_nan() || f >= TWO_POW_127 {
return Ordering::Less;
}
if f < -TWO_POW_127 {
return Ordering::Greater;
}
let whole = f.trunc();
let by_whole = i.cmp(&(whole as i128));
if by_whole != Ordering::Equal {
return by_whole;
}
let fraction = f - whole;
if fraction > 0.0 {
Ordering::Less
} else if fraction < 0.0 {
Ordering::Greater
} else {
Ordering::Equal
}
}
fn cmp_int_decimal(i: i128, d: Decimal) -> Ordering {
match Decimal::try_from_i128_with_scale(i, 0) {
Ok(as_decimal) => as_decimal.cmp(&d),
Err(_) if i > 0 => Ordering::Greater,
Err(_) => Ordering::Less,
}
}
pub fn cmp_numeric(a: Numeric, b: Numeric) -> Ordering {
match (a, b) {
(Numeric::Int(x), Numeric::Int(y)) => x.cmp(&y),
(Numeric::Int(x), Numeric::Decimal(y)) => cmp_int_decimal(x, y),
(Numeric::Decimal(x), Numeric::Int(y)) => cmp_int_decimal(y, x).reverse(),
(Numeric::Decimal(x), Numeric::Decimal(y)) => x.cmp(&y),
(Numeric::Int(x), Numeric::Float(y)) => cmp_int_float(x, y),
(Numeric::Float(x), Numeric::Int(y)) => cmp_int_float(y, x).reverse(),
(Numeric::Decimal(x), Numeric::Float(y)) => cmp_f64(x.as_f64(), y),
(Numeric::Float(x), Numeric::Decimal(y)) => cmp_f64(x, y.as_f64()),
(Numeric::Float(x), Numeric::Float(y)) => cmp_f64(x, y),
}
}
pub fn numeric_bucket_bits(n: Numeric) -> u64 {
let f = match n {
Numeric::Int(i) => i as f64,
Numeric::Decimal(d) => d.as_f64(),
Numeric::Float(f) => f,
};
if f.is_nan() {
f64::NAN.to_bits()
} else if f == 0.0 {
0.0f64.to_bits()
} else {
f.to_bits()
}
}
pub fn numeric_eq(a: Numeric, b: Numeric) -> bool {
cmp_numeric(a, b) == Ordering::Equal
}
#[cfg(test)]
mod tests {
use super::*;
fn dec(text: &str) -> Decimal {
Decimal::from_str_exact(text).unwrap()
}
#[test]
fn nan_is_above_every_number_and_equals_nan() {
assert_eq!(cmp_f64(f64::NAN, f64::INFINITY), Ordering::Greater);
assert_eq!(cmp_f64(f64::NEG_INFINITY, f64::NAN), Ordering::Less);
assert_eq!(cmp_f64(f64::NAN, f64::NAN), Ordering::Equal);
assert_eq!(cmp_f64(-0.0, 0.0), Ordering::Equal);
let nan = Numeric::Float(f64::NAN);
assert_eq!(cmp_numeric(nan, Numeric::Int(i128::MAX)), Ordering::Greater);
assert_eq!(cmp_numeric(Numeric::Int(i128::MIN), nan), Ordering::Less);
assert_eq!(
cmp_numeric(nan, Numeric::Decimal(Decimal::MAX)),
Ordering::Greater
);
assert_eq!(
cmp_numeric(Numeric::Decimal(dec("0.5")), nan),
Ordering::Less
);
assert_eq!(cmp_numeric(nan, nan), Ordering::Equal);
assert!(numeric_eq(nan, nan));
}
#[test]
fn mixed_sort_is_total() {
let mut values = [
Numeric::Float(f64::NAN),
Numeric::Int(3),
Numeric::Float(f64::INFINITY),
Numeric::Decimal(dec("2.5")),
Numeric::Float(f64::NAN),
Numeric::Float(f64::NEG_INFINITY),
Numeric::Int(-1),
];
values.sort_by(|a, b| cmp_numeric(*a, *b));
assert_eq!(values[0], Numeric::Float(f64::NEG_INFINITY));
assert_eq!(values[1], Numeric::Int(-1));
assert_eq!(values[2], Numeric::Decimal(dec("2.5")));
assert_eq!(values[3], Numeric::Int(3));
assert_eq!(values[4], Numeric::Float(f64::INFINITY));
assert!(matches!(values[5], Numeric::Float(f) if f.is_nan()));
assert!(matches!(values[6], Numeric::Float(f) if f.is_nan()));
}
#[test]
fn fractional_decimal_text_compares_exactly() {
let low = parse_numeric_str("12345678901234567.01").unwrap();
let high = parse_numeric_str("12345678901234567.02").unwrap();
assert_eq!(low, Numeric::Decimal(dec("12345678901234567.01")));
assert_eq!(cmp_numeric(low, high), Ordering::Less);
assert_eq!(cmp_numeric(high, low), Ordering::Greater);
assert!(!numeric_eq(low, high));
}
#[test]
fn decimal_against_integer_compares_exactly() {
let above = Numeric::Decimal(dec("9007199254740993.5"));
assert_eq!(
cmp_numeric(above, Numeric::Int(9_007_199_254_740_993)),
Ordering::Greater
);
assert_eq!(
cmp_numeric(Numeric::Int(9_007_199_254_740_994), above),
Ordering::Greater
);
assert_eq!(
cmp_numeric(Numeric::Int(i128::MAX), above),
Ordering::Greater
);
assert_eq!(cmp_numeric(Numeric::Int(i128::MIN), above), Ordering::Less);
assert_eq!(
cmp_numeric(Numeric::Decimal(dec("-0.5")), Numeric::Int(0)),
Ordering::Less
);
}
#[test]
fn numeric_text_reads_by_kind() {
assert_eq!(parse_numeric_str("12"), Some(Numeric::Int(12)));
assert_eq!(parse_numeric_str("5.0"), Some(Numeric::Int(5)));
assert_eq!(parse_numeric_str("0.1"), Some(Numeric::Decimal(dec("0.1"))));
assert_eq!(parse_numeric_str("1e3"), Some(Numeric::Float(1000.0)));
assert!(matches!(parse_numeric_str("NaN"), Some(Numeric::Float(f)) if f.is_nan()));
assert_eq!(parse_numeric_str("x"), None);
assert_eq!(parse_numeric_str("1_0.5"), None);
}
#[test]
fn integral_decimal_reads_as_integer() {
assert_eq!(decimal_reading(&dec("42.000")), Numeric::Int(42));
assert_eq!(
decimal_reading(&Decimal::MAX),
Numeric::Int(Decimal::MAX.mantissa())
);
assert_eq!(decimal_reading(&dec("0.25")), Numeric::Decimal(dec("0.25")));
}
}