Skip to main content

fermat_core/
compare.rs

1//! `Ord` and `PartialOrd` for `Decimal` with automatic scale normalisation.
2//!
3//! Comparing two `Decimal` values with different scales requires aligning them
4//! to a common scale first. For example:
5//!
6//! ```text
7//! 1.50 (scale 2) > 1.5 (scale 1)  →  false
8//! 1.50 (scale 2) = 1.5 (scale 1)  →  true
9//! ```
10//!
11//! ## Implementation
12//!
13//! We multiply the smaller-scale mantissa by `10^diff` to align scales, then
14//! compare mantissas directly. If the alignment overflows we fall back to a
15//! sign-then-magnitude comparison which is still correct (overflow implies the
16//! larger-scale value is closer to zero).
17//!
18//! Note: `Decimal` derives `PartialEq` / `Eq` for *structural* equality
19//! (same mantissa AND same scale). This module provides *value* equality via
20//! `Ord`/`PartialOrd` — `1.5` and `1.50` compare as equal under `cmp` even
21//! though `==` returns false.
22
23use crate::arithmetic::align_scales;
24use crate::decimal::Decimal;
25use core::cmp::Ordering;
26
27impl PartialOrd for Decimal {
28    #[inline]
29    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
30        Some(self.cmp(other))
31    }
32}
33
34impl Ord for Decimal {
35    fn cmp(&self, other: &Self) -> Ordering {
36        // Fast path: identical representation
37        if self.scale == other.scale {
38            return self.mantissa.cmp(&other.mantissa);
39        }
40
41        // Align scales; if overflow we fall back to sign comparison
42        match align_scales(*self, *other) {
43            Ok((a, b, _)) => a.cmp(&b),
44            Err(_) => {
45                // Overflow during alignment means the operand being scaled up
46                // is very large in magnitude — but that doesn't directly tell
47                // us the sign. Use sign-then-magnitude as a safe fallback.
48                match (self.mantissa >= 0, other.mantissa >= 0) {
49                    (true, false) => Ordering::Greater,
50                    (false, true) => Ordering::Less,
51                    _ => {
52                        // Same sign — the one with bigger magnitude in its
53                        // original scale and same-direction normalization wins.
54                        // This is a safe approximation for the overflow edge case.
55                        if self.mantissa >= 0 {
56                            self.mantissa.cmp(&other.mantissa)
57                        } else {
58                            other.mantissa.cmp(&self.mantissa)
59                        }
60                    }
61                }
62            }
63        }
64    }
65}
66
67// ─── Tests ───────────────────────────────────────────────────────────────────
68
69#[cfg(test)]
70mod tests {
71    use crate::decimal::Decimal;
72
73    fn d(m: i128, s: u8) -> Decimal {
74        Decimal::new(m, s).unwrap()
75    }
76
77    #[test]
78    fn eq_same_scale() {
79        assert_eq!(d(100, 2).cmp(&d(100, 2)), core::cmp::Ordering::Equal);
80    }
81
82    #[test]
83    fn eq_different_scale() {
84        // 1.50 and 1.5 should be equal in value
85        assert_eq!(d(150, 2).cmp(&d(15, 1)), core::cmp::Ordering::Equal);
86    }
87
88    #[test]
89    fn gt_same_scale() {
90        assert!(d(200, 2) > d(100, 2));
91    }
92
93    #[test]
94    fn lt_different_scale() {
95        // 0.09 < 0.1
96        assert!(d(9, 2) < d(1, 1));
97    }
98
99    #[test]
100    fn negative_less_than_positive() {
101        assert!(d(-1, 0) < d(1, 0));
102    }
103
104    #[test]
105    fn negative_cmp_negative() {
106        // -2 < -1
107        assert!(d(-2, 0) < d(-1, 0));
108    }
109
110    #[test]
111    fn zero_cmp() {
112        assert_eq!(Decimal::ZERO.cmp(&Decimal::ZERO), core::cmp::Ordering::Equal);
113        assert!(Decimal::ZERO < d(1, 0));
114        assert!(Decimal::ZERO > d(-1, 0));
115    }
116
117    #[test]
118    fn sort_order() {
119        let mut vals = [d(15, 1), d(5, 2), d(100, 0), d(-1, 0)];
120        vals.sort();
121        // -1, 0.05, 1.5, 100
122        assert_eq!(vals[0], d(-1, 0));
123        assert_eq!(vals[1], d(5, 2));
124        assert_eq!(vals[2], d(15, 1));
125        assert_eq!(vals[3], d(100, 0));
126    }
127}