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