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}