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        let g = gcd_i64(n.saturating_abs(), d);
115        if g == 0 {
116            return FastRational::Small { num: 0, den: 1 };
117        }
118        FastRational::Small {
119            num: n / g,
120            den: d / g,
121        }
122    }
123
124    /// Create a `FastRational` from a `BigRational`, demoting to `Small` if it fits.
125    pub fn from_big(br: BigRational) -> Self {
126        // Try to fit into i64/i64
127        let n_opt: Option<i64> = br.numer().try_into().ok();
128        let d_opt: Option<i64> = br.denom().try_into().ok();
129        match (n_opt, d_opt) {
130            (Some(n), Some(d)) if d != 0 => FastRational::new_small(n, d),
131            _ => FastRational::Big(Box::new(br)),
132        }
133    }
134
135    /// Convert to `BigRational`.
136    pub fn to_big_rational(&self) -> BigRational {
137        match self {
138            FastRational::Small { num, den } => {
139                BigRational::new(BigInt::from(*num), BigInt::from(*den))
140            }
141            FastRational::Big(b) => (**b).clone(),
142        }
143    }
144
145    /// Convert to `f64` (lossy).
146    #[inline]
147    pub fn to_f64(&self) -> f64 {
148        match self {
149            FastRational::Small { num, den } => *num as f64 / *den as f64,
150            FastRational::Big(b) => {
151                use num_traits::ToPrimitive;
152                b.numer().to_f64().unwrap_or(f64::NAN) / b.denom().to_f64().unwrap_or(f64::NAN)
153            }
154        }
155    }
156
157    /// Return the reciprocal (1/self). Returns `None` if self is zero.
158    pub fn recip(&self) -> Option<Self> {
159        match self {
160            FastRational::Small { num, den } => {
161                if *num == 0 {
162                    None
163                } else {
164                    Some(FastRational::new_small(*den, *num))
165                }
166            }
167            FastRational::Big(b) => {
168                if b.is_zero() {
169                    None
170                } else {
171                    Some(FastRational::from_big(b.recip()))
172                }
173            }
174        }
175    }
176
177    /// Compute the floor (greatest integer <= self).
178    pub fn floor(&self) -> BigInt {
179        match self {
180            FastRational::Small { num, den } => {
181                if *den == 1 {
182                    BigInt::from(*num)
183                } else {
184                    let q = num.div_floor(den);
185                    BigInt::from(q)
186                }
187            }
188            FastRational::Big(b) => crate::rational::floor(b),
189        }
190    }
191
192    /// Compute the ceiling (smallest integer >= self).
193    pub fn ceil(&self) -> BigInt {
194        match self {
195            FastRational::Small { num, den } => {
196                if *den == 1 {
197                    BigInt::from(*num)
198                } else {
199                    // ceil(n/d) for d > 0:
200                    //   positive: (n + d - 1) / d
201                    //   negative non-integer: n / d (Rust truncation rounds toward zero = ceiling)
202                    //   negative exact: n / d
203                    let q = if *num >= 0 {
204                        (*num + *den - 1) / *den
205                    } else {
206                        // Rust's truncation division rounds toward zero for negatives,
207                        // which is the ceiling for negative rationals.
208                        *num / *den
209                    };
210                    BigInt::from(q)
211                }
212            }
213            FastRational::Big(b) => crate::rational::ceil(b),
214        }
215    }
216
217    /// Return the absolute value.
218    pub fn abs(&self) -> Self {
219        match self {
220            FastRational::Small { num, den } => {
221                match num.checked_abs() {
222                    Some(n) => FastRational::Small { num: n, den: *den },
223                    None => {
224                        // num == i64::MIN
225                        let big = BigRational::new(BigInt::from(*num).abs(), BigInt::from(*den));
226                        FastRational::from_big(big)
227                    }
228                }
229            }
230            FastRational::Big(b) => FastRational::from_big(b.abs()),
231        }
232    }
233
234    /// Return the numerator as a `BigInt`.
235    pub fn numer(&self) -> BigInt {
236        match self {
237            FastRational::Small { num, .. } => BigInt::from(*num),
238            FastRational::Big(b) => b.numer().clone(),
239        }
240    }
241
242    /// Return the denominator as a `BigInt`.
243    pub fn denom(&self) -> BigInt {
244        match self {
245            FastRational::Small { den, .. } => BigInt::from(*den),
246            FastRational::Big(b) => b.denom().clone(),
247        }
248    }
249
250    /// Check if this rational is an integer (denominator == 1).
251    #[inline]
252    pub fn is_integer(&self) -> bool {
253        match self {
254            FastRational::Small { den, .. } => *den == 1,
255            FastRational::Big(b) => b.is_integer(),
256        }
257    }
258
259    /// Create a `FastRational` from a `BigRational` reference.
260    pub fn from_big_ref(br: &BigRational) -> Self {
261        let n_opt: Option<i64> = br.numer().try_into().ok();
262        let d_opt: Option<i64> = br.denom().try_into().ok();
263        match (n_opt, d_opt) {
264            (Some(n), Some(d)) if d != 0 => FastRational::new_small(n, d),
265            _ => FastRational::Big(Box::new(br.clone())),
266        }
267    }
268
269    /// Create a `FastRational` representing an integer from a `BigInt`.
270    pub fn from_bigint(bi: &BigInt) -> Self {
271        let n_opt: Option<i64> = bi.try_into().ok();
272        match n_opt {
273            Some(n) => FastRational::Small { num: n, den: 1 },
274            None => FastRational::Big(Box::new(BigRational::from_integer(bi.clone()))),
275        }
276    }
277}
278
279// ---------------------------------------------------------------------------
280// Small-path arithmetic (inlined for performance)
281// ---------------------------------------------------------------------------
282
283/// Add two small rationals, falling back to Big on overflow.
284#[inline]
285fn add_small(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> FastRational {
286    // a/b + c/d = (a*d + c*b) / (b*d) -- but check overflow at each step
287    if let (Some(ad), Some(cb), Some(bd)) = (
288        a_num.checked_mul(b_den),
289        b_num.checked_mul(a_den),
290        a_den.checked_mul(b_den),
291    ) && let Some(num) = ad.checked_add(cb)
292    {
293        return FastRational::new_small(num, bd);
294    }
295    // Overflow: fall back to Big
296    let a = BigRational::new(BigInt::from(a_num), BigInt::from(a_den));
297    let b = BigRational::new(BigInt::from(b_num), BigInt::from(b_den));
298    FastRational::from_big(a + b)
299}
300
301/// Subtract two small rationals, falling back to Big on overflow.
302#[inline]
303fn sub_small(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> FastRational {
304    // a/b - c/d = (a*d - c*b) / (b*d)
305    if let (Some(ad), Some(cb), Some(bd)) = (
306        a_num.checked_mul(b_den),
307        b_num.checked_mul(a_den),
308        a_den.checked_mul(b_den),
309    ) && let Some(num) = ad.checked_sub(cb)
310    {
311        return FastRational::new_small(num, bd);
312    }
313    let a = BigRational::new(BigInt::from(a_num), BigInt::from(a_den));
314    let b = BigRational::new(BigInt::from(b_num), BigInt::from(b_den));
315    FastRational::from_big(a - b)
316}
317
318/// Multiply two small rationals, falling back to Big on overflow.
319#[inline]
320fn mul_small(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> FastRational {
321    // (a/b) * (c/d) = (a*c) / (b*d)
322    // Cross-reduce first to minimize overflow chance
323    let g1 = gcd_i64(a_num.saturating_abs(), b_den);
324    let g2 = gcd_i64(b_num.saturating_abs(), a_den);
325    let an = if g1 != 0 { a_num / g1 } else { a_num };
326    let bd = if g1 != 0 { b_den / g1 } else { b_den };
327    let bn = if g2 != 0 { b_num / g2 } else { b_num };
328    let ad = if g2 != 0 { a_den / g2 } else { a_den };
329
330    if let (Some(num), Some(den)) = (an.checked_mul(bn), ad.checked_mul(bd)) {
331        return FastRational::new_small(num, den);
332    }
333    let a = BigRational::new(BigInt::from(a_num), BigInt::from(a_den));
334    let b = BigRational::new(BigInt::from(b_num), BigInt::from(b_den));
335    FastRational::from_big(a * b)
336}
337
338/// Divide two small rationals, falling back to Big on overflow.
339/// Returns `None` if divisor is zero.
340#[inline]
341fn div_small(a_num: i64, a_den: i64, b_num: i64, b_den: i64) -> Option<FastRational> {
342    if b_num == 0 {
343        return None;
344    }
345    // (a/b) / (c/d) = (a*d) / (b*c)
346    // We need new_small to normalize the sign, so just pass through directly.
347    // Use checked operations to detect overflow.
348    if let (Some(num), Some(den)) = (a_num.checked_mul(b_den), a_den.checked_mul(b_num)) {
349        Some(FastRational::new_small(num, den))
350    } else {
351        let a = BigRational::new(BigInt::from(a_num), BigInt::from(a_den));
352        let b = BigRational::new(BigInt::from(b_num), BigInt::from(b_den));
353        Some(FastRational::from_big(a / b))
354    }
355}
356
357// ---------------------------------------------------------------------------
358// Trait: From conversions
359// ---------------------------------------------------------------------------
360
361impl From<i64> for FastRational {
362    #[inline]
363    fn from(n: i64) -> Self {
364        FastRational::Small { num: n, den: 1 }
365    }
366}
367
368impl From<(i64, i64)> for FastRational {
369    fn from((n, d): (i64, i64)) -> Self {
370        if d == 0 {
371            // Return zero as a safe default for zero denominator
372            FastRational::Small { num: 0, den: 1 }
373        } else {
374            FastRational::new_small(n, d)
375        }
376    }
377}
378
379impl From<BigRational> for FastRational {
380    fn from(br: BigRational) -> Self {
381        FastRational::from_big(br)
382    }
383}
384
385impl From<BigInt> for FastRational {
386    fn from(bi: BigInt) -> Self {
387        FastRational::from_bigint(&bi)
388    }
389}
390
391impl From<&BigRational> for FastRational {
392    fn from(br: &BigRational) -> Self {
393        FastRational::from_big_ref(br)
394    }
395}
396
397// ---------------------------------------------------------------------------
398// Trait: Zero, One
399// ---------------------------------------------------------------------------
400
401impl Zero for FastRational {
402    #[inline]
403    fn zero() -> Self {
404        FastRational::Small { num: 0, den: 1 }
405    }
406
407    #[inline]
408    fn is_zero(&self) -> bool {
409        match self {
410            FastRational::Small { num, .. } => *num == 0,
411            FastRational::Big(b) => b.is_zero(),
412        }
413    }
414}
415
416impl One for FastRational {
417    #[inline]
418    fn one() -> Self {
419        FastRational::Small { num: 1, den: 1 }
420    }
421
422    #[inline]
423    fn is_one(&self) -> bool {
424        match self {
425            FastRational::Small { num, den } => *num == 1 && *den == 1,
426            FastRational::Big(b) => b.is_one(),
427        }
428    }
429}
430
431// ---------------------------------------------------------------------------
432// Trait: Signed
433// ---------------------------------------------------------------------------
434
435impl Signed for FastRational {
436    fn abs(&self) -> Self {
437        FastRational::abs(self)
438    }
439
440    fn abs_sub(&self, other: &Self) -> Self {
441        let diff = self - other;
442        if diff.is_positive() {
443            diff
444        } else {
445            FastRational::zero()
446        }
447    }
448
449    fn signum(&self) -> Self {
450        if self.is_zero() {
451            FastRational::zero()
452        } else if self.is_positive() {
453            FastRational::one()
454        } else {
455            FastRational::Small { num: -1, den: 1 }
456        }
457    }
458
459    fn is_positive(&self) -> bool {
460        match self {
461            FastRational::Small { num, .. } => *num > 0,
462            FastRational::Big(b) => b.is_positive(),
463        }
464    }
465
466    fn is_negative(&self) -> bool {
467        match self {
468            FastRational::Small { num, .. } => *num < 0,
469            FastRational::Big(b) => b.is_negative(),
470        }
471    }
472}
473
474impl num_traits::Num for FastRational {
475    type FromStrRadixErr = String;
476
477    fn from_str_radix(str: &str, radix: u32) -> Result<Self, Self::FromStrRadixErr> {
478        // Parse as BigRational, then try to demote
479        if let Some((num_str, den_str)) = str.split_once('/') {
480            let num = BigInt::from_str_radix(num_str.trim(), radix)
481                .map_err(|e| format!("invalid numerator: {}", e))?;
482            let den = BigInt::from_str_radix(den_str.trim(), radix)
483                .map_err(|e| format!("invalid denominator: {}", e))?;
484            if den.is_zero() {
485                return Err("denominator is zero".to_string());
486            }
487            Ok(FastRational::from_big(BigRational::new(num, den)))
488        } else {
489            let num = BigInt::from_str_radix(str.trim(), radix)
490                .map_err(|e| format!("invalid integer: {}", e))?;
491            Ok(FastRational::from_bigint(&num))
492        }
493    }
494}
495
496// ---------------------------------------------------------------------------
497// Trait: Neg
498// ---------------------------------------------------------------------------
499
500impl Neg for FastRational {
501    type Output = FastRational;
502
503    #[inline]
504    fn neg(self) -> Self::Output {
505        match self {
506            FastRational::Small { num, den } => match num.checked_neg() {
507                Some(n) => FastRational::Small { num: n, den },
508                None => {
509                    let big = BigRational::new(-BigInt::from(num), BigInt::from(den));
510                    FastRational::Big(Box::new(big))
511                }
512            },
513            FastRational::Big(b) => FastRational::from_big(-*b),
514        }
515    }
516}
517
518impl Neg for &FastRational {
519    type Output = FastRational;
520
521    #[inline]
522    fn neg(self) -> Self::Output {
523        match self {
524            FastRational::Small { num, den } => match num.checked_neg() {
525                Some(n) => FastRational::Small { num: n, den: *den },
526                None => {
527                    let big = BigRational::new(-BigInt::from(*num), BigInt::from(*den));
528                    FastRational::Big(Box::new(big))
529                }
530            },
531            FastRational::Big(b) => FastRational::from_big(-((**b).clone())),
532        }
533    }
534}
535
536// ---------------------------------------------------------------------------
537// Trait: Add
538// ---------------------------------------------------------------------------
539
540impl Add for FastRational {
541    type Output = FastRational;
542
543    #[inline]
544    fn add(self, rhs: FastRational) -> Self::Output {
545        (&self).add(&rhs)
546    }
547}
548
549impl Add<&FastRational> for FastRational {
550    type Output = FastRational;
551
552    #[inline]
553    fn add(self, rhs: &FastRational) -> Self::Output {
554        (&self).add(rhs)
555    }
556}
557
558impl Add<FastRational> for &FastRational {
559    type Output = FastRational;
560
561    #[inline]
562    fn add(self, rhs: FastRational) -> Self::Output {
563        self.add(&rhs)
564    }
565}
566
567impl Add<&FastRational> for &FastRational {
568    type Output = FastRational;
569
570    #[inline]
571    fn add(self, rhs: &FastRational) -> Self::Output {
572        match (self, rhs) {
573            (
574                FastRational::Small { num: an, den: ad },
575                FastRational::Small { num: bn, den: bd },
576            ) => add_small(*an, *ad, *bn, *bd),
577            (a, b) => {
578                let big_a = a.to_big_rational();
579                let big_b = b.to_big_rational();
580                FastRational::from_big(big_a + big_b)
581            }
582        }
583    }
584}
585
586impl AddAssign for FastRational {
587    #[inline]
588    fn add_assign(&mut self, rhs: FastRational) {
589        *self = (&*self) + &rhs;
590    }
591}
592
593impl AddAssign<&FastRational> for FastRational {
594    #[inline]
595    fn add_assign(&mut self, rhs: &FastRational) {
596        *self = (&*self) + rhs;
597    }
598}
599
600// ---------------------------------------------------------------------------
601// Trait: Sub
602// ---------------------------------------------------------------------------
603
604impl Sub for FastRational {
605    type Output = FastRational;
606
607    #[inline]
608    fn sub(self, rhs: FastRational) -> Self::Output {
609        (&self).sub(&rhs)
610    }
611}
612
613impl Sub<&FastRational> for FastRational {
614    type Output = FastRational;
615
616    #[inline]
617    fn sub(self, rhs: &FastRational) -> Self::Output {
618        (&self).sub(rhs)
619    }
620}
621
622impl Sub<FastRational> for &FastRational {
623    type Output = FastRational;
624
625    #[inline]
626    fn sub(self, rhs: FastRational) -> Self::Output {
627        self.sub(&rhs)
628    }
629}
630
631impl Sub<&FastRational> for &FastRational {
632    type Output = FastRational;
633
634    #[inline]
635    fn sub(self, rhs: &FastRational) -> Self::Output {
636        match (self, rhs) {
637            (
638                FastRational::Small { num: an, den: ad },
639                FastRational::Small { num: bn, den: bd },
640            ) => sub_small(*an, *ad, *bn, *bd),
641            (a, b) => {
642                let big_a = a.to_big_rational();
643                let big_b = b.to_big_rational();
644                FastRational::from_big(big_a - big_b)
645            }
646        }
647    }
648}
649
650impl SubAssign for FastRational {
651    #[inline]
652    fn sub_assign(&mut self, rhs: FastRational) {
653        *self = (&*self) - &rhs;
654    }
655}
656
657impl SubAssign<&FastRational> for FastRational {
658    #[inline]
659    fn sub_assign(&mut self, rhs: &FastRational) {
660        *self = (&*self) - rhs;
661    }
662}
663
664// ---------------------------------------------------------------------------
665// Trait: Mul
666// ---------------------------------------------------------------------------
667
668impl Mul for FastRational {
669    type Output = FastRational;
670
671    #[inline]
672    fn mul(self, rhs: FastRational) -> Self::Output {
673        (&self).mul(&rhs)
674    }
675}
676
677impl Mul<&FastRational> for FastRational {
678    type Output = FastRational;
679
680    #[inline]
681    fn mul(self, rhs: &FastRational) -> Self::Output {
682        (&self).mul(rhs)
683    }
684}
685
686impl Mul<FastRational> for &FastRational {
687    type Output = FastRational;
688
689    #[inline]
690    fn mul(self, rhs: FastRational) -> Self::Output {
691        self.mul(&rhs)
692    }
693}
694
695impl Mul<&FastRational> for &FastRational {
696    type Output = FastRational;
697
698    #[inline]
699    fn mul(self, rhs: &FastRational) -> Self::Output {
700        match (self, rhs) {
701            (
702                FastRational::Small { num: an, den: ad },
703                FastRational::Small { num: bn, den: bd },
704            ) => mul_small(*an, *ad, *bn, *bd),
705            (a, b) => {
706                let big_a = a.to_big_rational();
707                let big_b = b.to_big_rational();
708                FastRational::from_big(big_a * big_b)
709            }
710        }
711    }
712}
713
714impl MulAssign for FastRational {
715    #[inline]
716    fn mul_assign(&mut self, rhs: FastRational) {
717        *self = (&*self) * &rhs;
718    }
719}
720
721impl MulAssign<&FastRational> for FastRational {
722    #[inline]
723    fn mul_assign(&mut self, rhs: &FastRational) {
724        *self = (&*self) * rhs;
725    }
726}
727
728// ---------------------------------------------------------------------------
729// Trait: Div
730// ---------------------------------------------------------------------------
731
732impl Div for FastRational {
733    type Output = FastRational;
734
735    /// Divide two `FastRational` values.
736    ///
737    /// # Panics
738    ///
739    /// Panics if the divisor is zero.
740    #[inline]
741    fn div(self, rhs: FastRational) -> Self::Output {
742        (&self).div(&rhs)
743    }
744}
745
746impl Div<&FastRational> for FastRational {
747    type Output = FastRational;
748
749    #[inline]
750    fn div(self, rhs: &FastRational) -> Self::Output {
751        (&self).div(rhs)
752    }
753}
754
755impl Div<FastRational> for &FastRational {
756    type Output = FastRational;
757
758    #[inline]
759    fn div(self, rhs: FastRational) -> Self::Output {
760        self.div(&rhs)
761    }
762}
763
764impl Div<&FastRational> for &FastRational {
765    type Output = FastRational;
766
767    #[inline]
768    fn div(self, rhs: &FastRational) -> Self::Output {
769        match (self, rhs) {
770            (
771                FastRational::Small { num: an, den: ad },
772                FastRational::Small { num: bn, den: bd },
773            ) => match div_small(*an, *ad, *bn, *bd) {
774                Some(r) => r,
775                None => {
776                    debug_assert!(false, "FastRational: division by zero");
777                    FastRational::zero()
778                }
779            },
780            (a, b) => {
781                if b.is_zero() {
782                    debug_assert!(false, "FastRational: division by zero");
783                    return FastRational::zero();
784                }
785                let big_a = a.to_big_rational();
786                let big_b = b.to_big_rational();
787                FastRational::from_big(big_a / big_b)
788            }
789        }
790    }
791}
792
793impl DivAssign for FastRational {
794    #[inline]
795    fn div_assign(&mut self, rhs: FastRational) {
796        *self = (&*self) / &rhs;
797    }
798}
799
800impl DivAssign<&FastRational> for FastRational {
801    #[inline]
802    fn div_assign(&mut self, rhs: &FastRational) {
803        *self = (&*self) / rhs;
804    }
805}
806
807// ---------------------------------------------------------------------------
808// Trait: Rem (required by Num)
809// ---------------------------------------------------------------------------
810
811impl Rem for FastRational {
812    type Output = FastRational;
813
814    /// Remainder for rationals: `a % b = a - b * floor(a/b)`.
815    ///
816    /// For non-zero `b`, this returns a value in `[0, |b|)`.
817    #[inline]
818    fn rem(self, rhs: FastRational) -> Self::Output {
819        (&self).rem(&rhs)
820    }
821}
822
823impl Rem<&FastRational> for &FastRational {
824    type Output = FastRational;
825
826    #[inline]
827    fn rem(self, rhs: &FastRational) -> Self::Output {
828        if rhs.is_zero() {
829            return FastRational::zero();
830        }
831        let quotient = self / rhs;
832        let floor_q = FastRational::from_integer(quotient.floor());
833        self - &(rhs * &floor_q)
834    }
835}
836
837impl RemAssign for FastRational {
838    #[inline]
839    fn rem_assign(&mut self, rhs: FastRational) {
840        *self = (&*self).rem(&rhs);
841    }
842}
843
844// ---------------------------------------------------------------------------
845// Trait: PartialEq, Eq
846// ---------------------------------------------------------------------------
847
848impl PartialEq for FastRational {
849    fn eq(&self, other: &Self) -> bool {
850        match (self, other) {
851            (
852                FastRational::Small { num: an, den: ad },
853                FastRational::Small { num: bn, den: bd },
854            ) => {
855                // Both normalized: direct comparison
856                an == bn && ad == bd
857            }
858            (a, b) => {
859                // Cross-representation comparison
860                a.to_big_rational() == b.to_big_rational()
861            }
862        }
863    }
864}
865
866impl Eq for FastRational {}
867
868// ---------------------------------------------------------------------------
869// Trait: PartialOrd, Ord
870// ---------------------------------------------------------------------------
871
872impl PartialOrd for FastRational {
873    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
874        Some(self.cmp(other))
875    }
876}
877
878impl Ord for FastRational {
879    fn cmp(&self, other: &Self) -> Ordering {
880        match (self, other) {
881            (
882                FastRational::Small { num: an, den: ad },
883                FastRational::Small { num: bn, den: bd },
884            ) => {
885                // Compare a_num/a_den vs b_num/b_den
886                // = a_num * b_den vs b_num * a_den (both dens positive)
887                // Use checked_mul to avoid overflow
888                if let (Some(lhs), Some(rhs)) = (an.checked_mul(*bd), bn.checked_mul(*ad)) {
889                    return lhs.cmp(&rhs);
890                }
891                // Overflow: fall back to BigRational comparison
892                let big_a = BigRational::new(BigInt::from(*an), BigInt::from(*ad));
893                let big_b = BigRational::new(BigInt::from(*bn), BigInt::from(*bd));
894                big_a.cmp(&big_b)
895            }
896            (a, b) => {
897                let big_a = a.to_big_rational();
898                let big_b = b.to_big_rational();
899                big_a.cmp(&big_b)
900            }
901        }
902    }
903}
904
905// ---------------------------------------------------------------------------
906// Trait: Hash (consistent with Eq)
907// ---------------------------------------------------------------------------
908
909impl Hash for FastRational {
910    fn hash<H: Hasher>(&self, state: &mut H) {
911        // Both Small and Big must hash the same value for equal rationals.
912        // Since Small is always in lowest terms with den > 0, we can hash (num, den) directly
913        // for Small. For Big, we normalize and hash the same way.
914        match self {
915            FastRational::Small { num, den } => {
916                num.hash(state);
917                den.hash(state);
918            }
919            FastRational::Big(b) => {
920                // Normalize the BigRational and try to fit in i64
921                let reduced = b.reduced();
922                let n_opt: Option<i64> = reduced.numer().try_into().ok();
923                let d_opt: Option<i64> = reduced.denom().try_into().ok();
924                match (n_opt, d_opt) {
925                    (Some(n), Some(d)) if d > 0 => {
926                        n.hash(state);
927                        d.hash(state);
928                    }
929                    (Some(n), Some(d)) if d < 0 => {
930                        // Normalize sign
931                        (-n).hash(state);
932                        (-d).hash(state);
933                    }
934                    _ => {
935                        // Can't fit in i64; hash the big values directly
936                        // Ensure den > 0 normalization
937                        let (n, d) = if reduced.denom().is_negative() {
938                            (-reduced.numer(), -reduced.denom())
939                        } else {
940                            (reduced.numer().clone(), reduced.denom().clone())
941                        };
942                        n.hash(state);
943                        d.hash(state);
944                    }
945                }
946            }
947        }
948    }
949}
950
951// ---------------------------------------------------------------------------
952// Trait: Display
953// ---------------------------------------------------------------------------
954
955impl fmt::Display for FastRational {
956    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
957        match self {
958            FastRational::Small { num, den } => {
959                if *den == 1 {
960                    write!(f, "{}", num)
961                } else {
962                    write!(f, "{}/{}", num, den)
963                }
964            }
965            FastRational::Big(b) => write!(f, "{}", b),
966        }
967    }
968}
969
970// ---------------------------------------------------------------------------
971// Convenience: from_integer
972// ---------------------------------------------------------------------------
973
974impl FastRational {
975    /// Create a `FastRational` from an integer.
976    #[inline]
977    pub fn from_integer(n: BigInt) -> Self {
978        FastRational::from_bigint(&n)
979    }
980}
981
982// ---------------------------------------------------------------------------
983// Max / Min (used by simplex for clamping)
984// ---------------------------------------------------------------------------
985
986impl FastRational {
987    /// Returns the larger of self and other.
988    #[inline]
989    pub fn max(self, other: Self) -> Self {
990        if self >= other { self } else { other }
991    }
992
993    /// Returns the smaller of self and other.
994    #[inline]
995    pub fn min(self, other: Self) -> Self {
996        if self <= other { self } else { other }
997    }
998}
999
1000// ---------------------------------------------------------------------------
1001// Tests
1002// ---------------------------------------------------------------------------
1003
1004#[cfg(test)]
1005mod tests {
1006    use super::*;
1007    use std::collections::hash_map::DefaultHasher;
1008
1009    fn small(n: i64, d: i64) -> FastRational {
1010        FastRational::new_small(n, d)
1011    }
1012
1013    fn fr(n: i64) -> FastRational {
1014        FastRational::from(n)
1015    }
1016
1017    fn hash_of(val: &FastRational) -> u64 {
1018        let mut h = DefaultHasher::new();
1019        val.hash(&mut h);
1020        h.finish()
1021    }
1022
1023    // -- Construction and normalization --
1024
1025    #[test]
1026    fn test_new_small_normalization() {
1027        // 2/4 -> 1/2
1028        let r = small(2, 4);
1029        assert_eq!(r, small(1, 2));
1030    }
1031
1032    #[test]
1033    fn test_new_small_negative_den() {
1034        // 3/(-5) -> -3/5
1035        let r = small(3, -5);
1036        assert_eq!(r, small(-3, 5));
1037    }
1038
1039    #[test]
1040    fn test_new_small_zero() {
1041        let r = small(0, 42);
1042        assert_eq!(r, FastRational::zero());
1043    }
1044
1045    #[test]
1046    fn test_from_i64() {
1047        let r = FastRational::from(7i64);
1048        assert_eq!(r, small(7, 1));
1049    }
1050
1051    #[test]
1052    fn test_from_tuple() {
1053        let r = FastRational::from((6i64, 4i64));
1054        assert_eq!(r, small(3, 2));
1055    }
1056
1057    #[test]
1058    fn test_from_bigrational() {
1059        let br = BigRational::new(BigInt::from(10), BigInt::from(4));
1060        let r = FastRational::from(br);
1061        assert_eq!(r, small(5, 2));
1062    }
1063
1064    // -- Arithmetic --
1065
1066    #[test]
1067    fn test_add() {
1068        // 1/2 + 1/3 = 5/6
1069        let a = small(1, 2);
1070        let b = small(1, 3);
1071        assert_eq!(&a + &b, small(5, 6));
1072    }
1073
1074    #[test]
1075    fn test_sub() {
1076        // 3/4 - 1/4 = 1/2
1077        let a = small(3, 4);
1078        let b = small(1, 4);
1079        assert_eq!(&a - &b, small(1, 2));
1080    }
1081
1082    #[test]
1083    fn test_mul() {
1084        // 2/3 * 3/4 = 1/2
1085        let a = small(2, 3);
1086        let b = small(3, 4);
1087        assert_eq!(&a * &b, small(1, 2));
1088    }
1089
1090    #[test]
1091    fn test_div() {
1092        // (2/3) / (4/5) = 10/12 = 5/6
1093        let a = small(2, 3);
1094        let b = small(4, 5);
1095        assert_eq!(&a / &b, small(5, 6));
1096    }
1097
1098    #[test]
1099    fn test_neg() {
1100        let r = small(3, 5);
1101        assert_eq!(-r, small(-3, 5));
1102    }
1103
1104    #[test]
1105    fn test_add_assign() {
1106        let mut a = small(1, 2);
1107        a += small(1, 3);
1108        assert_eq!(a, small(5, 6));
1109    }
1110
1111    #[test]
1112    fn test_mul_assign() {
1113        let mut a = small(2, 3);
1114        a *= small(3, 4);
1115        assert_eq!(a, small(1, 2));
1116    }
1117
1118    // -- Overflow -> Big fallback --
1119
1120    #[test]
1121    fn test_overflow_to_big() {
1122        let big_val = i64::MAX;
1123        let a = fr(big_val);
1124        let b = fr(big_val);
1125        let result = &a + &b;
1126        // Should not panic, should promote to Big
1127        let expected = BigRational::from_integer(BigInt::from(big_val))
1128            + BigRational::from_integer(BigInt::from(big_val));
1129        assert_eq!(result.to_big_rational(), expected);
1130    }
1131
1132    // -- Comparison --
1133
1134    #[test]
1135    fn test_ord() {
1136        assert!(small(1, 3) < small(1, 2));
1137        assert!(small(-1, 2) < small(1, 2));
1138        assert!(small(0, 1) == FastRational::zero());
1139    }
1140
1141    #[test]
1142    fn test_eq_cross_representation() {
1143        // Small(1/2) vs Big(1/2) should be equal
1144        let s = small(1, 2);
1145        let b = FastRational::Big(Box::new(BigRational::new(BigInt::from(1), BigInt::from(2))));
1146        assert_eq!(s, b);
1147    }
1148
1149    // -- Hash consistency --
1150
1151    #[test]
1152    fn test_hash_consistency() {
1153        let s = small(1, 2);
1154        let b = FastRational::Big(Box::new(BigRational::new(BigInt::from(1), BigInt::from(2))));
1155        assert_eq!(hash_of(&s), hash_of(&b));
1156    }
1157
1158    // -- Signed, Zero, One --
1159
1160    #[test]
1161    fn test_zero_one() {
1162        assert!(FastRational::zero().is_zero());
1163        assert!(FastRational::one().is_one());
1164        assert!(!FastRational::zero().is_one());
1165        assert!(!FastRational::one().is_zero());
1166    }
1167
1168    #[test]
1169    fn test_signed() {
1170        assert!(small(3, 5).is_positive());
1171        assert!(small(-3, 5).is_negative());
1172        assert!(!FastRational::zero().is_positive());
1173        assert!(!FastRational::zero().is_negative());
1174    }
1175
1176    #[test]
1177    fn test_abs() {
1178        assert_eq!(small(-3, 5).abs(), small(3, 5));
1179        assert_eq!(small(3, 5).abs(), small(3, 5));
1180    }
1181
1182    // -- Misc methods --
1183
1184    #[test]
1185    fn test_recip() {
1186        assert_eq!(small(3, 5).recip(), Some(small(5, 3)));
1187        assert_eq!(FastRational::zero().recip(), None);
1188    }
1189
1190    #[test]
1191    fn test_floor_ceil() {
1192        // 7/3 = 2.333...
1193        let r = small(7, 3);
1194        assert_eq!(r.floor(), BigInt::from(2));
1195        assert_eq!(r.ceil(), BigInt::from(3));
1196
1197        // -7/3 = -2.333...
1198        let r = small(-7, 3);
1199        assert_eq!(r.floor(), BigInt::from(-3));
1200        assert_eq!(r.ceil(), BigInt::from(-2));
1201    }
1202
1203    #[test]
1204    fn test_to_f64() {
1205        let r = small(1, 2);
1206        assert!((r.to_f64() - 0.5).abs() < 1e-10);
1207    }
1208
1209    #[test]
1210    fn test_to_big_rational_roundtrip() {
1211        let r = small(7, 13);
1212        let big = r.to_big_rational();
1213        let back = FastRational::from_big(big);
1214        assert_eq!(r, back);
1215    }
1216
1217    #[test]
1218    fn test_numer_denom() {
1219        let r = small(3, 7);
1220        assert_eq!(r.numer(), BigInt::from(3));
1221        assert_eq!(r.denom(), BigInt::from(7));
1222    }
1223
1224    #[test]
1225    fn test_is_integer() {
1226        assert!(fr(5).is_integer());
1227        assert!(!small(5, 3).is_integer());
1228    }
1229
1230    #[test]
1231    fn test_display() {
1232        assert_eq!(format!("{}", fr(5)), "5");
1233        assert_eq!(format!("{}", small(3, 7)), "3/7");
1234        assert_eq!(format!("{}", small(-1, 2)), "-1/2");
1235    }
1236
1237    #[test]
1238    fn test_max_min() {
1239        let a = small(1, 3);
1240        let b = small(1, 2);
1241        assert_eq!(a.clone().max(b.clone()), b);
1242        assert_eq!(a.clone().min(b.clone()), a);
1243    }
1244
1245    #[test]
1246    fn test_from_str_radix() {
1247        use num_traits::Num;
1248        let r = FastRational::from_str_radix("3/7", 10);
1249        assert!(r.is_ok());
1250        assert_eq!(r.ok(), Some(small(3, 7)));
1251
1252        let r2 = FastRational::from_str_radix("42", 10);
1253        assert!(r2.is_ok());
1254        assert_eq!(r2.ok(), Some(fr(42)));
1255    }
1256}