Skip to main content

lemma/computation/
rational.rs

1//! Exact rational arithmetic with an inline i128 fast path.
2//!
3//! Small numerator/denominator pairs (both fit `i128`) use zero-allocation
4//! native arithmetic. On overflow the value promotes to heap-allocated
5//! [`BigInt`]. All constructors produce reduced form; callers never see
6//! unreduced fractions.
7//!
8//! Input literals parse through [`crate::literals::NumberLiteral`] (`Decimal::from_str`).
9//! API decimal conversion uses [`RationalInteger::try_to_decimal`], enforcing the same
10//! [`Decimal::MAX_SCALE`] digit limit.
11
12use rust_decimal::Decimal;
13use rust_decimal::MathematicalOps;
14use std::fmt;
15use std::str::FromStr;
16use std::sync::LazyLock;
17
18use crate::computation::bigint::AllocError;
19pub(crate) use crate::computation::bigint::{BigInt, BigUint};
20use crate::limits::MAX_RATIONAL_BITS;
21
22/// Digit limit for ℚ → [`Decimal`] API conversion.
23///
24/// Matches [`Decimal::MAX_SCALE`] and [`crate::literals::NumberLiteral`] input parsing.
25const DECIMAL_OUTPUT_DIGIT_LIMIT: usize = Decimal::MAX_SCALE as usize;
26
27static DECIMAL_MAX_RATIONAL: LazyLock<RationalInteger> =
28    LazyLock::new(|| decimal_to_rational_unchecked(Decimal::MAX.normalize()));
29
30static DECIMAL_MIN_RATIONAL: LazyLock<RationalInteger> =
31    LazyLock::new(|| decimal_to_rational_unchecked(Decimal::MIN.normalize()));
32
33fn decimal_to_rational_unchecked(decimal: Decimal) -> RationalInteger {
34    decimal_to_rational(decimal).expect("BUG: Decimal::MAX/MIN must convert to rational")
35}
36
37// ---------------------------------------------------------------------------
38// Small-integer GCD (binary GCD on u128, no allocation)
39// ---------------------------------------------------------------------------
40
41fn gcd_u128(mut a: u128, mut b: u128) -> u128 {
42    if a == 0 {
43        return b;
44    }
45    if b == 0 {
46        return a;
47    }
48    let shift = (a | b).trailing_zeros();
49    a >>= a.trailing_zeros();
50    loop {
51        b >>= b.trailing_zeros();
52        if a > b {
53            std::mem::swap(&mut a, &mut b);
54        }
55        b -= a;
56        if b == 0 {
57            return a << shift;
58        }
59    }
60}
61
62/// Reduce `(numer, denom)` in-place. `denom` must be > 0 on entry.
63fn reduce_small(numer: i128, denom: i128) -> (i128, i128) {
64    debug_assert!(denom > 0);
65    if numer == 0 {
66        return (0, 1);
67    }
68    let g = i128::try_from(gcd_u128(numer.unsigned_abs(), denom.unsigned_abs()))
69        .expect("BUG: gcd divides the positive i128 denominator");
70    (numer / g, denom / g)
71}
72
73// ---------------------------------------------------------------------------
74// RationalInteger
75// ---------------------------------------------------------------------------
76
77/// Every constructor produces the canonical form (positive denominator, gcd 1,
78/// `Big` only when the pair does not fit `i128`).
79#[derive(Clone, Debug)]
80enum Repr {
81    Small { numer: i128, denom: i128 },
82    Big { numer: BigInt, denom: BigInt },
83}
84
85#[derive(Clone, Debug)]
86pub struct RationalInteger {
87    repr: Repr,
88}
89
90impl RationalInteger {
91    fn to_big_numer(&self) -> BigInt {
92        match &self.repr {
93            Repr::Small { numer, .. } => BigInt::from_i128(*numer),
94            Repr::Big { numer, .. } => numer.clone(),
95        }
96    }
97
98    fn to_big_denom(&self) -> BigInt {
99        match &self.repr {
100            Repr::Small { denom, .. } => BigInt::from_i128(*denom),
101            Repr::Big { denom, .. } => denom.clone(),
102        }
103    }
104
105    /// Promote to Big representation for fallback arithmetic.
106    fn as_big_pair(&self) -> (BigInt, BigInt) {
107        (self.to_big_numer(), self.to_big_denom())
108    }
109}
110
111impl RationalInteger {
112    // -- Construction --
113
114    /// Construct from already-reduced i128 pair. `denom` MUST be > 0 and gcd == 1.
115    fn from_reduced_small(numer: i128, denom: i128) -> Self {
116        debug_assert!(denom > 0);
117        Self {
118            repr: Repr::Small { numer, denom },
119        }
120    }
121
122    /// Construct from i128 pair, reducing.
123    fn from_small(numer: i128, denom: i128) -> Result<Self, NumericFailure> {
124        if denom == 0 {
125            return Err(NumericFailure::DivisionByZero);
126        }
127        let (n, d) = if denom < 0 {
128            match (numer.checked_neg(), denom.checked_neg()) {
129                (Some(n), Some(d)) => (n, d),
130                _ => return Self::reduce_big(BigInt::from_i128(numer), BigInt::from_i128(denom)),
131            }
132        } else {
133            (numer, denom)
134        };
135        let (rn, rd) = reduce_small(n, d);
136        Ok(Self::from_reduced_small(rn, rd))
137    }
138
139    /// Construct without reducing. Callers guarantee `gcd(numer, denom) == 1`; `denom`
140    /// must be positive.
141    fn from_coprime(numer: BigInt, denom: BigInt) -> Self {
142        assert!(
143            denom.is_positive(),
144            "BUG: from_coprime requires a positive denominator, got {denom}"
145        );
146        match (numer.to_i128(), denom.to_i128()) {
147            (Some(n), Some(d)) => Self::from_reduced_small(n, d),
148            _ => Self {
149                repr: Repr::Big { numer, denom },
150            },
151        }
152    }
153
154    /// `(numer, denom)` of `1 / self` with a positive denominator. Reduced input stays reduced.
155    fn try_reciprocal_pair(&self) -> Result<(BigInt, BigInt), NumericFailure> {
156        if self.numer_is_zero() {
157            return Err(NumericFailure::DivisionByZero);
158        }
159        let (numer, denom) = self.as_big_pair();
160        if numer.is_negative() {
161            Ok((denom.try_neg()?, numer.try_neg()?))
162        } else {
163            Ok((denom, numer))
164        }
165    }
166
167    fn try_reciprocal(&self) -> Result<Self, NumericFailure> {
168        let (numer, denom) = self.try_reciprocal_pair()?;
169        Ok(Self::from_coprime(numer, denom))
170    }
171
172    pub fn try_new(numer: BigInt, denom: BigInt) -> Result<Self, NumericFailure> {
173        if denom.is_zero() {
174            return Err(NumericFailure::DivisionByZero);
175        }
176        // Try small path first
177        if let (Some(n), Some(d)) = (numer.to_i128(), denom.to_i128()) {
178            return Self::from_small(n, d);
179        }
180        Self::reduce_big(numer, denom)
181    }
182
183    pub fn from_i64_pair(numer: i64, denom: i64) -> Self {
184        Self::from_small(i128::from(numer), i128::from(denom))
185            .expect("BUG: i64 rational reduce cannot fail")
186    }
187
188    /// Construct from string-parsed BigInts (used by `SerializedFactor` round-trip).
189    pub fn from_bigint_strings(numer_str: &str, denom_str: &str) -> Result<Self, NumericFailure> {
190        let n = BigInt::try_from_str_radix(numer_str, 10).map_err(NumericFailure::from)?;
191        let d = BigInt::try_from_str_radix(denom_str, 10).map_err(NumericFailure::from)?;
192        Self::try_new(n, d)
193    }
194
195    // -- Predicates --
196
197    pub fn is_integer(&self) -> bool {
198        match &self.repr {
199            Repr::Small { denom, .. } => *denom == 1,
200            Repr::Big { denom, .. } => *denom == BigInt::one(),
201        }
202    }
203
204    pub fn numer_is_zero(&self) -> bool {
205        match &self.repr {
206            Repr::Small { numer, .. } => *numer == 0,
207            Repr::Big { numer, .. } => numer.is_zero(),
208        }
209    }
210
211    pub fn numer_is_positive(&self) -> bool {
212        match &self.repr {
213            Repr::Small { numer, .. } => *numer > 0,
214            Repr::Big { numer, .. } => numer.is_positive(),
215        }
216    }
217
218    pub fn numer_is_negative(&self) -> bool {
219        match &self.repr {
220            Repr::Small { numer, .. } => *numer < 0,
221            Repr::Big { numer, .. } => numer.is_negative(),
222        }
223    }
224
225    // -- Conversions out --
226
227    pub fn numer_to_i32(&self) -> Option<i32> {
228        match &self.repr {
229            Repr::Small { numer, .. } => i32::try_from(*numer).ok(),
230            Repr::Big { numer, .. } => numer.to_i32(),
231        }
232    }
233
234    pub fn numer_to_i128(&self) -> Option<i128> {
235        match &self.repr {
236            Repr::Small { numer, .. } => Some(*numer),
237            Repr::Big { numer, .. } => numer.to_i128(),
238        }
239    }
240
241    pub fn numer_to_u8(&self) -> Option<u8> {
242        match &self.repr {
243            Repr::Small { numer, .. } => u8::try_from(*numer).ok(),
244            Repr::Big { numer, .. } => numer.to_i32().and_then(|v| u8::try_from(v).ok()),
245        }
246    }
247
248    pub fn numer_to_usize(&self) -> Option<usize> {
249        match &self.repr {
250            Repr::Small { numer, .. } => usize::try_from(*numer).ok(),
251            Repr::Big { numer, .. } => numer.to_usize(),
252        }
253    }
254
255    pub fn numer_to_string(&self) -> String {
256        match &self.repr {
257            Repr::Small { numer, .. } => numer.to_string(),
258            Repr::Big { numer, .. } => numer.to_string(),
259        }
260    }
261
262    pub fn denom_to_string(&self) -> String {
263        match &self.repr {
264            Repr::Small { denom, .. } => denom.to_string(),
265            Repr::Big { denom, .. } => denom.to_string(),
266        }
267    }
268
269    /// Exact bit length of the numerator's magnitude; 0 for zero.
270    pub fn numer_magnitude_bits(&self) -> u64 {
271        match &self.repr {
272            Repr::Small { numer, .. } => {
273                if *numer == 0 {
274                    0
275                } else {
276                    128 - numer.unsigned_abs().leading_zeros() as u64
277                }
278            }
279            Repr::Big { numer, .. } => numer.magnitude().bits(),
280        }
281    }
282
283    /// Exact bit length of the denominator's magnitude.
284    pub fn denom_magnitude_bits(&self) -> u64 {
285        match &self.repr {
286            Repr::Small { denom, .. } => {
287                if *denom == 0 {
288                    0
289                } else {
290                    128 - denom.unsigned_abs().leading_zeros() as u64
291                }
292            }
293            Repr::Big { denom, .. } => denom.magnitude().bits(),
294        }
295    }
296
297    // -- Reduction --
298
299    pub fn try_reduce_ref(rational: &RationalInteger) -> Result<Self, NumericFailure> {
300        match &rational.repr {
301            Repr::Small { numer, denom } => Self::from_small(*numer, *denom),
302            Repr::Big { numer, denom } => Self::reduce_big(numer.try_clone()?, denom.try_clone()?),
303        }
304    }
305
306    /// Reduce a BigInt pair: positive denominator, gcd 1, shrunk to Small when both fit i128.
307    fn reduce_big(numer: BigInt, denom: BigInt) -> Result<Self, NumericFailure> {
308        if denom.is_zero() {
309            return Err(NumericFailure::DivisionByZero);
310        }
311        let (n, d) = if denom.is_negative() {
312            (numer.try_neg()?, denom.try_neg()?)
313        } else {
314            (numer, denom)
315        };
316        if n.is_zero() {
317            return Ok(Self::from_reduced_small(0, 1));
318        }
319        let gcd = n.try_abs()?.try_gcd(&d)?;
320        let n = n.try_div_trunc(&gcd)?;
321        let d = d.try_div_trunc(&gcd)?;
322        if let (Some(sn), Some(sd)) = (n.to_i128(), d.to_i128()) {
323            Ok(Self::from_reduced_small(sn, sd))
324        } else {
325            Ok(Self {
326                repr: Repr::Big { numer: n, denom: d },
327            })
328        }
329    }
330
331    pub fn try_reduce(self) -> Result<Self, NumericFailure> {
332        Self::try_reduce_ref(&self)
333    }
334
335    // -- Comparison --
336
337    pub fn try_cmp(&self, other: &Self) -> Result<std::cmp::Ordering, NumericFailure> {
338        match (&self.repr, &other.repr) {
339            (
340                Repr::Small {
341                    numer: ln,
342                    denom: ld,
343                },
344                Repr::Small {
345                    numer: rn,
346                    denom: rd,
347                },
348            ) => {
349                // cross-multiply: ln * rd  vs  rn * ld
350                // Use i128 checked to avoid overflow
351                if let (Some(left), Some(right)) = (ln.checked_mul(*rd), rn.checked_mul(*ld)) {
352                    return Ok(left.cmp(&right));
353                }
354                // Fallback to BigInt
355                let left = BigInt::from_i128(*ln).try_mul(&BigInt::from_i128(*rd))?;
356                let right = BigInt::from_i128(*rn).try_mul(&BigInt::from_i128(*ld))?;
357                Ok(left.cmp(&right))
358            }
359            _ => {
360                let left = self.to_big_numer().try_mul(&other.to_big_denom())?;
361                let right = other.to_big_numer().try_mul(&self.to_big_denom())?;
362                Ok(left.cmp(&right))
363            }
364        }
365    }
366
367    // -- Decimal conversion --
368
369    /// Round to [`Decimal::MAX_SCALE`] precision.
370    ///
371    /// Returns [`NumericFailure::Overflow`] only when `|self| > Decimal::MAX`.
372    ///
373    /// All `RationalInteger` values are constructed in reduced form, so no
374    /// re-reduction is needed here.
375    pub fn try_to_decimal(&self) -> Result<Decimal, NumericFailure> {
376        if self.numer_is_zero() {
377            return Ok(Decimal::ZERO);
378        }
379
380        if self.try_cmp(&DECIMAL_MAX_RATIONAL)? == std::cmp::Ordering::Greater {
381            return Err(NumericFailure::Overflow);
382        }
383        if self.try_cmp(&DECIMAL_MIN_RATIONAL)? == std::cmp::Ordering::Less {
384            return Err(NumericFailure::Overflow);
385        }
386
387        // For small integers with denom==1, fast path via i128 → string → Decimal
388        if let Repr::Small { numer, denom: 1 } = &self.repr {
389            return Decimal::from_str(&numer.to_string()).map_err(|_| NumericFailure::Overflow);
390        }
391
392        let negative = self.numer_is_negative();
393        let abs_numer = self.to_big_numer().try_abs()?;
394        let abs_denom = self.to_big_denom().try_abs()?;
395
396        let (int_quotient, mut remainder) = abs_numer.try_div_rem(&abs_denom)?;
397
398        let mut digit_string = if int_quotient.is_zero() {
399            String::from("0")
400        } else {
401            int_quotient.to_string()
402        };
403
404        if !remainder.is_zero() {
405            digit_string.push('.');
406            let integer_significant_digits = if int_quotient.is_zero() {
407                0
408            } else {
409                digit_string.len()
410            };
411
412            if int_quotient.is_zero() {
413                let mut significant_digits = 0usize;
414                while !remainder.is_zero() && significant_digits < DECIMAL_OUTPUT_DIGIT_LIMIT + 1 {
415                    remainder = remainder.try_mul(&BigInt::from_i64(10))?;
416                    let (digit_quotient, new_remainder) = remainder.try_div_rem(&abs_denom)?;
417                    let digit = u8::try_from(
418                        digit_quotient
419                            .to_i32()
420                            .expect("BUG: fractional digit quotient must fit i32"),
421                    )
422                    .expect("BUG: fractional digit must be 0-9");
423                    digit_string.push(char::from(b'0' + digit));
424                    remainder = new_remainder;
425                    if digit != 0 || significant_digits > 0 {
426                        significant_digits += 1;
427                    }
428                }
429            } else {
430                let max_fractional_digits = DECIMAL_OUTPUT_DIGIT_LIMIT
431                    .saturating_sub(integer_significant_digits)
432                    .saturating_add(1);
433                let mut fractional_count = 0usize;
434                while !remainder.is_zero() && fractional_count < max_fractional_digits {
435                    remainder = remainder.try_mul(&BigInt::from_i64(10))?;
436                    let (digit_quotient, new_remainder) = remainder.try_div_rem(&abs_denom)?;
437                    let digit = u8::try_from(
438                        digit_quotient
439                            .to_i32()
440                            .expect("BUG: fractional digit quotient must fit i32"),
441                    )
442                    .expect("BUG: fractional digit must be 0-9");
443                    digit_string.push(char::from(b'0' + digit));
444                    remainder = new_remainder;
445                    fractional_count += 1;
446                }
447            }
448        }
449
450        if negative {
451            digit_string.insert(0, '-');
452        }
453
454        Decimal::from_str(&digit_string).map_err(|_| NumericFailure::Overflow)
455    }
456
457    pub fn try_to_decimal_string(&self) -> Result<String, NumericFailure> {
458        self.try_to_decimal()
459            .map(|decimal| decimal_to_display_str(&decimal))
460    }
461
462    /// Human-readable decimal when `try_to_decimal` succeeds; fraction string on magnitude overflow.
463    pub fn display_str(&self) -> String {
464        match self.try_to_decimal() {
465            Ok(decimal) => decimal_to_display_str(&decimal),
466            Err(NumericFailure::Overflow) => rational_fraction_str(self),
467            Err(NumericFailure::OutOfMemory) => rational_fraction_str(self),
468            Err(NumericFailure::DivisionByZero) => {
469                unreachable!("BUG: display_str on reduced rational with zero denominator")
470            }
471            Err(NumericFailure::Irrational) => {
472                unreachable!("BUG: display_str does not perform irrational operations")
473            }
474            Err(NumericFailure::MagnitudeLimit) => {
475                unreachable!("BUG: try_to_decimal does not apply the arithmetic magnitude limit")
476            }
477        }
478    }
479}
480
481// -- Eq / Hash / Ord --
482
483impl PartialEq for RationalInteger {
484    fn eq(&self, other: &Self) -> bool {
485        match (&self.repr, &other.repr) {
486            (
487                Repr::Small {
488                    numer: ln,
489                    denom: ld,
490                },
491                Repr::Small {
492                    numer: rn,
493                    denom: rd,
494                },
495            ) => ln == rn && ld == rd,
496            _ => {
497                self.to_big_numer() == other.to_big_numer()
498                    && self.to_big_denom() == other.to_big_denom()
499            }
500        }
501    }
502}
503
504impl Eq for RationalInteger {}
505
506impl std::hash::Hash for RationalInteger {
507    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
508        match &self.repr {
509            Repr::Small { numer, denom } => {
510                numer.hash(state);
511                denom.hash(state);
512            }
513            Repr::Big { numer, denom } => {
514                // Normalize to i128 if possible so Small==Big hashing agrees
515                if let (Some(n), Some(d)) = (numer.to_i128(), denom.to_i128()) {
516                    n.hash(state);
517                    d.hash(state);
518                } else {
519                    numer.hash(state);
520                    denom.hash(state);
521                }
522            }
523        }
524    }
525}
526
527impl fmt::Display for RationalInteger {
528    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
529        write!(f, "{}", self.display_str())
530    }
531}
532
533// ---------------------------------------------------------------------------
534// NumericFailure
535// ---------------------------------------------------------------------------
536
537#[derive(Debug, Clone, PartialEq, Eq)]
538pub enum NumericFailure {
539    DivisionByZero,
540    Overflow,
541    OutOfMemory,
542    /// Arithmetic result exceeds [`MAX_RATIONAL_BITS`].
543    MagnitudeLimit,
544    Irrational,
545}
546
547impl fmt::Display for NumericFailure {
548    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
549        match self {
550            NumericFailure::DivisionByZero => {
551                formatter.write_str("Division by zero is not allowed.")
552            }
553            NumericFailure::Overflow => formatter.write_str("The numeric result overflowed."),
554            NumericFailure::OutOfMemory => {
555                formatter.write_str("The calculation ran out of memory.")
556            }
557            NumericFailure::MagnitudeLimit => write!(
558                formatter,
559                "The numeric result exceeds the maximum size of {MAX_RATIONAL_BITS} bits."
560            ),
561            NumericFailure::Irrational => formatter
562                .write_str("The numeric result is irrational and cannot be represented exactly."),
563        }
564    }
565}
566
567impl From<AllocError> for NumericFailure {
568    fn from(_: AllocError) -> Self {
569        NumericFailure::OutOfMemory
570    }
571}
572
573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
574pub enum NumericOperation {
575    Add,
576    Subtract,
577    Multiply,
578    Divide,
579    Modulo,
580    Power,
581}
582
583// ---------------------------------------------------------------------------
584// Free constructors
585// ---------------------------------------------------------------------------
586
587pub fn rational_one() -> RationalInteger {
588    rational_new(1, 1)
589}
590
591pub fn rational_zero() -> RationalInteger {
592    rational_new(0, 1)
593}
594
595pub fn rational_new(numer: i64, denom: i64) -> RationalInteger {
596    RationalInteger::from_i64_pair(numer, denom)
597}
598
599pub fn try_rational_new(numer: BigInt, denom: BigInt) -> Result<RationalInteger, NumericFailure> {
600    RationalInteger::try_new(numer, denom)
601}
602
603pub fn rational_is_zero(rational: &RationalInteger) -> bool {
604    rational.numer_is_zero()
605}
606
607pub fn rational_abs(rational: &RationalInteger) -> Result<RationalInteger, NumericFailure> {
608    match &rational.repr {
609        Repr::Small { numer, denom } => match numer.checked_abs() {
610            Some(n) => Ok(RationalInteger::from_reduced_small(n, *denom)),
611            None => try_rational_new(
612                BigInt::from_i128(*numer).try_abs()?,
613                BigInt::from_i128(*denom),
614            ),
615        },
616        Repr::Big { numer, denom } => {
617            let n = numer.try_abs()?;
618            let d = denom.try_clone()?;
619            Ok(RationalInteger {
620                repr: Repr::Big { numer: n, denom: d },
621            })
622        }
623    }
624}
625
626pub fn rational_trunc(rational: &RationalInteger) -> Result<RationalInteger, NumericFailure> {
627    match &rational.repr {
628        Repr::Small { numer, denom } => Ok(RationalInteger::from_reduced_small(numer / denom, 1)),
629        Repr::Big { numer, denom } => {
630            let truncated = numer.try_div_trunc(denom)?;
631            try_rational_new(truncated, BigInt::one())
632        }
633    }
634}
635
636pub fn rational_floor(rational: &RationalInteger) -> Result<RationalInteger, NumericFailure> {
637    let truncated = rational_trunc(rational)?;
638    if rational.numer_is_negative() && !rational.is_integer() {
639        try_sub(&truncated, &rational_one())
640    } else {
641        Ok(truncated)
642    }
643}
644
645pub fn rational_ceil(rational: &RationalInteger) -> Result<RationalInteger, NumericFailure> {
646    let truncated = rational_trunc(rational)?;
647    if !rational.numer_is_negative() && !rational.is_integer() {
648        try_add(&truncated, &rational_one())
649    } else {
650        Ok(truncated)
651    }
652}
653
654/// Nearest integer. An exact half goes away from zero.
655pub fn rational_round(rational: &RationalInteger) -> Result<RationalInteger, NumericFailure> {
656    let floor = rational_floor(rational)?;
657    let fraction = try_sub(rational, &floor)?;
658    match fraction.try_cmp(&rational_new(1, 2))? {
659        std::cmp::Ordering::Less => Ok(floor),
660        std::cmp::Ordering::Greater => try_add(&floor, &rational_one()),
661        std::cmp::Ordering::Equal => {
662            if rational.numer_is_negative() {
663                Ok(floor)
664            } else {
665                try_add(&floor, &rational_one())
666            }
667        }
668    }
669}
670
671pub fn decimal_to_rational(decimal: Decimal) -> Result<RationalInteger, NumericFailure> {
672    let mantissa = decimal.mantissa();
673    if mantissa == 0 {
674        return Ok(rational_new(0, 1));
675    }
676    let scale = decimal.scale();
677    if scale == 0 {
678        return RationalInteger::from_small(mantissa, 1);
679    }
680    // 10^scale — try i128 first (10^38 fits in i128)
681    if scale <= 38 {
682        if let Some(denom) = 10i128.checked_pow(scale) {
683            return RationalInteger::from_small(mantissa, denom);
684        }
685    }
686    // Fallback to BigInt
687    let mut denominator = BigInt::one();
688    for _ in 0..scale {
689        denominator = denominator
690            .try_mul(&BigInt::from_i64(10))
691            .map_err(NumericFailure::from)?;
692    }
693    try_rational_new(BigInt::from_i128(mantissa), denominator)
694}
695
696pub(crate) fn decimal_to_display_str(decimal: &Decimal) -> String {
697    let normalized = decimal.normalize();
698    if normalized.fract().is_zero() {
699        normalized.trunc().to_string()
700    } else {
701        normalized.to_string()
702    }
703}
704
705fn rational_fraction_str(rational: &RationalInteger) -> String {
706    if rational.is_integer() {
707        rational.numer_to_string()
708    } else {
709        format!(
710            "{}/{}",
711            rational.numer_to_string(),
712            rational.denom_to_string()
713        )
714    }
715}
716
717// ---------------------------------------------------------------------------
718// Arithmetic operations
719// ---------------------------------------------------------------------------
720
721/// Exact arithmetic for Lemma operators. Results beyond [`MAX_RATIONAL_BITS`] fail with
722/// [`NumericFailure::MagnitudeLimit`]; the bare `try_*` helpers are unbounded and reserved
723/// for exact scaling by planned constants.
724pub fn rational_operation(
725    left: &RationalInteger,
726    operation: NumericOperation,
727    right: &RationalInteger,
728) -> Result<RationalInteger, NumericFailure> {
729    let result = rational_operation_unbounded(left, operation, right)?;
730    if result.numer_magnitude_bits() > MAX_RATIONAL_BITS
731        || result.denom_magnitude_bits() > MAX_RATIONAL_BITS
732    {
733        return Err(NumericFailure::MagnitudeLimit);
734    }
735    Ok(result)
736}
737
738fn rational_operation_unbounded(
739    left: &RationalInteger,
740    operation: NumericOperation,
741    right: &RationalInteger,
742) -> Result<RationalInteger, NumericFailure> {
743    match operation {
744        NumericOperation::Add => try_add(left, right),
745        NumericOperation::Subtract => try_sub(left, right),
746        NumericOperation::Multiply => try_mul(left, right),
747        NumericOperation::Divide => {
748            if rational_is_zero(right) {
749                return Err(NumericFailure::DivisionByZero);
750            }
751            try_div(left, right)
752        }
753        NumericOperation::Modulo => {
754            if rational_is_zero(right) {
755                return Err(NumericFailure::DivisionByZero);
756            }
757            let quotient = try_div(left, right)?;
758            let floored = rational_floor(&quotient)?;
759            let product = try_mul(&floored, right)?;
760            try_sub(left, &product)
761        }
762        NumericOperation::Power => try_rational_power(left, right),
763    }
764}
765
766pub fn rational_operation_with_fallback(
767    left: &RationalInteger,
768    operation: NumericOperation,
769    right: &RationalInteger,
770) -> Result<RationalInteger, NumericFailure> {
771    match rational_operation(left, operation, right) {
772        Ok(result) => Ok(result),
773        Err(NumericFailure::DivisionByZero) => Err(NumericFailure::DivisionByZero),
774        Err(NumericFailure::Overflow) => Err(NumericFailure::Overflow),
775        Err(NumericFailure::OutOfMemory) => Err(NumericFailure::OutOfMemory),
776        Err(NumericFailure::MagnitudeLimit) => Err(NumericFailure::MagnitudeLimit),
777        Err(NumericFailure::Irrational) => approximate_rational_operation(left, operation, right),
778    }
779}
780
781fn approximate_rational_operation(
782    left: &RationalInteger,
783    operation: NumericOperation,
784    right: &RationalInteger,
785) -> Result<RationalInteger, NumericFailure> {
786    let left_decimal = left.try_to_decimal()?;
787    let right_decimal = right.try_to_decimal()?;
788    let result_decimal = decimal_arithmetic(left_decimal, operation, right_decimal)?;
789    decimal_to_rational(result_decimal)
790}
791
792fn decimal_arithmetic(
793    left: Decimal,
794    operation: NumericOperation,
795    right: Decimal,
796) -> Result<Decimal, NumericFailure> {
797    match operation {
798        NumericOperation::Add => left.checked_add(right).ok_or(NumericFailure::Overflow),
799        NumericOperation::Subtract => left.checked_sub(right).ok_or(NumericFailure::Overflow),
800        NumericOperation::Multiply => left.checked_mul(right).ok_or(NumericFailure::Overflow),
801        NumericOperation::Divide => {
802            if right.is_zero() {
803                return Err(NumericFailure::DivisionByZero);
804            }
805            left.checked_div(right).ok_or(NumericFailure::Overflow)
806        }
807        NumericOperation::Modulo => {
808            if right.is_zero() {
809                return Err(NumericFailure::DivisionByZero);
810            }
811            let quotient = left.checked_div(right).ok_or(NumericFailure::Overflow)?;
812            let floored = quotient.floor();
813            let product = floored.checked_mul(right).ok_or(NumericFailure::Overflow)?;
814            left.checked_sub(product).ok_or(NumericFailure::Overflow)
815        }
816        NumericOperation::Power => decimal_power(left, right),
817    }
818}
819
820fn decimal_is_half(exponent: Decimal) -> bool {
821    exponent
822        .checked_mul(Decimal::TWO)
823        .is_some_and(|doubled| doubled == Decimal::ONE)
824}
825
826fn decimal_power(base: Decimal, exponent: Decimal) -> Result<Decimal, NumericFailure> {
827    if exponent.fract().is_zero() {
828        let exponent_i64 =
829            i64::try_from(exponent.trunc().mantissa()).map_err(|_| NumericFailure::Overflow)?;
830        return base
831            .checked_powi(exponent_i64)
832            .ok_or(NumericFailure::Overflow);
833    }
834    if decimal_is_half(exponent) {
835        return base.sqrt().ok_or(NumericFailure::Irrational);
836    }
837    Err(NumericFailure::Irrational)
838}
839
840// -- Core four + power ---
841
842pub fn try_add(
843    left: &RationalInteger,
844    right: &RationalInteger,
845) -> Result<RationalInteger, NumericFailure> {
846    if let (
847        Repr::Small {
848            numer: ln,
849            denom: ld,
850        },
851        Repr::Small {
852            numer: rn,
853            denom: rd,
854        },
855    ) = (&left.repr, &right.repr)
856    {
857        // ln/ld + rn/rd = (ln*rd + rn*ld) / (ld*rd)
858        if let (Some(a), Some(b), Some(d)) = (
859            ln.checked_mul(*rd),
860            rn.checked_mul(*ld),
861            ld.checked_mul(*rd),
862        ) {
863            if let Some(n) = a.checked_add(b) {
864                return RationalInteger::from_small(n, d);
865            }
866        }
867    }
868    // BigInt fallback
869    let (ln, ld) = left.as_big_pair();
870    let (rn, rd) = right.as_big_pair();
871    let numerator = ln.try_mul(&rd)?.try_add(&rn.try_mul(&ld)?)?;
872    let denominator = ld.try_mul(&rd)?;
873    try_rational_new(numerator, denominator)
874}
875
876pub fn try_sub(
877    left: &RationalInteger,
878    right: &RationalInteger,
879) -> Result<RationalInteger, NumericFailure> {
880    if let (
881        Repr::Small {
882            numer: ln,
883            denom: ld,
884        },
885        Repr::Small {
886            numer: rn,
887            denom: rd,
888        },
889    ) = (&left.repr, &right.repr)
890    {
891        if let (Some(a), Some(b), Some(d)) = (
892            ln.checked_mul(*rd),
893            rn.checked_mul(*ld),
894            ld.checked_mul(*rd),
895        ) {
896            if let Some(n) = a.checked_sub(b) {
897                return RationalInteger::from_small(n, d);
898            }
899        }
900    }
901    let (ln, ld) = left.as_big_pair();
902    let (rn, rd) = right.as_big_pair();
903    let numerator = ln.try_mul(&rd)?.try_sub(&rn.try_mul(&ld)?)?;
904    let denominator = ld.try_mul(&rd)?;
905    try_rational_new(numerator, denominator)
906}
907
908pub fn try_mul(
909    left: &RationalInteger,
910    right: &RationalInteger,
911) -> Result<RationalInteger, NumericFailure> {
912    if let (
913        Repr::Small {
914            numer: ln,
915            denom: ld,
916        },
917        Repr::Small {
918            numer: rn,
919            denom: rd,
920        },
921    ) = (&left.repr, &right.repr)
922    {
923        if let (Some(n), Some(d)) = (ln.checked_mul(*rn), ld.checked_mul(*rd)) {
924            return RationalInteger::from_small(n, d);
925        }
926    }
927    if left.numer_is_zero() || right.numer_is_zero() {
928        return Ok(rational_zero());
929    }
930    let (ln, ld) = left.as_big_pair();
931    let (rn, rd) = right.as_big_pair();
932    cross_cancelled_product((&ln, &ld), (&rn, &rd))
933}
934
935/// `(ln / ld) * (rn / rd)` for reduced, non-zero operands with positive `ld` and `rd`.
936/// Cancels `gcd(ln, rd)` and `gcd(rn, ld)` first, so the result is reduced by
937/// construction and no gcd runs on the product.
938fn cross_cancelled_product(
939    (ln, ld): (&BigInt, &BigInt),
940    (rn, rd): (&BigInt, &BigInt),
941) -> Result<RationalInteger, NumericFailure> {
942    let left_cross = ln.try_gcd(rd)?;
943    let right_cross = rn.try_gcd(ld)?;
944    let numerator = ln
945        .try_div_trunc(&left_cross)?
946        .try_mul(&rn.try_div_trunc(&right_cross)?)?;
947    let denominator = ld
948        .try_div_trunc(&right_cross)?
949        .try_mul(&rd.try_div_trunc(&left_cross)?)?;
950    Ok(RationalInteger::from_coprime(numerator, denominator))
951}
952
953pub fn try_div(
954    left: &RationalInteger,
955    right: &RationalInteger,
956) -> Result<RationalInteger, NumericFailure> {
957    if right.numer_is_zero() {
958        return Err(NumericFailure::DivisionByZero);
959    }
960    if let (
961        Repr::Small {
962            numer: ln,
963            denom: ld,
964        },
965        Repr::Small {
966            numer: rn,
967            denom: rd,
968        },
969    ) = (&left.repr, &right.repr)
970    {
971        if let (Some(n), Some(d)) = (ln.checked_mul(*rd), ld.checked_mul(*rn)) {
972            return RationalInteger::from_small(n, d);
973        }
974    }
975    if left.numer_is_zero() {
976        return Ok(rational_zero());
977    }
978    let (ln, ld) = left.as_big_pair();
979    let (rn, rd) = right.try_reciprocal_pair()?;
980    cross_cancelled_product((&ln, &ld), (&rn, &rd))
981}
982
983/// Exact `exponent`-th power of `base` for any integer exponent.
984pub fn try_pow_integer(
985    base: &RationalInteger,
986    exponent: &BigInt,
987) -> Result<RationalInteger, NumericFailure> {
988    if exponent.is_zero() {
989        return Ok(rational_one());
990    }
991    if exponent.is_negative() {
992        try_pow_magnitude(&base.try_reciprocal()?, exponent.magnitude())
993    } else {
994        try_pow_magnitude(base, exponent.magnitude())
995    }
996}
997
998/// `gcd(a, b) = 1` implies `gcd(a^e, b^e) = 1`, so the power of a reduced base is
999/// reduced and needs no gcd.
1000fn try_pow_magnitude(
1001    base: &RationalInteger,
1002    exponent: &BigUint,
1003) -> Result<RationalInteger, NumericFailure> {
1004    if let (Repr::Small { numer, denom }, Some(small_exponent)) = (&base.repr, exponent.to_u32()) {
1005        if let (Some(n), Some(d)) = (
1006            numer.checked_pow(small_exponent),
1007            denom.checked_pow(small_exponent),
1008        ) {
1009            return Ok(RationalInteger::from_reduced_small(n, d));
1010        }
1011    }
1012    let (numer, denom) = base.as_big_pair();
1013    Ok(RationalInteger::from_coprime(
1014        numer.try_pow(exponent)?,
1015        denom.try_pow(exponent)?,
1016    ))
1017}
1018
1019pub fn try_rational_power(
1020    base: &RationalInteger,
1021    exponent: &RationalInteger,
1022) -> Result<RationalInteger, NumericFailure> {
1023    if !exponent.numer_is_positive() && !exponent.numer_is_negative() && exponent.numer_is_zero() {
1024        return Ok(rational_one());
1025    }
1026
1027    if exponent.is_integer() {
1028        let integer_exponent = exponent.to_big_numer();
1029        if power_exceeds_magnitude_limit(base, integer_exponent.magnitude()) {
1030            return Err(NumericFailure::MagnitudeLimit);
1031        }
1032        return try_pow_integer(base, &integer_exponent);
1033    }
1034
1035    if base.numer_is_zero() {
1036        if exponent.numer_is_negative() || exponent.numer_is_zero() {
1037            return Err(NumericFailure::DivisionByZero);
1038        }
1039        return Ok(rational_new(0, 1));
1040    }
1041
1042    let root_degree = exponent.to_big_denom().magnitude().clone();
1043    let abs_exponent = exponent.to_big_numer();
1044
1045    let result_negative = base.numer_is_negative() && abs_exponent.magnitude().is_odd();
1046    if result_negative && !root_degree.is_odd() {
1047        return Err(NumericFailure::Irrational);
1048    }
1049
1050    let numer_root = exact_integer_root(&base.to_big_numer().try_abs()?, &root_degree)?;
1051    let denom_root = exact_integer_root(&base.to_big_denom(), &root_degree)?;
1052    let signed_numer_root = if result_negative {
1053        numer_root.try_neg()?
1054    } else {
1055        numer_root
1056    };
1057    let root = RationalInteger::from_coprime(signed_numer_root, denom_root);
1058    if power_exceeds_magnitude_limit(&root, abs_exponent.magnitude()) {
1059        return Err(NumericFailure::MagnitudeLimit);
1060    }
1061    let raised = try_pow_magnitude(&root, abs_exponent.magnitude())?;
1062
1063    if exponent.numer_is_negative() {
1064        raised.try_reciprocal()
1065    } else {
1066        Ok(raised)
1067    }
1068}
1069
1070/// True when `base^exponent` provably has a numerator or denominator above
1071/// [`MAX_RATIONAL_BITS`]: a `b`-bit magnitude with `b >= 2` raised to `e` has at least
1072/// `(b - 1) * e + 1` bits. When false the result has at most twice the limit, so
1073/// computing it before the final check stays bounded.
1074fn power_exceeds_magnitude_limit(base: &RationalInteger, exponent: &BigUint) -> bool {
1075    [base.numer_magnitude_bits(), base.denom_magnitude_bits()]
1076        .into_iter()
1077        .filter(|bits| *bits >= 2)
1078        .any(|bits| match exponent.to_u32() {
1079            Some(exponent) => (bits - 1)
1080                .checked_mul(u64::from(exponent))
1081                .is_none_or(|lower_bound| lower_bound >= MAX_RATIONAL_BITS),
1082            None => true,
1083        })
1084}
1085
1086/// Exact integer `degree`-th root of a positive `value`; `Irrational` when none exists.
1087fn exact_integer_root(value: &BigInt, degree: &BigUint) -> Result<BigInt, NumericFailure> {
1088    let root = value.try_nth_root(degree)?;
1089    if root.try_pow(degree)? == *value {
1090        Ok(root)
1091    } else {
1092        Err(NumericFailure::Irrational)
1093    }
1094}
1095
1096pub use {try_div as checked_div, try_mul as checked_mul, try_pow_integer as checked_pow_integer};
1097
1098// ---------------------------------------------------------------------------
1099// Tests
1100// ---------------------------------------------------------------------------
1101
1102#[cfg(test)]
1103mod tests {
1104    use super::*;
1105    use rust_decimal::Decimal;
1106    use std::str::FromStr;
1107
1108    #[test]
1109    fn power_of_two_with_all_ones_exponent_is_positive() {
1110        let two_pow_127 =
1111            RationalInteger::from_bigint_strings("170141183460469231731687303715884105728", "1")
1112                .unwrap();
1113        assert!(two_pow_127.numer_is_positive(), "parsed 2^127 sign");
1114        let big =
1115            BigInt::try_from_str_radix("170141183460469231731687303715884105728", 10).unwrap();
1116        assert!(big.is_positive(), "BigInt 2^127 sign");
1117        let product = try_mul(&rational_new(1 << 62, 1), &rational_new(1 << 62, 1)).unwrap();
1118        let product = try_mul(&product, &rational_new(8, 1)).unwrap();
1119        assert!(product.numer_is_positive(), "2^62 * 2^62 * 8 sign");
1120        let powered = try_pow_integer(&rational_new(2, 1), &BigInt::from_i64(127)).unwrap();
1121        assert!(powered.numer_is_positive(), "2^127 via power sign");
1122        assert_eq!(powered, two_pow_127);
1123    }
1124
1125    #[test]
1126    fn i128_min_numerator_promotes_instead_of_failing() {
1127        let two_pow_127 =
1128            RationalInteger::from_bigint_strings("170141183460469231731687303715884105728", "1")
1129                .unwrap();
1130        let min = RationalInteger::from_small(i128::MIN, 1).unwrap();
1131        assert_eq!(rational_abs(&min).unwrap(), two_pow_127);
1132        assert_eq!(try_div(&min, &rational_new(-1, 1)).unwrap(), two_pow_127);
1133        let reciprocal = try_div(&rational_new(1, 1), &min).unwrap();
1134        assert_eq!(
1135            reciprocal,
1136            RationalInteger::from_bigint_strings("-1", "170141183460469231731687303715884105728")
1137                .unwrap()
1138        );
1139        assert_eq!(
1140            RationalInteger::from_small(i128::MIN, -2).unwrap(),
1141            RationalInteger::from_bigint_strings("85070591730234615865843651857942052864", "1")
1142                .unwrap()
1143        );
1144        assert_eq!(
1145            RationalInteger::from_small(1, i128::MIN).unwrap(),
1146            RationalInteger::from_bigint_strings("-1", "170141183460469231731687303715884105728")
1147                .unwrap()
1148        );
1149        assert_eq!(
1150            RationalInteger::from_small(i128::MIN, i128::MIN).unwrap(),
1151            rational_one()
1152        );
1153        assert_eq!(
1154            RationalInteger::from_small(3, i128::MIN).unwrap(),
1155            RationalInteger::from_bigint_strings("-3", "170141183460469231731687303715884105728")
1156                .unwrap()
1157        );
1158    }
1159
1160    fn two_pow(exponent: i64) -> RationalInteger {
1161        try_pow_integer(&rational_new(2, 1), &BigInt::from_i64(exponent)).unwrap()
1162    }
1163
1164    fn operate(
1165        left: &RationalInteger,
1166        operation: NumericOperation,
1167        right: &RationalInteger,
1168    ) -> Result<RationalInteger, NumericFailure> {
1169        rational_operation(left, operation, right)
1170    }
1171
1172    fn negate(value: &RationalInteger) -> RationalInteger {
1173        try_sub(&rational_zero(), value).unwrap()
1174    }
1175
1176    const LIMIT: i64 = MAX_RATIONAL_BITS as i64;
1177
1178    #[test]
1179    fn rational_operation_accepts_results_at_the_bit_limit() {
1180        let at_limit = two_pow(LIMIT - 1);
1181        assert_eq!(at_limit.numer_magnitude_bits(), MAX_RATIONAL_BITS);
1182        let half_limit = two_pow(LIMIT / 2 - 1);
1183        assert_eq!(
1184            operate(&half_limit, NumericOperation::Multiply, &two_pow(LIMIT / 2)).unwrap(),
1185            at_limit
1186        );
1187        assert_eq!(
1188            operate(
1189                &two_pow(LIMIT - 2),
1190                NumericOperation::Add,
1191                &two_pow(LIMIT - 2)
1192            )
1193            .unwrap(),
1194            at_limit
1195        );
1196        let reciprocal = operate(&rational_one(), NumericOperation::Divide, &at_limit).unwrap();
1197        assert_eq!(reciprocal.denom_magnitude_bits(), MAX_RATIONAL_BITS);
1198    }
1199
1200    #[test]
1201    fn rational_operation_rejects_numerator_beyond_the_bit_limit() {
1202        let at_limit = two_pow(LIMIT - 1);
1203        let two = rational_new(2, 1);
1204        for (operation, right) in [
1205            (NumericOperation::Add, at_limit.clone()),
1206            (NumericOperation::Subtract, negate(&at_limit)),
1207            (NumericOperation::Multiply, two.clone()),
1208            (NumericOperation::Divide, rational_new(1, 2)),
1209            (NumericOperation::Power, two.clone()),
1210        ] {
1211            assert_eq!(
1212                operate(&at_limit, operation, &right).unwrap_err(),
1213                NumericFailure::MagnitudeLimit,
1214                "{operation:?}"
1215            );
1216        }
1217    }
1218
1219    #[test]
1220    fn rational_operation_rejects_denominator_beyond_the_bit_limit() {
1221        let reciprocal_at_limit = try_div(&rational_one(), &two_pow(LIMIT - 1)).unwrap();
1222        assert_eq!(
1223            operate(
1224                &reciprocal_at_limit,
1225                NumericOperation::Divide,
1226                &rational_new(2, 1)
1227            )
1228            .unwrap_err(),
1229            NumericFailure::MagnitudeLimit
1230        );
1231        assert_eq!(
1232            operate(
1233                &reciprocal_at_limit,
1234                NumericOperation::Multiply,
1235                &rational_new(1, 2)
1236            )
1237            .unwrap_err(),
1238            NumericFailure::MagnitudeLimit
1239        );
1240    }
1241
1242    #[test]
1243    fn power_precheck_is_exact_at_the_bit_limit() {
1244        let two = rational_new(2, 1);
1245        assert_eq!(
1246            operate(&two, NumericOperation::Power, &rational_new(LIMIT - 1, 1)).unwrap(),
1247            two_pow(LIMIT - 1)
1248        );
1249        assert!(!power_exceeds_magnitude_limit(
1250            &two,
1251            &BigUint::try_from_u32(LIMIT as u32 - 1).unwrap()
1252        ));
1253        assert!(power_exceeds_magnitude_limit(
1254            &two,
1255            &BigUint::try_from_u32(LIMIT as u32).unwrap()
1256        ));
1257        let two_pow_sixteen = rational_new(1 << 16, 1);
1258        let largest_fitting_exponent = (LIMIT as u32 - 1) / 16;
1259        assert!(!power_exceeds_magnitude_limit(
1260            &two_pow_sixteen,
1261            &BigUint::try_from_u32(largest_fitting_exponent).unwrap()
1262        ));
1263        assert_eq!(
1264            operate(
1265                &two_pow_sixteen,
1266                NumericOperation::Power,
1267                &rational_new(i64::from(largest_fitting_exponent), 1)
1268            )
1269            .unwrap(),
1270            two_pow(16 * i64::from(largest_fitting_exponent))
1271        );
1272    }
1273
1274    #[test]
1275    fn power_precheck_rejects_huge_exponents_before_allocating() {
1276        let huge = RationalInteger::from_bigint_strings("1180591620717411303424", "1").unwrap();
1277        for base in [rational_new(2, 1), rational_new(1, 2), rational_new(-3, 7)] {
1278            assert_eq!(
1279                operate(&base, NumericOperation::Power, &huge).unwrap_err(),
1280                NumericFailure::MagnitudeLimit,
1281                "{base:?}"
1282            );
1283            assert_eq!(
1284                operate(&base, NumericOperation::Power, &negate(&huge)).unwrap_err(),
1285                NumericFailure::MagnitudeLimit,
1286                "{base:?} negative exponent"
1287            );
1288        }
1289        assert_eq!(
1290            operate(&rational_new(-1, 1), NumericOperation::Power, &huge).unwrap(),
1291            rational_one()
1292        );
1293    }
1294
1295    #[test]
1296    fn power_precheck_applies_to_the_root_of_fractional_exponents() {
1297        let four = rational_new(4, 1);
1298        assert_eq!(
1299            operate(&four, NumericOperation::Power, &rational_new(LIMIT - 1, 2)).unwrap(),
1300            two_pow(LIMIT - 1)
1301        );
1302        assert_eq!(
1303            operate(&four, NumericOperation::Power, &rational_new(LIMIT + 1, 2)).unwrap_err(),
1304            NumericFailure::MagnitudeLimit
1305        );
1306    }
1307
1308    use crate::computation::bigint::work_counters;
1309
1310    fn big(value: i64) -> BigInt {
1311        BigInt::from_i64(value)
1312    }
1313
1314    fn big_pow(base: i64, exponent: u32) -> BigInt {
1315        big(base)
1316            .try_pow(&BigUint::try_from_u32(exponent).unwrap())
1317            .unwrap()
1318    }
1319
1320    fn big_add(left: &BigInt, right: &BigInt) -> BigInt {
1321        left.try_add(right).unwrap()
1322    }
1323
1324    fn fraction(numer: BigInt, denom: BigInt) -> RationalInteger {
1325        try_rational_new(numer, denom).unwrap()
1326    }
1327
1328    fn assert_canonical(value: &RationalInteger) {
1329        match &value.repr {
1330            Repr::Small { numer, denom } => {
1331                assert!(*denom > 0, "{value:?} denominator must be positive");
1332                assert_eq!(
1333                    reduce_small(*numer, *denom),
1334                    (*numer, *denom),
1335                    "{value:?} is not reduced"
1336                );
1337            }
1338            Repr::Big { numer, denom } => {
1339                assert!(
1340                    denom.is_positive(),
1341                    "{value:?} denominator must be positive"
1342                );
1343                assert!(
1344                    numer.to_i128().is_none() || denom.to_i128().is_none(),
1345                    "{value:?} fits i128 and must be Small"
1346                );
1347                let reduced = RationalInteger::try_reduce_ref(value).expect("reducible");
1348                assert!(&reduced == value, "{value:?} is not reduced");
1349            }
1350        }
1351    }
1352
1353    fn reference_mul(left: &RationalInteger, right: &RationalInteger) -> RationalInteger {
1354        let (ln, ld) = left.as_big_pair();
1355        let (rn, rd) = right.as_big_pair();
1356        fraction(ln.try_mul(&rn).unwrap(), ld.try_mul(&rd).unwrap())
1357    }
1358
1359    fn reference_div(left: &RationalInteger, right: &RationalInteger) -> RationalInteger {
1360        let (ln, ld) = left.as_big_pair();
1361        let (rn, rd) = right.as_big_pair();
1362        fraction(ln.try_mul(&rd).unwrap(), ld.try_mul(&rn).unwrap())
1363    }
1364
1365    fn reference_pow(base: &RationalInteger, exponent: i64) -> RationalInteger {
1366        let (n, d) = base.as_big_pair();
1367        let magnitude = BigUint::try_from_u64(exponent.unsigned_abs()).unwrap();
1368        let (n, d) = (
1369            n.try_pow(&magnitude).unwrap(),
1370            d.try_pow(&magnitude).unwrap(),
1371        );
1372        if exponent < 0 {
1373            fraction(d, n)
1374        } else {
1375            fraction(n, d)
1376        }
1377    }
1378
1379    /// `2^32767 - 1` and `2^32767 + 1`: odd, two apart, hence coprime; 32767 and 32768 bits.
1380    fn cap_sized_coprime_pair() -> (BigInt, BigInt) {
1381        let power = big_pow(2, 32_767);
1382        (big_add(&power, &big(-1)), big_add(&power, &big(1)))
1383    }
1384
1385    fn arithmetic_fixtures() -> Vec<RationalInteger> {
1386        vec![
1387            rational_zero(),
1388            rational_one(),
1389            rational_new(-1, 1),
1390            rational_new(3, 7),
1391            rational_new(-22, 15),
1392            RationalInteger::from_small(i128::MAX, 1).unwrap(),
1393            RationalInteger::from_small(i128::MIN, 1).unwrap(),
1394            RationalInteger::from_small(1, i128::MAX).unwrap(),
1395            RationalInteger::from_small(i128::MIN, i128::MAX).unwrap(),
1396            fraction(big_add(&big_pow(2, 200), &big(1)), big_pow(3, 100)),
1397            fraction(big_pow(-5, 91), big_pow(2, 150)),
1398            fraction(big_pow(6, 80), big_pow(35, 40)),
1399            fraction(big_pow(35, 40), big_pow(-6, 81)),
1400            fraction(big_pow(2, 130), big(3)),
1401        ]
1402    }
1403
1404    #[test]
1405    fn mul_div_pow_match_reduction_of_the_raw_result() {
1406        let fixtures = arithmetic_fixtures();
1407        for left in &fixtures {
1408            for right in &fixtures {
1409                let product = try_mul(left, right).unwrap();
1410                assert_canonical(&product);
1411                assert_eq!(product, reference_mul(left, right), "{left:?} * {right:?}");
1412                if right.numer_is_zero() {
1413                    continue;
1414                }
1415                let quotient = try_div(left, right).unwrap();
1416                assert_canonical(&quotient);
1417                assert_eq!(quotient, reference_div(left, right), "{left:?} / {right:?}");
1418            }
1419            for exponent in -3..=3 {
1420                if exponent < 0 && left.numer_is_zero() {
1421                    continue;
1422                }
1423                let power = try_pow_integer(left, &BigInt::from_i64(exponent)).unwrap();
1424                assert_canonical(&power);
1425                assert_eq!(
1426                    power,
1427                    reference_pow(left, exponent),
1428                    "{left:?} ^ {exponent}"
1429                );
1430            }
1431        }
1432    }
1433
1434    #[test]
1435    fn try_div_keeps_denominator_positive_for_every_sign() {
1436        let (a, b) = cap_sized_coprime_pair();
1437        let positive = fraction(a.try_clone().unwrap(), big_pow(3, 50));
1438        let other = fraction(big_pow(7, 60), b);
1439        let negate = |value: &RationalInteger| try_sub(&rational_zero(), value).unwrap();
1440        for (left, right) in [
1441            (positive.clone(), negate(&other)),
1442            (negate(&positive), other.clone()),
1443            (negate(&positive), negate(&other)),
1444            (positive.clone(), other.clone()),
1445        ] {
1446            let quotient = try_div(&left, &right).unwrap();
1447            assert_canonical(&quotient);
1448            assert_eq!(quotient, reference_div(&left, &right));
1449        }
1450        assert_eq!(
1451            try_div(&positive, &negate(&positive)).unwrap(),
1452            rational_new(-1, 1)
1453        );
1454    }
1455
1456    #[test]
1457    fn power_of_a_reduced_fraction_performs_no_gcd() {
1458        let bases = [
1459            rational_new(7301, 7300),
1460            rational_new(-3, 7),
1461            fraction(big_add(&big_pow(2, 200), &big(1)), big_pow(3, 130)),
1462            fraction(big_pow(-5, 91), big_pow(2, 150)),
1463        ];
1464        for base in &bases {
1465            for exponent in [-301, -2, 2, 5, 200] {
1466                work_counters::reset();
1467                let power = try_pow_integer(base, &BigInt::from_i64(exponent)).unwrap();
1468                let counts = work_counters::snapshot();
1469                assert_eq!(counts.gcd_calls, 0, "{base:?} ^ {exponent}");
1470                assert_canonical(&power);
1471                assert_eq!(
1472                    power,
1473                    reference_pow(base, exponent),
1474                    "{base:?} ^ {exponent}"
1475                );
1476            }
1477        }
1478    }
1479
1480    #[test]
1481    fn product_of_reciprocals_takes_gcds_of_the_operands_only() {
1482        let (a, b) = cap_sized_coprime_pair();
1483        let operand_bits = a.bits().max(b.bits());
1484        let left = fraction(a.try_clone().unwrap(), b.try_clone().unwrap());
1485        let right = fraction(b, a);
1486        work_counters::reset();
1487        assert_eq!(try_mul(&left, &right).unwrap(), rational_one());
1488        let counts = work_counters::snapshot();
1489        assert_eq!(counts.gcd_calls, 2);
1490        assert!(counts.gcd_max_operand_bits <= operand_bits);
1491        work_counters::reset();
1492        assert_eq!(try_div(&left, &left).unwrap(), rational_one());
1493        let counts = work_counters::snapshot();
1494        assert_eq!(counts.gcd_calls, 2);
1495        assert!(counts.gcd_max_operand_bits <= operand_bits);
1496    }
1497
1498    #[test]
1499    fn rational_zero_is_zero() {
1500        assert!(rational_is_zero(&rational_zero()));
1501    }
1502
1503    #[test]
1504    fn decimal_one_half_lifts_to_rational() {
1505        let decimal = Decimal::from_str("0.5").unwrap();
1506        let rational = decimal_to_rational(decimal).unwrap();
1507        assert_eq!(rational, rational_new(1, 2));
1508    }
1509
1510    #[test]
1511    fn try_to_decimal_one_third() {
1512        let rational = rational_new(1, 3);
1513        let decimal = rational.try_to_decimal().unwrap();
1514        let expected = Decimal::from_str("0.3333333333333333333333333333").unwrap();
1515        assert_eq!(decimal, expected);
1516    }
1517
1518    #[test]
1519    fn try_to_decimal_tiny_rounds_to_zero() {
1520        let rational =
1521            RationalInteger::from_bigint_strings("1", "1000000000000000000000000000000").unwrap();
1522        assert_eq!(rational.try_to_decimal().unwrap(), Decimal::ZERO);
1523    }
1524
1525    #[test]
1526    fn try_to_decimal_magnitude_beyond_max_returns_overflow() {
1527        let max = Decimal::MAX.normalize();
1528        let max_rational = decimal_to_rational(max).unwrap();
1529        let twice = try_mul(&max_rational, &rational_new(2, 1)).unwrap();
1530        assert_eq!(
1531            twice.try_to_decimal().unwrap_err(),
1532            NumericFailure::Overflow,
1533        );
1534    }
1535
1536    #[test]
1537    fn try_to_decimal_string_integer() {
1538        let rational = rational_new(37, 1);
1539        assert_eq!(rational.try_to_decimal_string().unwrap(), "37");
1540    }
1541
1542    #[test]
1543    fn display_str_shows_decimal_when_try_to_decimal_succeeds() {
1544        let rational = rational_new(355, 113);
1545        let display = rational.display_str();
1546        assert!(
1547            !display.contains('/'),
1548            "rational with successful try_to_decimal must display as decimal, got {display}"
1549        );
1550    }
1551
1552    #[test]
1553    fn try_to_decimal_huge_cancelling_rationals() {
1554        let numer = BigInt::try_from_str_radix("1", 10)
1555            .unwrap()
1556            .try_pow(&BigUint::try_from_u32(100).unwrap())
1557            .unwrap();
1558        let rational = try_rational_new(numer.clone(), numer).unwrap();
1559        assert_eq!(rational.try_to_decimal().unwrap(), Decimal::ONE);
1560    }
1561
1562    #[test]
1563    fn try_mul_integer() {
1564        let left = rational_new(50, 1);
1565        let right = rational_new(86400, 1);
1566        let product = try_mul(&left, &right).unwrap();
1567        assert_eq!(product, rational_new(4_320_000, 1));
1568    }
1569
1570    #[test]
1571    fn try_pow_negative_exponent_inverts_base() {
1572        let hour_factor = rational_new(3600, 1);
1573        let inverse = try_pow_integer(&hour_factor, &BigInt::from_i64(-1)).unwrap();
1574        assert_eq!(inverse, rational_new(1, 3600));
1575    }
1576
1577    #[test]
1578    fn rational_operation_divide_by_zero() {
1579        let left = rational_new(1, 1);
1580        let right = rational_new(0, 1);
1581        let failure = rational_operation(&left, NumericOperation::Divide, &right).unwrap_err();
1582        assert_eq!(failure, NumericFailure::DivisionByZero);
1583    }
1584
1585    #[test]
1586    fn rational_operation_power_irrational() {
1587        let base = rational_new(2, 1);
1588        let exponent = rational_new(1, 2);
1589        let failure = rational_operation(&base, NumericOperation::Power, &exponent).unwrap_err();
1590        assert_eq!(failure, NumericFailure::Irrational);
1591    }
1592
1593    #[test]
1594    fn rational_operation_power_exact() {
1595        let base = rational_new(4, 1);
1596        let exponent = rational_new(1, 2);
1597        let result = rational_operation(&base, NumericOperation::Power, &exponent).unwrap();
1598        assert_eq!(result, rational_new(2, 1));
1599    }
1600
1601    #[test]
1602    fn rational_power_exact_roots_of_rationals() {
1603        let power = |base: RationalInteger, exponent: RationalInteger| {
1604            rational_operation(&base, NumericOperation::Power, &exponent)
1605        };
1606        assert_eq!(
1607            power(rational_new(9, 4), rational_new(1, 2)).unwrap(),
1608            rational_new(3, 2)
1609        );
1610        assert_eq!(
1611            power(rational_new(9, 4), rational_new(-1, 2)).unwrap(),
1612            rational_new(2, 3)
1613        );
1614        assert_eq!(
1615            power(rational_new(8, 1), rational_new(2, 3)).unwrap(),
1616            rational_new(4, 1)
1617        );
1618        assert_eq!(
1619            power(rational_new(-8, 1), rational_new(1, 3)).unwrap(),
1620            rational_new(-2, 1)
1621        );
1622        assert_eq!(
1623            power(rational_new(-4, 1), rational_new(2, 1)).unwrap(),
1624            rational_new(16, 1)
1625        );
1626        assert_eq!(
1627            power(rational_new(1, 1), rational_new(49_595, 100_000)).unwrap(),
1628            rational_new(1, 1)
1629        );
1630    }
1631
1632    #[test]
1633    fn power_with_huge_integer_exponent_of_one_and_minus_one() {
1634        let even_huge =
1635            RationalInteger::from_bigint_strings("1000000000000000000000000000000", "1").unwrap();
1636        let odd_huge =
1637            RationalInteger::from_bigint_strings("1000000000000000000000000000001", "1").unwrap();
1638        assert_eq!(
1639            rational_operation(&rational_new(1, 1), NumericOperation::Power, &even_huge).unwrap(),
1640            rational_new(1, 1)
1641        );
1642        assert_eq!(
1643            rational_operation(&rational_new(-1, 1), NumericOperation::Power, &odd_huge).unwrap(),
1644            rational_new(-1, 1)
1645        );
1646    }
1647
1648    #[test]
1649    fn power_with_huge_root_degree_above_bit_length_is_irrational() {
1650        let degree = RationalInteger::from_bigint_strings("1", "1000000000000").unwrap();
1651        assert_eq!(
1652            rational_operation(&rational_new(2, 1), NumericOperation::Power, &degree).unwrap_err(),
1653            NumericFailure::Irrational
1654        );
1655    }
1656
1657    #[test]
1658    fn power_with_exponent_beyond_every_allocation_is_out_of_memory() {
1659        let exponent = BigInt::from_i128(1i128 << 70);
1660        assert_eq!(
1661            try_pow_integer(&rational_new(2, 1), &exponent).unwrap_err(),
1662            NumericFailure::OutOfMemory
1663        );
1664        assert_eq!(
1665            try_pow_integer(&rational_new(1, 1), &exponent).unwrap(),
1666            rational_new(1, 1)
1667        );
1668    }
1669
1670    #[test]
1671    fn rational_power_irrational_fails_fast_for_large_degree() {
1672        let failure = rational_operation(
1673            &rational_new(480, 1),
1674            NumericOperation::Power,
1675            &rational_new(49_595, 100_000),
1676        )
1677        .unwrap_err();
1678        assert_eq!(failure, NumericFailure::Irrational);
1679        let failure = rational_operation(
1680            &rational_new(2, 1),
1681            NumericOperation::Power,
1682            &rational_new(49_595, 100_000),
1683        )
1684        .unwrap_err();
1685        assert_eq!(failure, NumericFailure::Irrational);
1686    }
1687
1688    #[test]
1689    fn rational_power_negative_base_even_root_is_irrational() {
1690        let failure = rational_operation(
1691            &rational_new(-4, 1),
1692            NumericOperation::Power,
1693            &rational_new(1, 2),
1694        )
1695        .unwrap_err();
1696        assert_eq!(failure, NumericFailure::Irrational);
1697    }
1698
1699    #[test]
1700    fn rational_operation_add() {
1701        let left = rational_new(1, 3);
1702        let right = rational_new(1, 6);
1703        let sum = rational_operation(&left, NumericOperation::Add, &right).unwrap();
1704        assert_eq!(sum, rational_new(1, 2));
1705    }
1706
1707    #[test]
1708    fn rational_operation_with_fallback_add_exact_rational() {
1709        let left = rational_new(1, 3);
1710        let right = rational_new(1, 6);
1711        let sum = rational_operation_with_fallback(&left, NumericOperation::Add, &right).unwrap();
1712        assert_eq!(sum, rational_new(1, 2));
1713    }
1714
1715    #[test]
1716    fn rational_operation_with_fallback_power_sqrt_via_decimal() {
1717        let result = rational_operation_with_fallback(
1718            &rational_new(2, 1),
1719            NumericOperation::Power,
1720            &rational_new(1, 2),
1721        )
1722        .unwrap();
1723        let expected = rational_new(2, 1).try_to_decimal().unwrap().sqrt().unwrap();
1724        assert_eq!(
1725            result.try_to_decimal().unwrap().round_dp(27),
1726            expected.round_dp(27),
1727        );
1728    }
1729
1730    #[test]
1731    fn rational_modulo_is_floored_and_takes_divisor_sign() {
1732        assert_eq!(
1733            operate(
1734                &rational_new(-7, 1),
1735                NumericOperation::Modulo,
1736                &rational_new(3, 1)
1737            )
1738            .unwrap(),
1739            rational_new(2, 1)
1740        );
1741        assert_eq!(
1742            operate(
1743                &rational_new(7, 1),
1744                NumericOperation::Modulo,
1745                &rational_new(-3, 1)
1746            )
1747            .unwrap(),
1748            rational_new(-2, 1)
1749        );
1750        assert_eq!(
1751            operate(
1752                &rational_new(-7, 2),
1753                NumericOperation::Modulo,
1754                &rational_new(1, 1)
1755            )
1756            .unwrap(),
1757            rational_new(1, 2)
1758        );
1759        assert_eq!(
1760            operate(
1761                &rational_new(-7, 1),
1762                NumericOperation::Modulo,
1763                &rational_new(-3, 1)
1764            )
1765            .unwrap(),
1766            rational_new(-1, 1)
1767        );
1768    }
1769
1770    #[test]
1771    fn rational_abs_negates_negative_numerator() {
1772        let negative = rational_new(-172_800, 1);
1773        assert_eq!(rational_abs(&negative).unwrap(), rational_new(172_800, 1));
1774    }
1775
1776    #[test]
1777    fn try_to_decimal_string_rejects_magnitude_overflow() {
1778        let too_large =
1779            RationalInteger::from_bigint_strings("10000000000000000000000000000000", "1").unwrap();
1780        assert_eq!(
1781            too_large.try_to_decimal().unwrap_err(),
1782            NumericFailure::Overflow
1783        );
1784        assert_eq!(
1785            too_large.try_to_decimal_string().unwrap_err(),
1786            NumericFailure::Overflow,
1787        );
1788    }
1789
1790    #[test]
1791    fn decimal_max_times_decimal_max_stays_exact_without_decimal_fallback() {
1792        let max_decimal = Decimal::MAX.normalize();
1793        let left = decimal_to_rational(max_decimal).unwrap();
1794        let right = decimal_to_rational(max_decimal).unwrap();
1795        let product = rational_operation(&left, NumericOperation::Multiply, &right).unwrap();
1796        let expected = try_mul(&left, &right).unwrap();
1797        assert_eq!(product, expected);
1798    }
1799
1800    #[test]
1801    fn alloc_error_maps_to_out_of_memory() {
1802        assert_eq!(
1803            NumericFailure::from(crate::computation::bigint::AllocError),
1804            NumericFailure::OutOfMemory
1805        );
1806    }
1807
1808    #[test]
1809    fn small_path_basic_arithmetic() {
1810        let a = rational_new(3, 1);
1811        let b = rational_new(5, 1);
1812        assert_eq!(try_add(&a, &b).unwrap(), rational_new(8, 1));
1813        assert_eq!(try_sub(&a, &b).unwrap(), rational_new(-2, 1));
1814        assert_eq!(try_mul(&a, &b).unwrap(), rational_new(15, 1));
1815        assert_eq!(try_div(&a, &b).unwrap(), rational_new(3, 5));
1816    }
1817
1818    #[test]
1819    fn small_path_reduces() {
1820        let r = rational_new(6, 4);
1821        assert_eq!(r, rational_new(3, 2));
1822    }
1823
1824    #[test]
1825    fn small_path_try_to_decimal_integer() {
1826        let r = rational_new(42, 1);
1827        assert_eq!(r.try_to_decimal().unwrap(), Decimal::from(42));
1828    }
1829}