Skip to main content

nodedb_types/
numeric_cmp.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Numeric readings and their total order, shared by every comparison path.
4//!
5//! - An integer reads as an exact `i128`. `i128` holds every `i64` and
6//!   every `u64`.
7//! - A fractional `Decimal` reads as an exact `Decimal`. A `DECIMAL` cell is
8//!   stored as its text, so numeric text with a fraction reads as a
9//!   `Decimal` too.
10//! - A float reads as an `f64`.
11//! - Integer and decimal pairs compare exactly. An integer against a float
12//!   compares exactly. A decimal against a float compares as `f64`.
13//! - NaN sorts above every number and equals NaN, as in PostgreSQL. The
14//!   order is total, so a sort over it never panics.
15
16use std::cmp::Ordering;
17
18use rust_decimal::Decimal;
19use rust_decimal::prelude::ToPrimitive;
20
21/// The numeric reading of a value for comparison and summation.
22#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum Numeric {
24    Int(i128),
25    Decimal(Decimal),
26    Float(f64),
27}
28
29/// Total order of two floats. NaN sorts above every number and equals NaN.
30/// `-0.0` equals `0.0`.
31pub fn cmp_f64(a: f64, b: f64) -> Ordering {
32    match a.partial_cmp(&b) {
33        Some(order) => order,
34        None => a.is_nan().cmp(&b.is_nan()),
35    }
36}
37
38/// A numeric string read as a number: integer text as an exact integer,
39/// fractional decimal text as an exact `Decimal`, and any other float text
40/// (an exponent, a fraction past the `Decimal` precision, `NaN`, `inf`) as
41/// an `f64`. `None` for non-numeric text.
42pub fn parse_numeric_str(s: &str) -> Option<Numeric> {
43    if let Ok(i) = s.parse::<i128>() {
44        return Some(Numeric::Int(i));
45    }
46    let float = s.parse::<f64>().ok()?;
47    Some(match Decimal::from_str_exact(s) {
48        Ok(d) => decimal_reading(&d),
49        Err(_) => Numeric::Float(float),
50    })
51}
52
53/// An integral `Decimal` as an exact integer, any other as an exact
54/// `Decimal`.
55pub fn decimal_reading(d: &Decimal) -> Numeric {
56    if d.is_integer()
57        && let Some(i) = d.to_i128()
58    {
59        return Numeric::Int(i);
60    }
61    Numeric::Decimal(*d)
62}
63
64/// `2^127` as `f64`: the first float above every `i128`.
65const TWO_POW_127: f64 = 170_141_183_460_469_231_731_687_303_715_884_105_728.0;
66
67/// Exact order of an `i128` against an `f64`, with no rounding of either.
68/// NaN is above every integer.
69fn cmp_int_float(i: i128, f: f64) -> Ordering {
70    if f.is_nan() || f >= TWO_POW_127 {
71        return Ordering::Less;
72    }
73    if f < -TWO_POW_127 {
74        return Ordering::Greater;
75    }
76    // `f` lies in `[-2^127, 2^127)`, so its integral part is an exact `i128`.
77    let whole = f.trunc();
78    let by_whole = i.cmp(&(whole as i128));
79    if by_whole != Ordering::Equal {
80        return by_whole;
81    }
82    let fraction = f - whole;
83    if fraction > 0.0 {
84        Ordering::Less
85    } else if fraction < 0.0 {
86        Ordering::Greater
87    } else {
88        Ordering::Equal
89    }
90}
91
92/// Exact order of an `i128` against a `Decimal`. An integer past the
93/// `Decimal` range is past every `Decimal` on its side of zero.
94fn cmp_int_decimal(i: i128, d: Decimal) -> Ordering {
95    match Decimal::try_from_i128_with_scale(i, 0) {
96        Ok(as_decimal) => as_decimal.cmp(&d),
97        Err(_) if i > 0 => Ordering::Greater,
98        Err(_) => Ordering::Less,
99    }
100}
101
102/// Total order of two numeric readings. See the module docs for the rule.
103pub fn cmp_numeric(a: Numeric, b: Numeric) -> Ordering {
104    match (a, b) {
105        (Numeric::Int(x), Numeric::Int(y)) => x.cmp(&y),
106        (Numeric::Int(x), Numeric::Decimal(y)) => cmp_int_decimal(x, y),
107        (Numeric::Decimal(x), Numeric::Int(y)) => cmp_int_decimal(y, x).reverse(),
108        (Numeric::Decimal(x), Numeric::Decimal(y)) => x.cmp(&y),
109        (Numeric::Int(x), Numeric::Float(y)) => cmp_int_float(x, y),
110        (Numeric::Float(x), Numeric::Int(y)) => cmp_int_float(y, x).reverse(),
111        (Numeric::Decimal(x), Numeric::Float(y)) => cmp_f64(x.as_f64(), y),
112        (Numeric::Float(x), Numeric::Decimal(y)) => cmp_f64(x, y.as_f64()),
113        (Numeric::Float(x), Numeric::Float(y)) => cmp_f64(x, y),
114    }
115}
116
117/// The `f64` a numeric reading rounds to, by the same conversion
118/// [`cmp_numeric`] uses for a decimal against a float. Two readings
119/// [`cmp_numeric`] orders `Equal` round to the same `f64`, with `-0.0` read
120/// as `0.0` and every NaN as one NaN. Distinct readings can share an `f64`,
121/// so this is a hash bucket, never an equality.
122pub fn numeric_bucket_bits(n: Numeric) -> u64 {
123    let f = match n {
124        Numeric::Int(i) => i as f64,
125        Numeric::Decimal(d) => d.as_f64(),
126        Numeric::Float(f) => f,
127    };
128    if f.is_nan() {
129        f64::NAN.to_bits()
130    } else if f == 0.0 {
131        0.0f64.to_bits()
132    } else {
133        f.to_bits()
134    }
135}
136
137/// Equality of two numeric readings for a coerced `=`: [`cmp_numeric`]
138/// orders the pair `Equal`. Exact for every pair, as PostgreSQL float `=`
139/// is, and NaN equals NaN.
140pub fn numeric_eq(a: Numeric, b: Numeric) -> bool {
141    cmp_numeric(a, b) == Ordering::Equal
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    fn dec(text: &str) -> Decimal {
149        Decimal::from_str_exact(text).unwrap()
150    }
151
152    #[test]
153    fn nan_is_above_every_number_and_equals_nan() {
154        assert_eq!(cmp_f64(f64::NAN, f64::INFINITY), Ordering::Greater);
155        assert_eq!(cmp_f64(f64::NEG_INFINITY, f64::NAN), Ordering::Less);
156        assert_eq!(cmp_f64(f64::NAN, f64::NAN), Ordering::Equal);
157        assert_eq!(cmp_f64(-0.0, 0.0), Ordering::Equal);
158        let nan = Numeric::Float(f64::NAN);
159        assert_eq!(cmp_numeric(nan, Numeric::Int(i128::MAX)), Ordering::Greater);
160        assert_eq!(cmp_numeric(Numeric::Int(i128::MIN), nan), Ordering::Less);
161        assert_eq!(
162            cmp_numeric(nan, Numeric::Decimal(Decimal::MAX)),
163            Ordering::Greater
164        );
165        assert_eq!(
166            cmp_numeric(Numeric::Decimal(dec("0.5")), nan),
167            Ordering::Less
168        );
169        assert_eq!(cmp_numeric(nan, nan), Ordering::Equal);
170        assert!(numeric_eq(nan, nan));
171    }
172
173    /// A sort over NaN, infinities and every reading kind runs to a
174    /// consistent order.
175    #[test]
176    fn mixed_sort_is_total() {
177        let mut values = [
178            Numeric::Float(f64::NAN),
179            Numeric::Int(3),
180            Numeric::Float(f64::INFINITY),
181            Numeric::Decimal(dec("2.5")),
182            Numeric::Float(f64::NAN),
183            Numeric::Float(f64::NEG_INFINITY),
184            Numeric::Int(-1),
185        ];
186        values.sort_by(|a, b| cmp_numeric(*a, *b));
187        assert_eq!(values[0], Numeric::Float(f64::NEG_INFINITY));
188        assert_eq!(values[1], Numeric::Int(-1));
189        assert_eq!(values[2], Numeric::Decimal(dec("2.5")));
190        assert_eq!(values[3], Numeric::Int(3));
191        assert_eq!(values[4], Numeric::Float(f64::INFINITY));
192        assert!(matches!(values[5], Numeric::Float(f) if f.is_nan()));
193        assert!(matches!(values[6], Numeric::Float(f) if f.is_nan()));
194    }
195
196    /// Two decimals one hundredth apart past `2^53` compare exactly.
197    #[test]
198    fn fractional_decimal_text_compares_exactly() {
199        let low = parse_numeric_str("12345678901234567.01").unwrap();
200        let high = parse_numeric_str("12345678901234567.02").unwrap();
201        assert_eq!(low, Numeric::Decimal(dec("12345678901234567.01")));
202        assert_eq!(cmp_numeric(low, high), Ordering::Less);
203        assert_eq!(cmp_numeric(high, low), Ordering::Greater);
204        assert!(!numeric_eq(low, high));
205    }
206
207    #[test]
208    fn decimal_against_integer_compares_exactly() {
209        let above = Numeric::Decimal(dec("9007199254740993.5"));
210        assert_eq!(
211            cmp_numeric(above, Numeric::Int(9_007_199_254_740_993)),
212            Ordering::Greater
213        );
214        assert_eq!(
215            cmp_numeric(Numeric::Int(9_007_199_254_740_994), above),
216            Ordering::Greater
217        );
218        assert_eq!(
219            cmp_numeric(Numeric::Int(i128::MAX), above),
220            Ordering::Greater
221        );
222        assert_eq!(cmp_numeric(Numeric::Int(i128::MIN), above), Ordering::Less);
223        assert_eq!(
224            cmp_numeric(Numeric::Decimal(dec("-0.5")), Numeric::Int(0)),
225            Ordering::Less
226        );
227    }
228
229    #[test]
230    fn numeric_text_reads_by_kind() {
231        assert_eq!(parse_numeric_str("12"), Some(Numeric::Int(12)));
232        assert_eq!(parse_numeric_str("5.0"), Some(Numeric::Int(5)));
233        assert_eq!(parse_numeric_str("0.1"), Some(Numeric::Decimal(dec("0.1"))));
234        assert_eq!(parse_numeric_str("1e3"), Some(Numeric::Float(1000.0)));
235        assert!(matches!(parse_numeric_str("NaN"), Some(Numeric::Float(f)) if f.is_nan()));
236        assert_eq!(parse_numeric_str("x"), None);
237        assert_eq!(parse_numeric_str("1_0.5"), None);
238    }
239
240    #[test]
241    fn integral_decimal_reads_as_integer() {
242        assert_eq!(decimal_reading(&dec("42.000")), Numeric::Int(42));
243        assert_eq!(
244            decimal_reading(&Decimal::MAX),
245            Numeric::Int(Decimal::MAX.mantissa())
246        );
247        assert_eq!(decimal_reading(&dec("0.25")), Numeric::Decimal(dec("0.25")));
248    }
249}