Skip to main content

oxiz_math/
fast_rational.rs

1//! Fast rational number type for Simplex performance.
2//!
3//! [`FastRational`] uses a two-tier representation:
4//! - **Small**: `i64/i64` for values that fit without overflow (>95% of simplex operations)
5//! - **Big**: Heap-allocated `BigRational` as a fallback when overflow occurs
6//!
7//! This avoids heap allocation for the common case, providing 8-12% speedup
8//! on LRA benchmarks compared to always using `BigRational`.
9//!
10//! ## Invariants
11//!
12//! For `Small { num, den }`:
13//! - `den > 0` (sign is always on the numerator)
14//! - `gcd(|num|, den) == 1` (always in lowest terms)
15//!
16//! ## Consistency
17//!
18//! - `Eq` and `Hash` are consistent: the same rational value always compares
19//!   equal and hashes the same regardless of representation.
20//! - `Ord` is a total order consistent with rational ordering.
21
22#[allow(unused_imports)]
23use crate::prelude::*;
24use core::cmp::Ordering;
25use core::fmt;
26use core::hash::{Hash, Hasher};
27use core::ops::{
28    Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign,
29};
30use num_bigint::BigInt;
31use num_integer::Integer;
32use num_rational::BigRational;
33use num_traits::{One, Signed, Zero};
34
35/// A rational number that uses `i64/i64` for small values and falls back to `BigRational`.
36///
37/// Over 95% of Simplex operations stay in the small path, avoiding heap allocation.
38#[derive(Clone, Debug)]
39pub enum FastRational {
40    /// Small representation: `num / den` where `den > 0` and `gcd(|num|, den) == 1`.
41    Small {
42        /// Numerator (can be negative, zero, or positive).
43        num: i64,
44        /// Denominator (always positive, always coprime with `|num|`).
45        den: i64,
46    },
47    /// Big representation: heap-allocated arbitrary-precision rational.
48    Big(Box<BigRational>),
49}
50
51// ---------------------------------------------------------------------------
52// GCD helper
53// ---------------------------------------------------------------------------
54
55/// Binary GCD (Stein's algorithm) for `i64` values.
56///
57/// Inputs are taken as absolute values internally. Returns `gcd(|a|, |b|)`.
58#[inline]
59fn gcd_i64(a: i64, b: i64) -> i64 {
60    // Take absolute values (saturating for i64::MIN)
61    let mut a = a.unsigned_abs();
62    let mut b = b.unsigned_abs();
63    if a == 0 {
64        return b as i64;
65    }
66    if b == 0 {
67        return a as i64;
68    }
69    // Use Stein's binary GCD on unsigned values
70    let shift = (a | b).trailing_zeros();
71    a >>= a.trailing_zeros();
72    loop {
73        b >>= b.trailing_zeros();
74        if a > b {
75            core::mem::swap(&mut a, &mut b);
76        }
77        b -= a;
78        if b == 0 {
79            break;
80        }
81    }
82    (a << shift) as i64
83}
84
85// ---------------------------------------------------------------------------
86// Construction & normalization
87// ---------------------------------------------------------------------------
88
89impl FastRational {
90    /// Create a new `Small` rational, normalizing sign and reducing to lowest terms.
91    ///
92    /// A non-zero `den` is a caller obligation. It is checked by a
93    /// `debug_assert!`, so the check is compiled out in release builds; passing
94    /// `den == 0` there is not a trap and not undefined behaviour, it simply
95    /// yields a malformed `Small` whose denominator stays `0` and which poisons
96    /// every later operation on the value.
97    ///
98    /// This is about the *denominator argument* of this constructor. It says
99    /// nothing about dividing *by* zero: [`Div`] for `FastRational` rejects a
100    /// zero divisor with a panic in every build profile.
101    ///
102    /// # Panics
103    ///
104    /// Panics in debug builds if `den == 0`.
105    #[inline]
106    pub fn new_small(num: i64, den: i64) -> Self {
107        debug_assert!(den != 0, "FastRational: denominator must not be zero");
108        if num == 0 {
109            return FastRational::Small { num: 0, den: 1 };
110        }
111        let (n, d) = if den < 0 {
112            // Negate both to ensure den > 0
113            // Handle i64::MIN carefully
114            match (num.checked_neg(), den.checked_neg()) {
115                (Some(nn), Some(dd)) => (nn, dd),
116                _ => {
117                    // Overflow on negation -- promote to Big
118                    let big = BigRational::new(BigInt::from(num), BigInt::from(den));
119                    return FastRational::Big(Box::new(big));
120                }
121            }
122        } else {
123            (num, den)
124        };
125        // NB: pass `n` directly rather than `n.saturating_abs()`. `gcd_i64`
126        // already takes the absolute value via `unsigned_abs()`, which
127        // (unlike `i64::saturating_abs`) is exact for `i64::MIN` (2^63 fits
128        // in `u64`). Pre-computing a saturated abs here would corrupt the
129        // gcd for `n == i64::MIN` and silently produce a wrong reduced
130        // fraction (or, for `new_small`, an unreduced `Small` that violates
131        // the lowest-terms invariant).
132        let g = gcd_i64(n, d);
133        if g == 0 {
134            return FastRational::Small { num: 0, den: 1 };
135        }
136        FastRational::Small {
137            num: n / g,
138            den: d / g,
139        }
140    }
141
142    /// Create a `FastRational` from a `BigRational`, demoting to `Small` if it fits.
143    pub fn from_big(br: BigRational) -> Self {
144        // Try to fit into i64/i64
145        let n_opt: Option<i64> = br.numer().try_into().ok();
146        let d_opt: Option<i64> = br.denom().try_into().ok();
147        match (n_opt, d_opt) {
148            (Some(n), Some(d)) if d != 0 => FastRational::new_small(n, d),
149            _ => FastRational::Big(Box::new(br)),
150        }
151    }
152
153    /// Convert to `BigRational`.
154    pub fn to_big_rational(&self) -> BigRational {
155        match self {
156            FastRational::Small { num, den } => {
157                BigRational::new(BigInt::from(*num), BigInt::from(*den))
158            }
159            FastRational::Big(b) => (**b).clone(),
160        }
161    }
162
163    /// Convert to `f64` (lossy).
164    #[inline]
165    pub fn to_f64(&self) -> f64 {
166        match self {
167            FastRational::Small { num, den } => *num as f64 / *den as f64,
168            FastRational::Big(b) => {
169                use num_traits::ToPrimitive;
170                b.numer().to_f64().unwrap_or(f64::NAN) / b.denom().to_f64().unwrap_or(f64::NAN)
171            }
172        }
173    }
174
175    /// Return the reciprocal (1/self). Returns `None` if self is zero.
176    pub fn recip(&self) -> Option<Self> {
177        match self {
178            FastRational::Small { num, den } => {
179                if *num == 0 {
180                    None
181                } else {
182                    Some(FastRational::new_small(*den, *num))
183                }
184            }
185            FastRational::Big(b) => {
186                if b.is_zero() {
187                    None
188                } else {
189                    Some(FastRational::from_big(b.recip()))
190                }
191            }
192        }
193    }
194
195    /// Compute the floor (greatest integer <= self).
196    pub fn floor(&self) -> BigInt {
197        match self {
198            FastRational::Small { num, den } => {
199                if *den == 1 {
200                    BigInt::from(*num)
201                } else {
202                    let q = num.div_floor(den);
203                    BigInt::from(q)
204                }
205            }
206            FastRational::Big(b) => crate::rational::floor(b),
207        }
208    }
209
210    /// Compute the ceiling (smallest integer >= self).
211    pub fn ceil(&self) -> BigInt {
212        match self {
213            FastRational::Small { num, den } => {
214                if *den == 1 {
215                    BigInt::from(*num)
216                } else {
217                    // Use `Integer::div_ceil`, which computes ceil(n/d) via
218                    // `div_rem` (truncating division + adjustment) rather
219                    // than `(n + d - 1) / d`. The naive formula overflows
220                    // `i64` whenever `num + den` exceeds `i64::MAX` (e.g.
221                    // `Small { num: i64::MAX, den: 2 }`); `div_rem` never
222                    // adds `num` and `den` together, so it cannot overflow
223                    // for any `den > 0` (guaranteed by the `Small`
224                    // invariant), mirroring `div_floor` used in `floor()`.
225                    let q = num.div_ceil(den);
226                    BigInt::from(q)
227                }
228            }
229            FastRational::Big(b) => crate::rational::ceil(b),
230        }
231    }
232
233    /// Return the absolute value.
234    pub fn abs(&self) -> Self {
235        match self {
236            FastRational::Small { num, den } => {
237                match num.checked_abs() {
238                    Some(n) => FastRational::Small { num: n, den: *den },
239                    None => {
240                        // num == i64::MIN
241                        let big = BigRational::new(BigInt::from(*num).abs(), BigInt::from(*den));
242                        FastRational::from_big(big)
243                    }
244                }
245            }
246            FastRational::Big(b) => FastRational::from_big(b.abs()),
247        }
248    }
249
250    /// Return the numerator as a `BigInt`.
251    pub fn numer(&self) -> BigInt {
252        match self {
253            FastRational::Small { num, .. } => BigInt::from(*num),
254            FastRational::Big(b) => b.numer().clone(),
255        }
256    }
257
258    /// Return the denominator as a `BigInt`.
259    pub fn denom(&self) -> BigInt {
260        match self {
261            FastRational::Small { den, .. } => BigInt::from(*den),
262            FastRational::Big(b) => b.denom().clone(),
263        }
264    }
265
266    /// Check if this rational is an integer (denominator == 1).
267    #[inline]
268    pub fn is_integer(&self) -> bool {
269        match self {
270            FastRational::Small { den, .. } => *den == 1,
271            FastRational::Big(b) => b.is_integer(),
272        }
273    }
274
275    /// Create a `FastRational` from a `BigRational` reference.
276    pub fn from_big_ref(br: &BigRational) -> Self {
277        let n_opt: Option<i64> = br.numer().try_into().ok();
278        let d_opt: Option<i64> = br.denom().try_into().ok();
279        match (n_opt, d_opt) {
280            (Some(n), Some(d)) if d != 0 => FastRational::new_small(n, d),
281            _ => FastRational::Big(Box::new(br.clone())),
282        }
283    }
284
285    /// Create a `FastRational` representing an integer from a `BigInt`.
286    pub fn from_bigint(bi: &BigInt) -> Self {
287        let n_opt: Option<i64> = bi.try_into().ok();
288        match n_opt {
289            Some(n) => FastRational::Small { num: n, den: 1 },
290            None => FastRational::Big(Box::new(BigRational::from_integer(bi.clone()))),
291        }
292    }
293}
294
295// ---------------------------------------------------------------------------
296// Small-path arithmetic (inlined for performance)
297// ---------------------------------------------------------------------------
298
299/// Add two small rationals, falling back to Big on overflow.
300#[inline]
301fn add_small(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> FastRational {
302    // a/b + c/d = (a*d + c*b) / (b*d) -- but check overflow at each step
303    if let (Some(ad), Some(cb), Some(bd)) = (
304        a_num.checked_mul(b_den),
305        b_num.checked_mul(a_den),
306        a_den.checked_mul(b_den),
307    ) && let Some(num) = ad.checked_add(cb)
308    {
309        return FastRational::new_small(num, bd);
310    }
311    // Overflow: fall back to Big
312    let a = BigRational::new(BigInt::from(a_num), BigInt::from(a_den));
313    let b = BigRational::new(BigInt::from(b_num), BigInt::from(b_den));
314    FastRational::from_big(a + b)
315}
316
317/// Subtract two small rationals, falling back to Big on overflow.
318#[inline]
319fn sub_small(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> FastRational {
320    // a/b - c/d = (a*d - c*b) / (b*d)
321    if let (Some(ad), Some(cb), Some(bd)) = (
322        a_num.checked_mul(b_den),
323        b_num.checked_mul(a_den),
324        a_den.checked_mul(b_den),
325    ) && let Some(num) = ad.checked_sub(cb)
326    {
327        return FastRational::new_small(num, bd);
328    }
329    let a = BigRational::new(BigInt::from(a_num), BigInt::from(a_den));
330    let b = BigRational::new(BigInt::from(b_num), BigInt::from(b_den));
331    FastRational::from_big(a - b)
332}
333
334/// Multiply two small rationals, falling back to Big on overflow.
335#[inline]
336fn mul_small(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> FastRational {
337    // (a/b) * (c/d) = (a*c) / (b*d)
338    // Cross-reduce first to minimize overflow chance.
339    // NB: pass the numerators directly (not `.saturating_abs()`) -- see the
340    // comment in `new_small` for why a pre-saturated abs corrupts the gcd
341    // (and hence the product) when a numerator is `i64::MIN`.
342    let g1 = gcd_i64(a_num, b_den);
343    let g2 = gcd_i64(b_num, a_den);
344    let an = if g1 != 0 { a_num / g1 } else { a_num };
345    let bd = if g1 != 0 { b_den / g1 } else { b_den };
346    let bn = if g2 != 0 { b_num / g2 } else { b_num };
347    let ad = if g2 != 0 { a_den / g2 } else { a_den };
348
349    if let (Some(num), Some(den)) = (an.checked_mul(bn), ad.checked_mul(bd)) {
350        return FastRational::new_small(num, den);
351    }
352    let a = BigRational::new(BigInt::from(a_num), BigInt::from(a_den));
353    let b = BigRational::new(BigInt::from(b_num), BigInt::from(b_den));
354    FastRational::from_big(a * b)
355}
356
357/// Divide two small rationals, falling back to Big on overflow.
358/// Returns `None` if divisor is zero.
359#[inline]
360fn div_small(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> Option<FastRational> {
361    if b_num == 0 {
362        return None;
363    }
364    // (a/b) / (c/d) = (a*d) / (b*c)
365    // We need new_small to normalize the sign, so just pass through directly.
366    // Use checked operations to detect overflow.
367    if let (Some(num), Some(den)) = (a_num.checked_mul(b_den), a_den.checked_mul(b_num)) {
368        Some(FastRational::new_small(num, den))
369    } else {
370        let a = BigRational::new(BigInt::from(a_num), BigInt::from(a_den));
371        let b = BigRational::new(BigInt::from(b_num), BigInt::from(b_den));
372        Some(FastRational::from_big(a / b))
373    }
374}
375
376// ---------------------------------------------------------------------------
377// Trait: From conversions
378// ---------------------------------------------------------------------------
379
380impl From<i64> for FastRational {
381    #[inline]
382    fn from(n: i64) -> Self {
383        FastRational::Small { num: n, den: 1 }
384    }
385}
386
387impl From<(i64, i64)> for FastRational {
388    fn from((n, d): (i64, i64)) -> Self {
389        if d == 0 {
390            // Return zero as a safe default for zero denominator
391            FastRational::Small { num: 0, den: 1 }
392        } else {
393            FastRational::new_small(n, d)
394        }
395    }
396}
397
398impl From<BigRational> for FastRational {
399    fn from(br: BigRational) -> Self {
400        FastRational::from_big(br)
401    }
402}
403
404impl From<BigInt> for FastRational {
405    fn from(bi: BigInt) -> Self {
406        FastRational::from_bigint(&bi)
407    }
408}
409
410impl From<&BigRational> for FastRational {
411    fn from(br: &BigRational) -> Self {
412        FastRational::from_big_ref(br)
413    }
414}
415
416// ---------------------------------------------------------------------------
417// Trait: Zero, One
418// ---------------------------------------------------------------------------
419
420impl Zero for FastRational {
421    #[inline]
422    fn zero() -> Self {
423        FastRational::Small { num: 0, den: 1 }
424    }
425
426    #[inline]
427    fn is_zero(&self) -> bool {
428        match self {
429            FastRational::Small { num, .. } => *num == 0,
430            FastRational::Big(b) => b.is_zero(),
431        }
432    }
433}
434
435impl One for FastRational {
436    #[inline]
437    fn one() -> Self {
438        FastRational::Small { num: 1, den: 1 }
439    }
440
441    #[inline]
442    fn is_one(&self) -> bool {
443        match self {
444            FastRational::Small { num, den } => *num == 1 && *den == 1,
445            FastRational::Big(b) => b.is_one(),
446        }
447    }
448}
449
450// ---------------------------------------------------------------------------
451// Trait: Signed
452// ---------------------------------------------------------------------------
453
454impl Signed for FastRational {
455    fn abs(&self) -> Self {
456        FastRational::abs(self)
457    }
458
459    fn abs_sub(&self, other: &Self) -> Self {
460        let diff = self - other;
461        if diff.is_positive() {
462            diff
463        } else {
464            FastRational::zero()
465        }
466    }
467
468    fn signum(&self) -> Self {
469        if self.is_zero() {
470            FastRational::zero()
471        } else if self.is_positive() {
472            FastRational::one()
473        } else {
474            FastRational::Small { num: -1, den: 1 }
475        }
476    }
477
478    fn is_positive(&self) -> bool {
479        match self {
480            FastRational::Small { num, .. } => *num > 0,
481            FastRational::Big(b) => b.is_positive(),
482        }
483    }
484
485    fn is_negative(&self) -> bool {
486        match self {
487            FastRational::Small { num, .. } => *num < 0,
488            FastRational::Big(b) => b.is_negative(),
489        }
490    }
491}
492
493impl num_traits::Num for FastRational {
494    type FromStrRadixErr = String;
495
496    fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
497        // Parse as BigRational, then try to demote
498        if let Some((num_str, den_str)) = str.split_once('/') {
499            let num = BigInt::from_str_radix(num_str.trim(), radix)
500                .map_err(|e| format!("invalid numerator: {}", e))?;
501            let den = BigInt::from_str_radix(den_str.trim(), radix)
502                .map_err(|e| format!("invalid denominator: {}", e))?;
503            if den.is_zero() {
504                return Err("denominator is zero".to_string());
505            }
506            Ok(FastRational::from_big(BigRational::new(num, den)))
507        } else {
508            let num = BigInt::from_str_radix(str.trim(), radix)
509                .map_err(|e| format!("invalid integer: {}", e))?;
510            Ok(FastRational::from_bigint(&num))
511        }
512    }
513}
514
515// ---------------------------------------------------------------------------
516// Trait: Neg
517// ---------------------------------------------------------------------------
518
519impl Neg for FastRational {
520    type Output = FastRational;
521
522    #[inline]
523    fn neg(self) -> Self::Output {
524        match self {
525            FastRational::Small { num, den } => match num.checked_neg() {
526                Some(n) => FastRational::Small { num: n, den },
527                None => {
528                    let big = BigRational::new(-BigInt::from(num), BigInt::from(den));
529                    FastRational::Big(Box::new(big))
530                }
531            },
532            FastRational::Big(b) => FastRational::from_big(-*b),
533        }
534    }
535}
536
537impl Neg for &FastRational {
538    type Output = FastRational;
539
540    #[inline]
541    fn neg(self) -> Self::Output {
542        match self {
543            FastRational::Small { num, den } => match num.checked_neg() {
544                Some(n) => FastRational::Small { num: n, den: *den },
545                None => {
546                    let big = BigRational::new(-BigInt::from(*num), BigInt::from(*den));
547                    FastRational::Big(Box::new(big))
548                }
549            },
550            FastRational::Big(b) => FastRational::from_big(-((**b).clone())),
551        }
552    }
553}
554
555// ---------------------------------------------------------------------------
556// Trait: Add
557// ---------------------------------------------------------------------------
558
559impl Add for FastRational {
560    type Output = FastRational;
561
562    #[inline]
563    fn add(self, rhs: FastRational) -> Self::Output {
564        (&self).add(&rhs)
565    }
566}
567
568impl Add<&FastRational> for FastRational {
569    type Output = FastRational;
570
571    #[inline]
572    fn add(self, rhs: &FastRational) -> Self::Output {
573        (&self).add(rhs)
574    }
575}
576
577impl Add<FastRational> for &FastRational {
578    type Output = FastRational;
579
580    #[inline]
581    fn add(self, rhs: FastRational) -> Self::Output {
582        self.add(&rhs)
583    }
584}
585
586impl Add<&FastRational> for &FastRational {
587    type Output = FastRational;
588
589    #[inline]
590    fn add(self, rhs: &FastRational) -> Self::Output {
591        match (self, rhs) {
592            (
593                FastRational::Small { num: an, den: ad },
594                FastRational::Small { num: bn, den: bd },
595            ) => add_small(*an, *ad, *bn, *bd),
596            (a, b) => {
597                let big_a = a.to_big_rational();
598                let big_b = b.to_big_rational();
599                FastRational::from_big(big_a + big_b)
600            }
601        }
602    }
603}
604
605impl AddAssign for FastRational {
606    #[inline]
607    fn add_assign(&mut self, rhs: FastRational) {
608        *self = (&*self) + &rhs;
609    }
610}
611
612impl AddAssign<&FastRational> for FastRational {
613    #[inline]
614    fn add_assign(&mut self, rhs: &FastRational) {
615        *self = (&*self) + rhs;
616    }
617}
618
619// ---------------------------------------------------------------------------
620// Trait: Sub
621// ---------------------------------------------------------------------------
622
623impl Sub for FastRational {
624    type Output = FastRational;
625
626    #[inline]
627    fn sub(self, rhs: FastRational) -> Self::Output {
628        (&self).sub(&rhs)
629    }
630}
631
632impl Sub<&FastRational> for FastRational {
633    type Output = FastRational;
634
635    #[inline]
636    fn sub(self, rhs: &FastRational) -> Self::Output {
637        (&self).sub(rhs)
638    }
639}
640
641impl Sub<FastRational> for &FastRational {
642    type Output = FastRational;
643
644    #[inline]
645    fn sub(self, rhs: FastRational) -> Self::Output {
646        self.sub(&rhs)
647    }
648}
649
650impl Sub<&FastRational> for &FastRational {
651    type Output = FastRational;
652
653    #[inline]
654    fn sub(self, rhs: &FastRational) -> Self::Output {
655        match (self, rhs) {
656            (
657                FastRational::Small { num: an, den: ad },
658                FastRational::Small { num: bn, den: bd },
659            ) => sub_small(*an, *ad, *bn, *bd),
660            (a, b) => {
661                let big_a = a.to_big_rational();
662                let big_b = b.to_big_rational();
663                FastRational::from_big(big_a - big_b)
664            }
665        }
666    }
667}
668
669impl SubAssign for FastRational {
670    #[inline]
671    fn sub_assign(&mut self, rhs: FastRational) {
672        *self = (&*self) - &rhs;
673    }
674}
675
676impl SubAssign<&FastRational> for FastRational {
677    #[inline]
678    fn sub_assign(&mut self, rhs: &FastRational) {
679        *self = (&*self) - rhs;
680    }
681}
682
683// ---------------------------------------------------------------------------
684// Trait: Mul
685// ---------------------------------------------------------------------------
686
687impl Mul for FastRational {
688    type Output = FastRational;
689
690    #[inline]
691    fn mul(self, rhs: FastRational) -> Self::Output {
692        (&self).mul(&rhs)
693    }
694}
695
696impl Mul<&FastRational> for FastRational {
697    type Output = FastRational;
698
699    #[inline]
700    fn mul(self, rhs: &FastRational) -> Self::Output {
701        (&self).mul(rhs)
702    }
703}
704
705impl Mul<FastRational> for &FastRational {
706    type Output = FastRational;
707
708    #[inline]
709    fn mul(self, rhs: FastRational) -> Self::Output {
710        self.mul(&rhs)
711    }
712}
713
714impl Mul<&FastRational> for &FastRational {
715    type Output = FastRational;
716
717    #[inline]
718    fn mul(self, rhs: &FastRational) -> Self::Output {
719        match (self, rhs) {
720            (
721                FastRational::Small { num: an, den: ad },
722                FastRational::Small { num: bn, den: bd },
723            ) => mul_small(*an, *ad, *bn, *bd),
724            (a, b) => {
725                let big_a = a.to_big_rational();
726                let big_b = b.to_big_rational();
727                FastRational::from_big(big_a * big_b)
728            }
729        }
730    }
731}
732
733impl MulAssign for FastRational {
734    #[inline]
735    fn mul_assign(&mut self, rhs: FastRational) {
736        *self = (&*self) * &rhs;
737    }
738}
739
740impl MulAssign<&FastRational> for FastRational {
741    #[inline]
742    fn mul_assign(&mut self, rhs: &FastRational) {
743        *self = (&*self) * rhs;
744    }
745}
746
747// ---------------------------------------------------------------------------
748// Trait: Div
749// ---------------------------------------------------------------------------
750
751impl Div for FastRational {
752    type Output = FastRational;
753
754    /// Divide two `FastRational` values.
755    ///
756    /// # Panics
757    ///
758    /// Panics if the divisor is zero. Unlike the `den == 0` check in
759    /// [`FastRational::new_small`], this one is unconditional: it holds in
760    /// release builds too.
761    #[inline]
762    fn div(self, rhs: FastRational) -> Self::Output {
763        (&self).div(&rhs)
764    }
765}
766
767impl Div<&FastRational> for FastRational {
768    type Output = FastRational;
769
770    #[inline]
771    fn div(self, rhs: &FastRational) -> Self::Output {
772        (&self).div(rhs)
773    }
774}
775
776impl Div<FastRational> for &FastRational {
777    type Output = FastRational;
778
779    #[inline]
780    fn div(self, rhs: FastRational) -> Self::Output {
781        self.div(&rhs)
782    }
783}
784
785impl Div<&FastRational> for &FastRational {
786    type Output = FastRational;
787
788    #[inline]
789    fn div(self, rhs: &FastRational) -> Self::Output {
790        match (self, rhs) {
791            (
792                FastRational::Small { num: an, den: ad },
793                FastRational::Small { num: bn, den: bd },
794            ) => match div_small(*an, *ad, *bn, *bd) {
795                Some(r) => r,
796                // `debug_assert!` is compiled out in release builds, which
797                // previously let this fall through to `FastRational::zero()`
798                // — a silently wrong result rather than the documented
799                // panic. Division by zero must fail loudly in both profiles,
800                // matching `i64`'s own division-by-zero behavior.
801                None => panic!("FastRational: division by zero"),
802            },
803            (a, b) => {
804                if b.is_zero() {
805                    panic!("FastRational: division by zero");
806                }
807                let big_a = a.to_big_rational();
808                let big_b = b.to_big_rational();
809                FastRational::from_big(big_a / big_b)
810            }
811        }
812    }
813}
814
815impl DivAssign for FastRational {
816    #[inline]
817    fn div_assign(&mut self, rhs: FastRational) {
818        *self = (&*self) / &rhs;
819    }
820}
821
822impl DivAssign<&FastRational> for FastRational {
823    #[inline]
824    fn div_assign(&mut self, rhs: &FastRational) {
825        *self = (&*self) / rhs;
826    }
827}
828
829// ---------------------------------------------------------------------------
830// Trait: Rem (required by Num)
831// ---------------------------------------------------------------------------
832
833impl Rem for FastRational {
834    type Output = FastRational;
835
836    /// Remainder for rationals: `a % b = a - b * floor(a/b)`.
837    ///
838    /// For non-zero `b`, this returns a value in `[0, |b|)`.
839    #[inline]
840    fn rem(self, rhs: FastRational) -> Self::Output {
841        (&self).rem(&rhs)
842    }
843}
844
845impl Rem<&FastRational> for &FastRational {
846    type Output = FastRational;
847
848    #[inline]
849    fn rem(self, rhs: &FastRational) -> Self::Output {
850        if rhs.is_zero() {
851            return FastRational::zero();
852        }
853        let quotient = self / rhs;
854        let floor_q = FastRational::from_integer(quotient.floor());
855        self - &(rhs * &floor_q)
856    }
857}
858
859impl RemAssign for FastRational {
860    #[inline]
861    fn rem_assign(&mut self, rhs: FastRational) {
862        *self = (&*self).rem(&rhs);
863    }
864}
865
866// ---------------------------------------------------------------------------
867// Trait: PartialEq, Eq
868// ---------------------------------------------------------------------------
869
870impl PartialEq for FastRational {
871    fn eq(&self, other: &Self) -> bool {
872        match (self, other) {
873            (
874                FastRational::Small { num: an, den: ad },
875                FastRational::Small { num: bn, den: bd },
876            ) => {
877                // Both normalized: direct comparison
878                an == bn && ad == bd
879            }
880            (a, b) => {
881                // Cross-representation comparison
882                a.to_big_rational() == b.to_big_rational()
883            }
884        }
885    }
886}
887
888impl Eq for FastRational {}
889
890// ---------------------------------------------------------------------------
891// Trait: PartialOrd, Ord
892// ---------------------------------------------------------------------------
893
894impl PartialOrd for FastRational {
895    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
896        Some(self.cmp(other))
897    }
898}
899
900impl Ord for FastRational {
901    fn cmp(&self, other: &Self) -> Ordering {
902        match (self, other) {
903            (
904                FastRational::Small { num: an, den: ad },
905                FastRational::Small { num: bn, den: bd },
906            ) => {
907                // Compare a_num/a_den vs b_num/b_den
908                // = a_num * b_den vs b_num * a_den (both dens positive)
909                // Use checked_mul to avoid overflow
910                if let (Some(lhs), Some(rhs)) = (an.checked_mul(*bd), bn.checked_mul(*ad)) {
911                    return lhs.cmp(&rhs);
912                }
913                // Overflow: fall back to BigRational comparison
914                let big_a = BigRational::new(BigInt::from(*an), BigInt::from(*ad));
915                let big_b = BigRational::new(BigInt::from(*bn), BigInt::from(*bd));
916                big_a.cmp(&big_b)
917            }
918            (a, b) => {
919                let big_a = a.to_big_rational();
920                let big_b = b.to_big_rational();
921                big_a.cmp(&big_b)
922            }
923        }
924    }
925}
926
927// ---------------------------------------------------------------------------
928// Trait: Hash (consistent with Eq)
929// ---------------------------------------------------------------------------
930
931impl Hash for FastRational {
932    fn hash<H: Hasher>(&self, state: &mut H) {
933        // Both Small and Big must hash the same value for equal rationals.
934        // Since Small is always in lowest terms with den > 0, we can hash (num, den) directly
935        // for Small. For Big, we normalize and hash the same way.
936        match self {
937            FastRational::Small { num, den } => {
938                num.hash(state);
939                den.hash(state);
940            }
941            FastRational::Big(b) => {
942                // Normalize the BigRational and try to fit in i64
943                let reduced = b.reduced();
944                let n_opt: Option<i64> = reduced.numer().try_into().ok();
945                let d_opt: Option<i64> = reduced.denom().try_into().ok();
946                match (n_opt, d_opt) {
947                    (Some(n), Some(d)) if d > 0 => {
948                        n.hash(state);
949                        d.hash(state);
950                    }
951                    (Some(n), Some(d)) if d < 0 => {
952                        // Normalize sign
953                        (-n).hash(state);
954                        (-d).hash(state);
955                    }
956                    _ => {
957                        // Can't fit in i64; hash the big values directly
958                        // Ensure den > 0 normalization
959                        let (n, d) = if reduced.denom().is_negative() {
960                            (-reduced.numer(), -reduced.denom())
961                        } else {
962                            (reduced.numer().clone(), reduced.denom().clone())
963                        };
964                        n.hash(state);
965                        d.hash(state);
966                    }
967                }
968            }
969        }
970    }
971}
972
973// ---------------------------------------------------------------------------
974// Trait: Display
975// ---------------------------------------------------------------------------
976
977impl fmt::Display for FastRational {
978    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
979        match self {
980            FastRational::Small { num, den } => {
981                if *den == 1 {
982                    write!(f, "{}", num)
983                } else {
984                    write!(f, "{}/{}", num, den)
985                }
986            }
987            FastRational::Big(b) => write!(f, "{}", b),
988        }
989    }
990}
991
992// ---------------------------------------------------------------------------
993// Convenience: from_integer
994// ---------------------------------------------------------------------------
995
996impl FastRational {
997    /// Create a `FastRational` from an integer.
998    #[inline]
999    pub fn from_integer(n: BigInt) -> Self {
1000        FastRational::from_bigint(&n)
1001    }
1002}
1003
1004// ---------------------------------------------------------------------------
1005// Max / Min (used by simplex for clamping)
1006// ---------------------------------------------------------------------------
1007
1008impl FastRational {
1009    /// Returns the larger of self and other.
1010    #[inline]
1011    pub fn max(self, other: Self) -> Self {
1012        if self >= other { self } else { other }
1013    }
1014
1015    /// Returns the smaller of self and other.
1016    #[inline]
1017    pub fn min(self, other: Self) -> Self {
1018        if self <= other { self } else { other }
1019    }
1020}
1021
1022// ---------------------------------------------------------------------------
1023// Tests
1024// ---------------------------------------------------------------------------
1025
1026#[cfg(test)]
1027mod tests {
1028    use super::*;
1029    use std::collections::hash_map::DefaultHasher;
1030
1031    fn small(n: i64, d: i64) -> FastRational {
1032        FastRational::new_small(n, d)
1033    }
1034
1035    fn fr(n: i64) -> FastRational {
1036        FastRational::from(n)
1037    }
1038
1039    fn hash_of(val: &FastRational) -> u64 {
1040        let mut h = DefaultHasher::new();
1041        val.hash(&mut h);
1042        h.finish()
1043    }
1044
1045    // -- Construction and normalization --
1046
1047    #[test]
1048    fn test_new_small_normalization() {
1049        // 2/4 -> 1/2
1050        let r = small(2, 4);
1051        assert_eq!(r, small(1, 2));
1052    }
1053
1054    #[test]
1055    fn test_new_small_negative_den() {
1056        // 3/(-5) -> -3/5
1057        let r = small(3, -5);
1058        assert_eq!(r, small(-3, 5));
1059    }
1060
1061    #[test]
1062    fn test_new_small_zero() {
1063        let r = small(0, 42);
1064        assert_eq!(r, FastRational::zero());
1065    }
1066
1067    #[test]
1068    fn test_from_i64() {
1069        let r = FastRational::from(7i64);
1070        assert_eq!(r, small(7, 1));
1071    }
1072
1073    #[test]
1074    fn test_from_tuple() {
1075        let r = FastRational::from((6i64, 4i64));
1076        assert_eq!(r, small(3, 2));
1077    }
1078
1079    #[test]
1080    fn test_from_bigrational() {
1081        let br = BigRational::new(BigInt::from(10), BigInt::from(4));
1082        let r = FastRational::from(br);
1083        assert_eq!(r, small(5, 2));
1084    }
1085
1086    // -- Arithmetic --
1087
1088    #[test]
1089    fn test_add() {
1090        // 1/2 + 1/3 = 5/6
1091        let a = small(1, 2);
1092        let b = small(1, 3);
1093        assert_eq!(&a + &b, small(5, 6));
1094    }
1095
1096    #[test]
1097    fn test_sub() {
1098        // 3/4 - 1/4 = 1/2
1099        let a = small(3, 4);
1100        let b = small(1, 4);
1101        assert_eq!(&a - &b, small(1, 2));
1102    }
1103
1104    #[test]
1105    fn test_mul() {
1106        // 2/3 * 3/4 = 1/2
1107        let a = small(2, 3);
1108        let b = small(3, 4);
1109        assert_eq!(&a * &b, small(1, 2));
1110    }
1111
1112    #[test]
1113    fn test_div() {
1114        // (2/3) / (4/5) = 10/12 = 5/6
1115        let a = small(2, 3);
1116        let b = small(4, 5);
1117        assert_eq!(&a / &b, small(5, 6));
1118    }
1119
1120    #[test]
1121    fn test_neg() {
1122        let r = small(3, 5);
1123        assert_eq!(-r, small(-3, 5));
1124    }
1125
1126    #[test]
1127    fn test_add_assign() {
1128        let mut a = small(1, 2);
1129        a += small(1, 3);
1130        assert_eq!(a, small(5, 6));
1131    }
1132
1133    #[test]
1134    fn test_mul_assign() {
1135        let mut a = small(2, 3);
1136        a *= small(3, 4);
1137        assert_eq!(a, small(1, 2));
1138    }
1139
1140    // -- Overflow -> Big fallback --
1141
1142    #[test]
1143    fn test_overflow_to_big() {
1144        let big_val = i64::MAX;
1145        let a = fr(big_val);
1146        let b = fr(big_val);
1147        let result = &a + &b;
1148        // Should not panic, should promote to Big
1149        let expected = BigRational::from_integer(BigInt::from(big_val))
1150            + BigRational::from_integer(BigInt::from(big_val));
1151        assert_eq!(result.to_big_rational(), expected);
1152    }
1153
1154    // -- Comparison --
1155
1156    #[test]
1157    fn test_ord() {
1158        assert!(small(1, 3) < small(1, 2));
1159        assert!(small(-1, 2) < small(1, 2));
1160        assert!(small(0, 1) == FastRational::zero());
1161    }
1162
1163    #[test]
1164    fn test_eq_cross_representation() {
1165        // Small(1/2) vs Big(1/2) should be equal
1166        let s = small(1, 2);
1167        let b = FastRational::Big(Box::new(BigRational::new(BigInt::from(1), BigInt::from(2))));
1168        assert_eq!(s, b);
1169    }
1170
1171    // -- Hash consistency --
1172
1173    #[test]
1174    fn test_hash_consistency() {
1175        let s = small(1, 2);
1176        let b = FastRational::Big(Box::new(BigRational::new(BigInt::from(1), BigInt::from(2))));
1177        assert_eq!(hash_of(&s), hash_of(&b));
1178    }
1179
1180    // -- Signed, Zero, One --
1181
1182    #[test]
1183    fn test_zero_one() {
1184        assert!(FastRational::zero().is_zero());
1185        assert!(FastRational::one().is_one());
1186        assert!(!FastRational::zero().is_one());
1187        assert!(!FastRational::one().is_zero());
1188    }
1189
1190    #[test]
1191    fn test_signed() {
1192        assert!(small(3, 5).is_positive());
1193        assert!(small(-3, 5).is_negative());
1194        assert!(!FastRational::zero().is_positive());
1195        assert!(!FastRational::zero().is_negative());
1196    }
1197
1198    #[test]
1199    fn test_abs() {
1200        assert_eq!(small(-3, 5).abs(), small(3, 5));
1201        assert_eq!(small(3, 5).abs(), small(3, 5));
1202    }
1203
1204    // -- Misc methods --
1205
1206    #[test]
1207    fn test_recip() {
1208        assert_eq!(small(3, 5).recip(), Some(small(5, 3)));
1209        assert_eq!(FastRational::zero().recip(), None);
1210    }
1211
1212    #[test]
1213    fn test_floor_ceil() {
1214        // 7/3 = 2.333...
1215        let r = small(7, 3);
1216        assert_eq!(r.floor(), BigInt::from(2));
1217        assert_eq!(r.ceil(), BigInt::from(3));
1218
1219        // -7/3 = -2.333...
1220        let r = small(-7, 3);
1221        assert_eq!(r.floor(), BigInt::from(-3));
1222        assert_eq!(r.ceil(), BigInt::from(-2));
1223    }
1224
1225    #[test]
1226    fn test_ceil_i64_max_boundary_no_overflow() {
1227        // Regression test for MATH-6: the naive `(num + den - 1) / den`
1228        // formula overflows i64 when num is close to i64::MAX. `new_small`
1229        // reduces to lowest terms via gcd, so use a numerator/denominator
1230        // pair that stays in reduced form near i64::MAX (i64::MAX is odd,
1231        // so gcd(i64::MAX, 2) == 1 and the value survives reduction as
1232        // exactly `Small { num: i64::MAX, den: 2 }`, the case cited by the
1233        // finding).
1234        let r = FastRational::new_small(i64::MAX, 2);
1235        let big = r.to_big_rational();
1236        assert_eq!(r.ceil(), crate::rational::ceil(&big));
1237        assert_eq!(r.floor(), crate::rational::floor(&big));
1238        // Sanity: ceil == floor + 1 since i64::MAX/2 is non-integer.
1239        assert_eq!(r.ceil(), r.floor() + BigInt::from(1));
1240    }
1241
1242    #[test]
1243    fn test_ceil_i64_min_boundary() {
1244        // Negative boundary: i64::MIN / 2 is exact (no rounding needed),
1245        // must not overflow or panic.
1246        let r = FastRational::new_small(i64::MIN, 2);
1247        assert_eq!(r.ceil(), BigInt::from(i64::MIN / 2));
1248        assert_eq!(r.floor(), BigInt::from(i64::MIN / 2));
1249    }
1250
1251    #[test]
1252    fn test_ceil_large_positive_non_integer() {
1253        // Large odd numerator over a larger denominator, near the i64
1254        // boundary, to exercise the div_ceil path without triggering the
1255        // `den == 1` fast path.
1256        let r = FastRational::new_small(i64::MAX - 1, 4);
1257        let ceil = r.ceil();
1258        let floor = r.floor();
1259        // ceil - floor is 0 (exact) or 1 (fractional); verify consistency
1260        // via BigRational cross-check rather than duplicating arithmetic.
1261        let big = r.to_big_rational();
1262        let expected_floor = crate::rational::floor(&big);
1263        let expected_ceil = crate::rational::ceil(&big);
1264        assert_eq!(floor, expected_floor);
1265        assert_eq!(ceil, expected_ceil);
1266    }
1267
1268    #[test]
1269    fn test_to_f64() {
1270        let r = small(1, 2);
1271        assert!((r.to_f64() - 0.5).abs() < 1e-10);
1272    }
1273
1274    #[test]
1275    fn test_to_big_rational_roundtrip() {
1276        let r = small(7, 13);
1277        let big = r.to_big_rational();
1278        let back = FastRational::from_big(big);
1279        assert_eq!(r, back);
1280    }
1281
1282    #[test]
1283    fn test_numer_denom() {
1284        let r = small(3, 7);
1285        assert_eq!(r.numer(), BigInt::from(3));
1286        assert_eq!(r.denom(), BigInt::from(7));
1287    }
1288
1289    #[test]
1290    fn test_is_integer() {
1291        assert!(fr(5).is_integer());
1292        assert!(!small(5, 3).is_integer());
1293    }
1294
1295    #[test]
1296    fn test_display() {
1297        assert_eq!(format!("{}", fr(5)), "5");
1298        assert_eq!(format!("{}", small(3, 7)), "3/7");
1299        assert_eq!(format!("{}", small(-1, 2)), "-1/2");
1300    }
1301
1302    #[test]
1303    fn test_max_min() {
1304        let a = small(1, 3);
1305        let b = small(1, 2);
1306        assert_eq!(a.clone().max(b.clone()), b);
1307        assert_eq!(a.clone().min(b.clone()), a);
1308    }
1309
1310    // -- Regression tests: i64::MIN must never be pre-corrupted by
1311    //    `saturating_abs` before reaching `gcd_i64` (which already handles
1312    //    it exactly via `unsigned_abs`). See audit finding on
1313    //    `mul_small`/`new_small`.
1314
1315    #[test]
1316    fn test_new_small_i64_min_odd_denominator_stays_irreducible() {
1317        // gcd(2^63, 7) == 1: i64::MIN/7 is already in lowest terms and must
1318        // be returned unchanged, not silently reduced to an integer.
1319        let r = FastRational::new_small(i64::MIN, 7);
1320        assert_eq!(
1321            r,
1322            FastRational::Small {
1323                num: i64::MIN,
1324                den: 7
1325            }
1326        );
1327        // Round-trip through BigRational must agree exactly.
1328        let expected = BigRational::new(BigInt::from(i64::MIN), BigInt::from(7));
1329        assert_eq!(r.to_big_rational(), expected);
1330    }
1331
1332    #[test]
1333    fn test_new_small_i64_min_even_denominator_reduces_correctly() {
1334        // gcd(2^63, 2) == 2: must reduce to (MIN/2)/1, preserving the
1335        // Small invariant `gcd(|num|, den) == 1`.
1336        let r = FastRational::new_small(i64::MIN, 2);
1337        assert_eq!(
1338            r,
1339            FastRational::Small {
1340                num: i64::MIN / 2,
1341                den: 1
1342            }
1343        );
1344        let expected = BigRational::new(BigInt::from(i64::MIN), BigInt::from(2));
1345        assert_eq!(r.to_big_rational(), expected);
1346    }
1347
1348    #[test]
1349    fn test_mul_small_i64_min_by_small_fraction() {
1350        // Regression for the audit finding: mul_small(MIN, 1, 1, 7) used to
1351        // return -1317624576693539401 (an *integer*, silently wrong by a
1352        // factor of 1/7) because `saturating_abs(i64::MIN) == i64::MAX`
1353        // corrupted the cross-reduction gcd against a denominator of 7
1354        // (which happens to divide i64::MAX exactly).
1355        let a = FastRational::Small {
1356            num: i64::MIN,
1357            den: 1,
1358        };
1359        let b = FastRational::Small { num: 1, den: 7 };
1360        let result = &a * &b;
1361
1362        let expected = BigRational::new(BigInt::from(i64::MIN), BigInt::from(7));
1363        assert_eq!(
1364            result.to_big_rational(),
1365            expected,
1366            "MIN * (1/7) must equal MIN/7 exactly, not truncate to an integer"
1367        );
1368        // The result must not be integral (MIN is not a multiple of 7).
1369        assert!(!result.is_integer());
1370    }
1371
1372    #[test]
1373    fn test_mul_small_i64_min_eq_hash_invariant_preserved() {
1374        // The lowest-terms invariant must hold after multiplication
1375        // involving i64::MIN, so Eq/Hash stay consistent with equal values
1376        // produced via a different path (Big).
1377        let a = FastRational::Small {
1378            num: i64::MIN,
1379            den: 1,
1380        };
1381        let b = FastRational::Small { num: 1, den: 7 };
1382        let small_result = &a * &b;
1383
1384        let big_a = BigRational::from_integer(BigInt::from(i64::MIN));
1385        let big_b = BigRational::new(BigInt::one(), BigInt::from(7));
1386        let big_result = FastRational::from_big(big_a * big_b);
1387
1388        assert_eq!(small_result, big_result);
1389        assert_eq!(hash_of(&small_result), hash_of(&big_result));
1390    }
1391
1392    #[test]
1393    fn test_from_str_radix() {
1394        use num_traits::Num;
1395        let r = FastRational::from_str_radix("3/7", 10);
1396        assert!(r.is_ok());
1397        assert_eq!(r.ok(), Some(small(3, 7)));
1398
1399        let r2 = FastRational::from_str_radix("42", 10);
1400        assert!(r2.is_ok());
1401        assert_eq!(r2.ok(), Some(fr(42)));
1402    }
1403}