Skip to main content

dashu_float/
cmp.rs

1use core::cmp::Ordering;
2
3use dashu_base::{AbsOrd, EstimatedLog2, Sign};
4use dashu_int::{IBig, UBig};
5
6use crate::{
7    fbig::FBig,
8    repr::Repr,
9    repr::Word,
10    round::Round,
11    utils::{shl_digits, shl_digits_in_place},
12};
13
14impl<R1: Round, R2: Round, const B: Word> PartialEq<FBig<R2, B>> for FBig<R1, B> {
15    #[inline]
16    fn eq(&self, other: &FBig<R2, B>) -> bool {
17        match (self.repr.is_infinite(), other.repr.is_infinite()) {
18            // +inf == +inf, -inf == -inf
19            (true, true) => !((self.repr.exponent >= 0) ^ (other.repr.exponent >= 0)),
20
21            // the representation is normalized so direct comparing is okay,
22            // and the context doesn't count in comparison
23            (false, false) => self.repr == other.repr,
24
25            // inf != any exact numbers
26            (_, _) => false,
27        }
28    }
29}
30impl<R: Round, const B: Word> Eq for FBig<R, B> {}
31
32/// Compare two [`Repr`]s, optionally at the precision of the [`FBig`] contexts they came from.
33///
34/// `precision = None` compares the **exact** values: neither side is rounded, whatever the
35/// significands' widths. The `Some((lhs_prec, rhs_prec))` form is the `FBig` comparison's
36/// magnitude shortcut, and it only applies when *both* precisions are nonzero — an
37/// unlimited-precision (0) operand makes the comparison exact again, which is what comparing
38/// against `FBig::ONE`/`ZERO` gets.
39///
40/// The distinction is load-bearing at the inverse transcendentals' domain checks and `±1`
41/// endpoint rows: `±1` is a *pole* there (`asin`/`acos`/`acosh`/`atanh` have a vertical tangent,
42/// `ln_1p` has one at `−1`), so an argument that merely *rounds* onto the endpoint is not the
43/// endpoint value — its true result sits `√(2δ)` away, many ulps at a fixed work precision
44/// (`asin(1 − 10⁻¹⁵⁵)` at 100 digits is `π/2 − 1.4·10⁻⁷⁸`) — and anything that *snaps* to the
45/// endpoint certifies a result the true one is nowhere near. Those sites pass `None`. Away from
46/// the poles (the `±1/2` rows, where the derivative is bounded) a rounded comparison is safe
47/// instead: a sub-ulp argument perturbation moves the result by at most an ulp, and `1/2` is not
48/// even representable in an odd base.
49pub(crate) fn repr_cmp_same_base<const B: Word, const ABS: bool>(
50    lhs: &Repr<B>,
51    rhs: &Repr<B>,
52    precision: Option<(usize, usize)>,
53) -> Ordering {
54    // case 1: compare with inf
55    match (lhs.is_infinite(), rhs.is_infinite()) {
56        (true, true) => {
57            return if ABS {
58                Ordering::Equal
59            } else {
60                lhs.exponent.cmp(&rhs.exponent)
61            }
62        }
63        (false, true) => {
64            return match ABS || rhs.exponent >= 0 {
65                true => Ordering::Less,
66                false => Ordering::Greater,
67            }
68        }
69        (true, false) => {
70            return match ABS || lhs.exponent >= 0 {
71                true => Ordering::Greater,
72                false => Ordering::Less,
73            }
74        }
75        _ => {}
76    };
77
78    // case 2: compare sign
79    let sign = if ABS {
80        Sign::Positive
81    } else {
82        match (lhs.significand.sign(), rhs.significand.sign()) {
83            (Sign::Positive, Sign::Positive) => Sign::Positive,
84            (Sign::Positive, Sign::Negative) => return Ordering::Greater,
85            (Sign::Negative, Sign::Positive) => return Ordering::Less,
86            (Sign::Negative, Sign::Negative) => Sign::Negative,
87        }
88    };
89
90    // case 3: compare with 0 (both +0 and -0 are zero)
91    match (lhs.significand.is_zero(), rhs.significand.is_zero()) {
92        (true, true) => return Ordering::Equal,
93        (true, false) => {
94            // rhs must be positive, otherwise case 2 will return
95            return Ordering::Less;
96        }
97        (false, true) => {
98            // lhs must be positive, otherwise case 2 will return
99            return Ordering::Greater;
100        }
101        _ => {}
102    }
103
104    // case 4: compare exponent and precision
105    let (lhs_exp, rhs_exp) = (lhs.exponent, rhs.exponent);
106    if let Some((lhs_prec, rhs_prec)) = precision {
107        // only compare when both number are not having arbitrary precision
108        if lhs_prec != 0 && rhs_prec != 0 {
109            // Saturating: an exponent near `isize::MAX` (e.g. the result of `powi(2, n)` for a
110            // near-max `n`, which is representable so the range guard doesn't short-circuit it)
111            // makes `exp + precision` overflow. These are magnitude shortcuts only — saturating
112            // just forgoes the shortcut (falling through to the exact case 6), it never mis-orders.
113            if lhs_exp > rhs_exp.saturating_add(rhs_prec as isize) {
114                return sign * Ordering::Greater;
115            }
116            if rhs_exp > lhs_exp.saturating_add(lhs_prec as isize) {
117                return sign * Ordering::Less;
118            }
119        }
120    }
121
122    // case 5: compare exponent and digits
123    let (lhs_digits, rhs_digits) = (lhs.digits_ub(), rhs.digits_ub());
124    if lhs_exp > rhs_exp.saturating_add(rhs_digits as isize) {
125        return sign * Ordering::Greater;
126    }
127    if rhs_exp > lhs_exp.saturating_add(lhs_digits as isize) {
128        return sign * Ordering::Less;
129    }
130
131    // case 6: compare exact values by shifting
132    let (lhs_signif, rhs_signif) = (&lhs.significand, &rhs.significand);
133    if ABS {
134        match lhs_exp.cmp(&rhs_exp) {
135            Ordering::Equal => lhs_signif.abs_cmp(rhs_signif),
136            Ordering::Greater => {
137                shl_digits::<B>(lhs_signif, (lhs_exp - rhs_exp) as usize).abs_cmp(rhs_signif)
138            }
139            Ordering::Less => {
140                lhs_signif.abs_cmp(&shl_digits::<B>(rhs_signif, (rhs_exp - lhs_exp) as usize))
141            }
142        }
143    } else {
144        match lhs_exp.cmp(&rhs_exp) {
145            Ordering::Equal => lhs_signif.cmp(rhs_signif),
146            Ordering::Greater => {
147                shl_digits::<B>(lhs_signif, (lhs_exp - rhs_exp) as usize).cmp(rhs_signif)
148            }
149            Ordering::Less => {
150                lhs_signif.cmp(&shl_digits::<B>(rhs_signif, (rhs_exp - lhs_exp) as usize))
151            }
152        }
153    }
154}
155
156impl<const B: Word> PartialOrd for Repr<B> {
157    #[inline]
158    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
159        Some(self.cmp(other))
160    }
161}
162
163impl<const B: Word> Ord for Repr<B> {
164    #[inline]
165    fn cmp(&self, other: &Self) -> Ordering {
166        repr_cmp_same_base::<B, false>(self, other, None)
167    }
168}
169
170impl<R1: Round, R2: Round, const B: Word> PartialOrd<FBig<R2, B>> for FBig<R1, B> {
171    #[inline]
172    fn partial_cmp(&self, other: &FBig<R2, B>) -> Option<Ordering> {
173        Some(repr_cmp_same_base::<B, false>(
174            &self.repr,
175            &other.repr,
176            Some((self.context.precision, other.context.precision)),
177        ))
178    }
179}
180
181impl<R: Round, const B: Word> Ord for FBig<R, B> {
182    #[inline]
183    fn cmp(&self, other: &Self) -> Ordering {
184        repr_cmp_same_base::<B, false>(
185            &self.repr,
186            &other.repr,
187            Some((self.context.precision, other.context.precision)),
188        )
189    }
190}
191
192impl<R: Round, const B: Word> AbsOrd for FBig<R, B> {
193    #[inline]
194    fn abs_cmp(&self, other: &Self) -> Ordering {
195        repr_cmp_same_base::<B, true>(
196            &self.repr,
197            &other.repr,
198            Some((self.context.precision, other.context.precision)),
199        )
200    }
201}
202
203pub(crate) fn repr_cmp_ubig<const B: Word, const ABS: bool>(lhs: &Repr<B>, rhs: &UBig) -> Ordering {
204    // case 1: compare with inf
205    if lhs.is_infinite() {
206        return if lhs.exponent > 0 || ABS {
207            Ordering::Greater
208        } else {
209            Ordering::Less
210        };
211    }
212
213    // case 2: compare sign
214    if !ABS && lhs.significand.sign() == Sign::Negative {
215        return Ordering::Less;
216    }
217
218    // case 3: compare log2 estimations
219    let (lhs_lo, lhs_hi) = lhs.log2_bounds();
220    let (rhs_lo, rhs_hi) = rhs.log2_bounds();
221    if lhs_lo > rhs_hi {
222        return Ordering::Greater;
223    }
224    if lhs_hi < rhs_lo {
225        return Ordering::Less;
226    }
227
228    // case 4: compare the exact values
229    let mut rhs: IBig = rhs.clone().into();
230    if lhs.exponent < 0 {
231        shl_digits_in_place::<B>(&mut rhs, (-lhs.exponent) as usize);
232        lhs.significand.cmp(&rhs)
233    } else {
234        shl_digits::<B>(&lhs.significand, lhs.exponent as usize).cmp(&rhs)
235    }
236}
237
238pub(crate) fn repr_cmp_ibig<const B: Word, const ABS: bool>(lhs: &Repr<B>, rhs: &IBig) -> Ordering {
239    // case 1: compare with inf
240    if lhs.is_infinite() {
241        return if lhs.exponent > 0 || ABS {
242            Ordering::Greater
243        } else {
244            Ordering::Less
245        };
246    }
247
248    // case 2: compare sign
249    let sign = if ABS {
250        Sign::Positive
251    } else {
252        match (lhs.significand.sign(), rhs.sign()) {
253            (Sign::Positive, Sign::Positive) => Sign::Positive,
254            (Sign::Positive, Sign::Negative) => return Ordering::Greater,
255            (Sign::Negative, Sign::Positive) => return Ordering::Less,
256            (Sign::Negative, Sign::Negative) => Sign::Negative,
257        }
258    };
259
260    // case 3: compare log2 estimations
261    let (lhs_lo, lhs_hi) = lhs.log2_bounds();
262    let (rhs_lo, rhs_hi) = rhs.log2_bounds();
263    if lhs_lo > rhs_hi {
264        return sign * Ordering::Greater;
265    }
266    if lhs_hi < rhs_lo {
267        return sign * Ordering::Less;
268    }
269
270    // case 4: compare the exact values
271    if lhs.exponent < 0 {
272        lhs.significand
273            .cmp(&shl_digits::<B>(rhs, (-lhs.exponent) as usize))
274    } else {
275        shl_digits::<B>(&lhs.significand, lhs.exponent as usize).cmp(rhs)
276    }
277}
278
279macro_rules! impl_abs_ord_with_method {
280    ($T:ty, $method:ident) => {
281        impl<const B: Word> AbsOrd<$T> for Repr<B> {
282            #[inline]
283            fn abs_cmp(&self, other: &$T) -> Ordering {
284                $method::<B, true>(self, other)
285            }
286        }
287        impl<const B: Word> AbsOrd<Repr<B>> for $T {
288            #[inline]
289            fn abs_cmp(&self, other: &Repr<B>) -> Ordering {
290                $method::<B, true>(other, self).reverse()
291            }
292        }
293        impl<R: Round, const B: Word> AbsOrd<$T> for FBig<R, B> {
294            #[inline]
295            fn abs_cmp(&self, other: &$T) -> Ordering {
296                $method::<B, true>(&self.repr, other)
297            }
298        }
299        impl<R: Round, const B: Word> AbsOrd<FBig<R, B>> for $T {
300            #[inline]
301            fn abs_cmp(&self, other: &FBig<R, B>) -> Ordering {
302                $method::<B, true>(&other.repr, self).reverse()
303            }
304        }
305    };
306}
307impl_abs_ord_with_method!(UBig, repr_cmp_ubig);
308impl_abs_ord_with_method!(IBig, repr_cmp_ibig);