Skip to main content

dashu_float/third_party/
num_order.rs

1use _num_modular::{FixedMersenneInt, ModularAbs, ModularInteger};
2use core::cmp::Ordering;
3use dashu_base::{BitTest, EstimatedLog2, FloatEncoding, Sign, Signed};
4use dashu_int::{IBig, UBig, Word};
5use num_order::{NumHash, NumOrd};
6
7use crate::{
8    cmp::{repr_cmp_ibig, repr_cmp_ubig},
9    fbig::FBig,
10    repr::Repr,
11    round::Round,
12    utils::shl_digits_in_place,
13};
14
15impl<const B1: Word, const B2: Word> NumOrd<Repr<B2>> for Repr<B1> {
16    fn num_cmp(&self, other: &Repr<B2>) -> Ordering {
17        // case 1: compare with inf
18        match (self.is_infinite(), other.is_infinite()) {
19            (true, true) => return self.exponent.cmp(&other.exponent),
20            (false, true) => {
21                return match other.exponent >= 0 {
22                    true => Ordering::Less,
23                    false => Ordering::Greater,
24                }
25            }
26            (true, false) => {
27                return match self.exponent >= 0 {
28                    true => Ordering::Greater,
29                    false => Ordering::Less,
30                }
31            }
32            _ => {}
33        };
34
35        // case 2: compare sign
36        let sign = match (self.significand.sign(), other.significand.sign()) {
37            (Sign::Positive, Sign::Positive) => Sign::Positive,
38            (Sign::Positive, Sign::Negative) => return Ordering::Greater,
39            (Sign::Negative, Sign::Positive) => return Ordering::Less,
40            (Sign::Negative, Sign::Negative) => Sign::Negative,
41        };
42
43        // case 3: compare log2 estimations
44        let (self_lo, self_hi) = self.log2_bounds();
45        let (other_lo, other_hi) = other.log2_bounds();
46        if self_lo > other_hi {
47            return sign * Ordering::Greater;
48        }
49        if self_hi < other_lo {
50            return sign * Ordering::Less;
51        }
52
53        // case 4: compare the exact values
54        let (mut lhs, mut rhs) = (self.significand.clone(), other.significand.clone());
55        if self.exponent < 0 {
56            shl_digits_in_place::<B1>(&mut rhs, (-self.exponent) as usize);
57        } else {
58            shl_digits_in_place::<B1>(&mut lhs, self.exponent as usize);
59        }
60        if other.exponent < 0 {
61            shl_digits_in_place::<B2>(&mut lhs, (-other.exponent) as usize);
62        } else {
63            shl_digits_in_place::<B2>(&mut rhs, other.exponent as usize);
64        }
65        lhs.cmp(&rhs)
66    }
67    #[inline]
68    fn num_partial_cmp(&self, other: &Repr<B2>) -> Option<Ordering> {
69        Some(self.num_cmp(other))
70    }
71}
72
73impl<R1: Round, R2: Round, const B1: Word, const B2: Word> NumOrd<FBig<R2, B2>> for FBig<R1, B1> {
74    #[inline]
75    fn num_cmp(&self, other: &FBig<R2, B2>) -> Ordering {
76        self.repr.num_cmp(&other.repr)
77    }
78    #[inline]
79    fn num_partial_cmp(&self, other: &FBig<R2, B2>) -> Option<Ordering> {
80        self.repr.num_partial_cmp(&other.repr)
81    }
82}
83
84macro_rules! impl_num_ord_with_method {
85    ($T:ty, $method:ident) => {
86        impl<const B: Word> NumOrd<$T> for Repr<B> {
87            #[inline]
88            fn num_cmp(&self, other: &$T) -> Ordering {
89                $method::<B, false>(self, other)
90            }
91            #[inline]
92            fn num_partial_cmp(&self, other: &$T) -> Option<Ordering> {
93                Some($method::<B, false>(self, other))
94            }
95        }
96        impl<const B: Word> NumOrd<Repr<B>> for $T {
97            #[inline]
98            fn num_cmp(&self, other: &Repr<B>) -> Ordering {
99                $method::<B, false>(other, self).reverse()
100            }
101            #[inline]
102            fn num_partial_cmp(&self, other: &Repr<B>) -> Option<Ordering> {
103                Some($method::<B, false>(other, self).reverse())
104            }
105        }
106    };
107}
108impl_num_ord_with_method!(UBig, repr_cmp_ubig);
109impl_num_ord_with_method!(IBig, repr_cmp_ibig);
110
111macro_rules! forward_num_ord_to_repr {
112    ($t:ty) => {
113        impl<R: Round, const B: Word> NumOrd<$t> for FBig<R, B> {
114            #[inline]
115            fn num_cmp(&self, other: &$t) -> Ordering {
116                self.repr.num_cmp(other)
117            }
118            #[inline]
119            fn num_partial_cmp(&self, other: &$t) -> Option<Ordering> {
120                self.repr.num_partial_cmp(other)
121            }
122        }
123
124        impl<R: Round, const B: Word> NumOrd<FBig<R, B>> for $t {
125            #[inline]
126            fn num_cmp(&self, other: &FBig<R, B>) -> Ordering {
127                self.num_cmp(&other.repr)
128            }
129            #[inline]
130            fn num_partial_cmp(&self, other: &FBig<R, B>) -> Option<Ordering> {
131                self.num_partial_cmp(&other.repr)
132            }
133        }
134    };
135}
136forward_num_ord_to_repr!(UBig);
137forward_num_ord_to_repr!(IBig);
138
139macro_rules! impl_num_ord_fbig_unsigned {
140    ($($t:ty)*) => {$(
141        impl<const B: Word> NumOrd<$t> for Repr<B> {
142            #[inline]
143            fn num_partial_cmp(&self, other: &$t) -> Option<Ordering> {
144                Some(repr_cmp_ubig::<B, false>(self, &UBig::from(*other)))
145            }
146        }
147        impl<const B: Word> NumOrd<Repr<B>> for $t {
148            #[inline]
149            fn num_partial_cmp(&self, other: &Repr<B>) -> Option<Ordering> {
150                Some(repr_cmp_ubig::<B, false>(other, &UBig::from(*self)).reverse())
151            }
152        }
153        impl<R: Round, const B: Word> NumOrd<$t> for FBig<R, B> {
154            #[inline]
155            fn num_partial_cmp(&self, other: &$t) -> Option<Ordering> {
156                Some(repr_cmp_ubig::<B, false>(&self.repr, &UBig::from(*other)))
157            }
158        }
159        impl<R: Round, const B: Word> NumOrd<FBig<R, B>> for $t {
160            #[inline]
161            fn num_partial_cmp(&self, other: &FBig<R, B>) -> Option<Ordering> {
162                Some(repr_cmp_ubig::<B, false>(&other.repr, &UBig::from(*self)).reverse())
163            }
164        }
165    )*};
166}
167impl_num_ord_fbig_unsigned!(u8 u16 u32 u64 u128 usize);
168
169macro_rules! impl_num_ord_with_signed {
170    ($($t:ty)*) => {$(
171        impl<const B: Word> NumOrd<$t> for Repr<B> {
172            #[inline]
173            fn num_partial_cmp(&self, other: &$t) -> Option<Ordering> {
174                Some(repr_cmp_ibig::<B, false>(self, &IBig::from(*other)))
175            }
176        }
177        impl<const B: Word> NumOrd<Repr<B>> for $t {
178            #[inline]
179            fn num_partial_cmp(&self, other: &Repr<B>) -> Option<Ordering> {
180                Some(repr_cmp_ibig::<B, false>(other, &IBig::from(*self)).reverse())
181            }
182        }
183        impl<R: Round, const B: Word> NumOrd<$t> for FBig<R, B> {
184            #[inline]
185            fn num_partial_cmp(&self, other: &$t) -> Option<Ordering> {
186                Some(repr_cmp_ibig::<B, false>(&self.repr, &IBig::from(*other)))
187            }
188        }
189        impl<R: Round, const B: Word> NumOrd<FBig<R, B>> for $t {
190            #[inline]
191            fn num_partial_cmp(&self, other: &FBig<R, B>) -> Option<Ordering> {
192                Some(repr_cmp_ibig::<B, false>(&other.repr, &IBig::from(*self)).reverse())
193            }
194        }
195    )*};
196}
197impl_num_ord_with_signed!(i8 i16 i32 i64 i128 isize);
198
199macro_rules! impl_num_ord_with_float {
200    ($($t:ty)*) => {$(
201        impl<const B: Word> NumOrd<$t> for Repr<B> {
202            #[inline]
203            fn num_partial_cmp(&self, other: &$t) -> Option<Ordering> {
204                // step0: compare with nan and 0
205                if other.is_nan() {
206                    return None;
207                } else if *other == 0. {
208                    // primitive `0.` matches both `+0.0` and `-0.0`; either signed zero of
209                    // `self` is numerically equal to it (Repr treats `+0` and `-0` as equal),
210                    // and any other finite/infinite `self` compares by sign against that zero.
211                    return if self.is_pos_zero() || self.is_neg_zero() {
212                        Some(Ordering::Equal)
213                    } else {
214                        Some(self.sign() * Ordering::Greater)
215                    };
216                }
217
218                // step1: compare sign
219                let sign = match (self.sign(), other.sign()) {
220                    (Sign::Positive, Sign::Positive) => Sign::Positive,
221                    (Sign::Positive, Sign::Negative) => return Some(Ordering::Greater),
222                    (Sign::Negative, Sign::Positive) => return Some(Ordering::Less),
223                    (Sign::Negative, Sign::Negative) => Sign::Negative,
224                };
225
226                // step2: compare with inf
227                match (self.is_infinite(), other.is_infinite()) {
228                    (true, true) => return Some(Ordering::Equal),
229                    (false, true) => return Some(sign * Ordering::Less),
230                    (true, false) => return Some(sign * Ordering::Greater),
231                    _ => {}
232                };
233
234                // step3: test if the number is bigger than the max float value
235                // Here we don't use EstimatedLog2, since a direct comparison is not that expensive.
236                // We just need a quick way to determine if one number is much larger than the other.
237                // The bit length (essentially ⌊log2(x)⌋ + 1) is used instead here.
238                let self_signif_log2 = self.significand.bit_len() as isize;
239                let self_log2 = self_signif_log2 + B.bit_len() as isize * self.exponent;
240                let (self_log2_lb, self_log2_ub) = if self.exponent >= 0 {
241                    (self_log2 - self.exponent, self_log2)
242                } else {
243                    (self_log2, self_log2 - self.exponent)
244                };
245                if self_log2_lb > (<$t>::MANTISSA_DIGITS as isize + <$t>::MAX_EXP as isize) {
246                    return Some(sign * Ordering::Greater);
247                }
248
249                // step4: decode the float and compare the bits
250                let (other_signif, other_exp) = other.decode().unwrap();
251                let other_log2 = other_signif.bit_len() as isize + other_exp as isize;
252                if self_log2_lb > other_log2 {
253                    return Some(sign * Ordering::Greater);
254                } else if self_log2_ub < other_log2 {
255                    return Some(sign * Ordering::Less);
256                }
257
258                // step5: do the final comparison
259                let (mut lhs, mut rhs) = (self.significand.clone(), IBig::from(other_signif));
260                if self.exponent < 0 {
261                    shl_digits_in_place::<B>(&mut rhs, (-self.exponent) as usize);
262                } else {
263                    shl_digits_in_place::<B>(&mut lhs, self.exponent as usize);
264                }
265                if other_exp < 0 {
266                    lhs <<= (-other_exp) as usize;
267                } else {
268                    rhs <<= other_exp as usize;
269                }
270                Some(lhs.cmp(&rhs))
271            }
272        }
273
274        impl<const B: Word> NumOrd<Repr<B>> for $t {
275            #[inline]
276            fn num_partial_cmp(&self, other: &Repr<B>) -> Option<Ordering> {
277                other.num_partial_cmp(self).map(|ord| ord.reverse())
278            }
279        }
280    )*};
281}
282impl_num_ord_with_float!(f32 f64);
283forward_num_ord_to_repr!(f32);
284forward_num_ord_to_repr!(f64);
285
286impl<const B: Word> Repr<B> {
287    /// The numeric-hash residue (mod 2¹²⁷−1) used by [`NumHash`]:
288    /// `sgn(significand) · (|significand| mod M127) · (B^exponent mod M127)`.
289    ///
290    /// Special values: `+0` → `0`, `-0` → `0`, `+∞` → `HASH_INF` (= `M127`), `-∞` → `HASH_NEGINF`
291    /// (= `-M127`), matching num-order's `f64::fhash`. The subsequent `i128::num_hash` maps both
292    /// `HASH_INF` and `HASH_NEGINF` back to `0`, so the *final* hash of ±∞ is `0` — but the
293    /// *residue* distinguishes them so that composite types (e.g. `CBig`) combine them algebraically
294    /// the same way num-order's `Complex<f64>` does.
295    pub fn num_hash_residue(&self) -> i128 {
296        // 2^127 - 1 is used in the num-order crate
297        type MInt = FixedMersenneInt<127, 1>;
298        const M127: i128 = i128::MAX;
299        const M127U: u128 = M127 as u128;
300
301        if self.significand.is_zero() {
302            // Distinguish infinities (sentinel exponents) from signed zero.
303            return match self.exponent {
304                isize::MAX => M127,          // +∞  → HASH_INF
305                isize::MIN => i128::MIN + 1, // -∞  → HASH_NEGINF (= -M127)
306                _ => 0,                      // ±0
307            };
308        }
309
310        let signif_residue = &self.significand % M127;
311        let signif_hash = MInt::new(signif_residue.unsigned_abs(), &M127U);
312        let exp_hash = if B == 2 {
313            signif_hash.convert(1 << self.exponent.absm(&127))
314        } else if self.exponent < 0 {
315            signif_hash
316                .convert(B as u128)
317                .pow(&(-self.exponent as u128))
318                .inv()
319                .unwrap()
320        } else {
321            signif_hash.convert(B as u128).pow(&(self.exponent as u128))
322        };
323
324        let mut hash = (signif_hash * exp_hash).residue() as i128;
325        if signif_residue < 0 {
326            hash = -hash;
327        }
328        hash
329    }
330}
331
332impl<const B: Word> NumHash for Repr<B> {
333    #[inline]
334    fn num_hash<H: core::hash::Hasher>(&self, state: &mut H) {
335        self.num_hash_residue().num_hash(state)
336    }
337}
338
339impl<R: Round, const B: Word> NumHash for FBig<R, B> {
340    #[inline]
341    fn num_hash<H: core::hash::Hasher>(&self, state: &mut H) {
342        self.repr.num_hash(state)
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use super::*;
349    use crate::DBig;
350    use core::cmp::Ordering;
351    use num_order::{NumHash, NumOrd};
352
353    /// Default binary FBig (Zero rounding, base 2).
354    type FBin = FBig;
355
356    /// Hash a `NumHash` value to u64 for comparison.
357    fn num_hash<T: NumHash>(value: &T) -> u64 {
358        use std::collections::hash_map::DefaultHasher;
359        use std::hash::Hasher;
360        let mut hasher = DefaultHasher::new();
361        value.num_hash(&mut hasher);
362        hasher.finish()
363    }
364
365    /// Capture the i128 residue a `NumHash` impl writes (the `i128` NumHash writes its value via
366    /// `Hasher::write_i128`), so the *field element* can be compared directly.
367    fn residue<T: NumHash>(value: &T) -> i128 {
368        struct Collector(i128);
369        impl core::hash::Hasher for Collector {
370            fn write_i128(&mut self, v: i128) {
371                self.0 = v;
372            }
373            fn write(&mut self, _: &[u8]) {}
374            fn finish(&self) -> u64 {
375                0
376            }
377        }
378        let mut c = Collector(0);
379        value.num_hash(&mut c);
380        c.0
381    }
382
383    // The base-2 Repr residue must equal num-order's f64 `fhash` for the same finite value — this
384    // is what lets dashu-cmplx's CBig reuse Repr residues and stay in sync with num-order's
385    // Complex<f64> hashing.
386    #[test]
387    fn test_fbig_num_hash_matches_f64() {
388        for v in [
389            1.0_f64,
390            2.0,
391            3.0,
392            0.5,
393            0.25,
394            -0.75,
395            100.0,
396            1e-10,
397            1e20,
398            123.456,
399            1.0 / 3.0,
400            f64::INFINITY,
401            f64::NEG_INFINITY,
402            -0.0,
403        ] {
404            let f: FBin = core::convert::TryFrom::try_from(v).unwrap();
405            assert_eq!(residue(&f), residue(&v), "FBig/f64 num_hash disagree for {v}");
406        }
407    }
408
409    // -- NumOrd for Repr (same base) --
410
411    #[test]
412    fn test_num_ord_repr_zero_vs_neg_zero() {
413        // +0 == -0 (IEEE 754)
414        assert_eq!(Repr::<2>::zero().num_cmp(&Repr::<2>::neg_zero()), Ordering::Equal);
415        assert_eq!(Repr::<2>::neg_zero().num_cmp(&Repr::<2>::zero()), Ordering::Equal);
416    }
417
418    #[test]
419    fn test_num_ord_repr_neg_zero_vs_finite() {
420        let one = Repr::<2>::one();
421        let neg_one = Repr::<2>::neg_one();
422        // -0 < positive
423        assert_eq!(Repr::<2>::neg_zero().num_cmp(&one), Ordering::Less);
424        // -0 > negative
425        assert_eq!(Repr::<2>::neg_zero().num_cmp(&neg_one), Ordering::Greater);
426    }
427
428    #[test]
429    fn test_num_ord_repr_infinities() {
430        // +inf > -inf
431        assert_eq!(Repr::<2>::infinity().num_cmp(&Repr::<2>::neg_infinity()), Ordering::Greater);
432        // -inf < +inf
433        assert_eq!(Repr::<2>::neg_infinity().num_cmp(&Repr::<2>::infinity()), Ordering::Less);
434        // +inf == +inf
435        assert_eq!(Repr::<2>::infinity().num_cmp(&Repr::<2>::infinity()), Ordering::Equal);
436        // -inf == -inf
437        assert_eq!(Repr::<2>::neg_infinity().num_cmp(&Repr::<2>::neg_infinity()), Ordering::Equal);
438    }
439
440    #[test]
441    fn test_num_ord_repr_zero_vs_infinity() {
442        // +0 < +inf
443        assert_eq!(Repr::<2>::zero().num_cmp(&Repr::<2>::infinity()), Ordering::Less);
444        // -0 < +inf
445        assert_eq!(Repr::<2>::neg_zero().num_cmp(&Repr::<2>::infinity()), Ordering::Less);
446        // +0 > -inf
447        assert_eq!(Repr::<2>::zero().num_cmp(&Repr::<2>::neg_infinity()), Ordering::Greater);
448        // -0 > -inf
449        assert_eq!(Repr::<2>::neg_zero().num_cmp(&Repr::<2>::neg_infinity()), Ordering::Greater);
450    }
451
452    // -- NumOrd for Repr (cross-base) --
453
454    #[test]
455    fn test_num_ord_repr_cross_base_zero() {
456        // Base-2 neg_zero == Base-10 zero
457        assert_eq!(Repr::<2>::neg_zero().num_cmp(&Repr::<10>::zero()), Ordering::Equal);
458        // Base-2 neg_zero == Base-10 neg_zero
459        assert_eq!(Repr::<2>::neg_zero().num_cmp(&Repr::<10>::neg_zero()), Ordering::Equal);
460    }
461
462    #[test]
463    fn test_num_ord_repr_cross_base_infinity() {
464        // Base-2 +inf == Base-10 +inf
465        assert_eq!(Repr::<2>::infinity().num_cmp(&Repr::<10>::infinity()), Ordering::Equal);
466        // Base-2 +inf > Base-10 -inf
467        assert_eq!(Repr::<2>::infinity().num_cmp(&Repr::<10>::neg_infinity()), Ordering::Greater);
468        // Base-2 -inf == Base-10 -inf
469        assert_eq!(Repr::<2>::neg_infinity().num_cmp(&Repr::<10>::neg_infinity()), Ordering::Equal);
470    }
471
472    // -- NumOrd for FBig --
473
474    #[test]
475    fn test_num_ord_fbig_neg_zero() {
476        let negz: FBin = FBig::from_repr_const(Repr::<2>::neg_zero());
477        let posz = FBin::ZERO;
478        assert_eq!(negz.num_cmp(&posz), Ordering::Equal);
479        assert_eq!(posz.num_cmp(&negz), Ordering::Equal);
480
481        // -0 < +1, -0 > -1
482        assert_eq!(negz.num_cmp(&FBin::ONE), Ordering::Less);
483        assert_eq!(negz.num_cmp(&FBin::NEG_ONE), Ordering::Greater);
484    }
485
486    #[test]
487    fn test_num_ord_fbig_cross_base_zero() {
488        let negz: FBin = FBig::from_repr_const(Repr::<2>::neg_zero());
489        assert_eq!(negz.num_cmp(&DBig::ZERO), Ordering::Equal);
490        assert_eq!(DBig::ZERO.num_cmp(&negz), Ordering::Equal);
491    }
492
493    // -- NumHash for Repr --
494
495    #[test]
496    fn test_num_hash_repr_zero_neg_zero_equal() {
497        // +0 and -0 compare equal, so they must hash the same
498        assert_eq!(num_hash(&Repr::<2>::zero()), num_hash(&Repr::<2>::neg_zero()));
499        assert_eq!(num_hash(&Repr::<10>::zero()), num_hash(&Repr::<10>::neg_zero()));
500    }
501
502    #[test]
503    fn test_num_hash_repr_infinities_same_sign() {
504        // Same-sign infinities hash the same
505        assert_eq!(num_hash(&Repr::<2>::infinity()), num_hash(&Repr::<10>::infinity()));
506        assert_eq!(num_hash(&Repr::<2>::neg_infinity()), num_hash(&Repr::<10>::neg_infinity()));
507    }
508
509    #[test]
510    fn test_num_hash_repr_zero_matches_integer_zero() {
511        // +0 and -0 should hash the same as integer zero
512        assert_eq!(num_hash(&Repr::<2>::zero()), num_hash(&0i128));
513        assert_eq!(num_hash(&Repr::<2>::neg_zero()), num_hash(&0i128));
514    }
515
516    // -- NumHash for FBig --
517
518    #[test]
519    fn test_num_hash_fbig_neg_zero() {
520        let negz: FBin = FBig::from_repr_const(Repr::<2>::neg_zero());
521        assert_eq!(num_hash(&negz), num_hash(&FBin::ZERO));
522    }
523
524    #[test]
525    fn test_num_hash_fbig_cross_base_zero() {
526        let negz: FBin = FBig::from_repr_const(Repr::<2>::neg_zero());
527        assert_eq!(num_hash(&negz), num_hash(&DBig::ZERO));
528    }
529}