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