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    // Real non-integer powers: a^b = exp(b·ln(a)). Reject negative bases; rust_decimal's
838    // checked_powd would flip the sign of |a|^b, which is not real-valued for general b.
839    if base.is_sign_negative() {
840        return Err(NumericFailure::Irrational);
841    }
842    base.checked_powd(exponent).ok_or(NumericFailure::Overflow)
843}
844
845// -- Core four + power ---
846
847pub fn try_add(
848    left: &RationalInteger,
849    right: &RationalInteger,
850) -> Result<RationalInteger, NumericFailure> {
851    if let (
852        Repr::Small {
853            numer: ln,
854            denom: ld,
855        },
856        Repr::Small {
857            numer: rn,
858            denom: rd,
859        },
860    ) = (&left.repr, &right.repr)
861    {
862        // ln/ld + rn/rd = (ln*rd + rn*ld) / (ld*rd)
863        if let (Some(a), Some(b), Some(d)) = (
864            ln.checked_mul(*rd),
865            rn.checked_mul(*ld),
866            ld.checked_mul(*rd),
867        ) {
868            if let Some(n) = a.checked_add(b) {
869                return RationalInteger::from_small(n, d);
870            }
871        }
872    }
873    // BigInt fallback
874    let (ln, ld) = left.as_big_pair();
875    let (rn, rd) = right.as_big_pair();
876    let numerator = ln.try_mul(&rd)?.try_add(&rn.try_mul(&ld)?)?;
877    let denominator = ld.try_mul(&rd)?;
878    try_rational_new(numerator, denominator)
879}
880
881pub fn try_sub(
882    left: &RationalInteger,
883    right: &RationalInteger,
884) -> Result<RationalInteger, NumericFailure> {
885    if let (
886        Repr::Small {
887            numer: ln,
888            denom: ld,
889        },
890        Repr::Small {
891            numer: rn,
892            denom: rd,
893        },
894    ) = (&left.repr, &right.repr)
895    {
896        if let (Some(a), Some(b), Some(d)) = (
897            ln.checked_mul(*rd),
898            rn.checked_mul(*ld),
899            ld.checked_mul(*rd),
900        ) {
901            if let Some(n) = a.checked_sub(b) {
902                return RationalInteger::from_small(n, d);
903            }
904        }
905    }
906    let (ln, ld) = left.as_big_pair();
907    let (rn, rd) = right.as_big_pair();
908    let numerator = ln.try_mul(&rd)?.try_sub(&rn.try_mul(&ld)?)?;
909    let denominator = ld.try_mul(&rd)?;
910    try_rational_new(numerator, denominator)
911}
912
913pub fn try_mul(
914    left: &RationalInteger,
915    right: &RationalInteger,
916) -> Result<RationalInteger, NumericFailure> {
917    if let (
918        Repr::Small {
919            numer: ln,
920            denom: ld,
921        },
922        Repr::Small {
923            numer: rn,
924            denom: rd,
925        },
926    ) = (&left.repr, &right.repr)
927    {
928        if let (Some(n), Some(d)) = (ln.checked_mul(*rn), ld.checked_mul(*rd)) {
929            return RationalInteger::from_small(n, d);
930        }
931    }
932    if left.numer_is_zero() || right.numer_is_zero() {
933        return Ok(rational_zero());
934    }
935    let (ln, ld) = left.as_big_pair();
936    let (rn, rd) = right.as_big_pair();
937    cross_cancelled_product((&ln, &ld), (&rn, &rd))
938}
939
940/// `(ln / ld) * (rn / rd)` for reduced, non-zero operands with positive `ld` and `rd`.
941/// Cancels `gcd(ln, rd)` and `gcd(rn, ld)` first, so the result is reduced by
942/// construction and no gcd runs on the product.
943fn cross_cancelled_product(
944    (ln, ld): (&BigInt, &BigInt),
945    (rn, rd): (&BigInt, &BigInt),
946) -> Result<RationalInteger, NumericFailure> {
947    let left_cross = ln.try_gcd(rd)?;
948    let right_cross = rn.try_gcd(ld)?;
949    let numerator = ln
950        .try_div_trunc(&left_cross)?
951        .try_mul(&rn.try_div_trunc(&right_cross)?)?;
952    let denominator = ld
953        .try_div_trunc(&right_cross)?
954        .try_mul(&rd.try_div_trunc(&left_cross)?)?;
955    Ok(RationalInteger::from_coprime(numerator, denominator))
956}
957
958pub fn try_div(
959    left: &RationalInteger,
960    right: &RationalInteger,
961) -> Result<RationalInteger, NumericFailure> {
962    if right.numer_is_zero() {
963        return Err(NumericFailure::DivisionByZero);
964    }
965    if let (
966        Repr::Small {
967            numer: ln,
968            denom: ld,
969        },
970        Repr::Small {
971            numer: rn,
972            denom: rd,
973        },
974    ) = (&left.repr, &right.repr)
975    {
976        if let (Some(n), Some(d)) = (ln.checked_mul(*rd), ld.checked_mul(*rn)) {
977            return RationalInteger::from_small(n, d);
978        }
979    }
980    if left.numer_is_zero() {
981        return Ok(rational_zero());
982    }
983    let (ln, ld) = left.as_big_pair();
984    let (rn, rd) = right.try_reciprocal_pair()?;
985    cross_cancelled_product((&ln, &ld), (&rn, &rd))
986}
987
988/// Exact `exponent`-th power of `base` for any integer exponent.
989pub fn try_pow_integer(
990    base: &RationalInteger,
991    exponent: &BigInt,
992) -> Result<RationalInteger, NumericFailure> {
993    if exponent.is_zero() {
994        return Ok(rational_one());
995    }
996    if exponent.is_negative() {
997        try_pow_magnitude(&base.try_reciprocal()?, exponent.magnitude())
998    } else {
999        try_pow_magnitude(base, exponent.magnitude())
1000    }
1001}
1002
1003/// `gcd(a, b) = 1` implies `gcd(a^e, b^e) = 1`, so the power of a reduced base is
1004/// reduced and needs no gcd.
1005fn try_pow_magnitude(
1006    base: &RationalInteger,
1007    exponent: &BigUint,
1008) -> Result<RationalInteger, NumericFailure> {
1009    if let (Repr::Small { numer, denom }, Some(small_exponent)) = (&base.repr, exponent.to_u32()) {
1010        if let (Some(n), Some(d)) = (
1011            numer.checked_pow(small_exponent),
1012            denom.checked_pow(small_exponent),
1013        ) {
1014            return Ok(RationalInteger::from_reduced_small(n, d));
1015        }
1016    }
1017    let (numer, denom) = base.as_big_pair();
1018    Ok(RationalInteger::from_coprime(
1019        numer.try_pow(exponent)?,
1020        denom.try_pow(exponent)?,
1021    ))
1022}
1023
1024pub fn try_rational_power(
1025    base: &RationalInteger,
1026    exponent: &RationalInteger,
1027) -> Result<RationalInteger, NumericFailure> {
1028    if !exponent.numer_is_positive() && !exponent.numer_is_negative() && exponent.numer_is_zero() {
1029        return Ok(rational_one());
1030    }
1031
1032    if exponent.is_integer() {
1033        let integer_exponent = exponent.to_big_numer();
1034        if power_exceeds_magnitude_limit(base, integer_exponent.magnitude()) {
1035            return Err(NumericFailure::MagnitudeLimit);
1036        }
1037        return try_pow_integer(base, &integer_exponent);
1038    }
1039
1040    if base.numer_is_zero() {
1041        if exponent.numer_is_negative() || exponent.numer_is_zero() {
1042            return Err(NumericFailure::DivisionByZero);
1043        }
1044        return Ok(rational_new(0, 1));
1045    }
1046
1047    let root_degree = exponent.to_big_denom().magnitude().clone();
1048    let abs_exponent = exponent.to_big_numer();
1049
1050    let result_negative = base.numer_is_negative() && abs_exponent.magnitude().is_odd();
1051    if result_negative && !root_degree.is_odd() {
1052        return Err(NumericFailure::Irrational);
1053    }
1054
1055    let numer_root = exact_integer_root(&base.to_big_numer().try_abs()?, &root_degree)?;
1056    let denom_root = exact_integer_root(&base.to_big_denom(), &root_degree)?;
1057    let signed_numer_root = if result_negative {
1058        numer_root.try_neg()?
1059    } else {
1060        numer_root
1061    };
1062    let root = RationalInteger::from_coprime(signed_numer_root, denom_root);
1063    if power_exceeds_magnitude_limit(&root, abs_exponent.magnitude()) {
1064        return Err(NumericFailure::MagnitudeLimit);
1065    }
1066    let raised = try_pow_magnitude(&root, abs_exponent.magnitude())?;
1067
1068    if exponent.numer_is_negative() {
1069        raised.try_reciprocal()
1070    } else {
1071        Ok(raised)
1072    }
1073}
1074
1075/// True when `base^exponent` provably has a numerator or denominator above
1076/// [`MAX_RATIONAL_BITS`]: a `b`-bit magnitude with `b >= 2` raised to `e` has at least
1077/// `(b - 1) * e + 1` bits. When false the result has at most twice the limit, so
1078/// computing it before the final check stays bounded.
1079fn power_exceeds_magnitude_limit(base: &RationalInteger, exponent: &BigUint) -> bool {
1080    [base.numer_magnitude_bits(), base.denom_magnitude_bits()]
1081        .into_iter()
1082        .filter(|bits| *bits >= 2)
1083        .any(|bits| match exponent.to_u32() {
1084            Some(exponent) => (bits - 1)
1085                .checked_mul(u64::from(exponent))
1086                .is_none_or(|lower_bound| lower_bound >= MAX_RATIONAL_BITS),
1087            None => true,
1088        })
1089}
1090
1091/// Exact integer `degree`-th root of a positive `value`; `Irrational` when none exists.
1092fn exact_integer_root(value: &BigInt, degree: &BigUint) -> Result<BigInt, NumericFailure> {
1093    let root = value.try_nth_root(degree)?;
1094    if root.try_pow(degree)? == *value {
1095        Ok(root)
1096    } else {
1097        Err(NumericFailure::Irrational)
1098    }
1099}
1100
1101pub use {try_div as checked_div, try_mul as checked_mul, try_pow_integer as checked_pow_integer};
1102
1103// ---------------------------------------------------------------------------
1104// Tests
1105// ---------------------------------------------------------------------------
1106
1107#[cfg(test)]
1108mod tests {
1109    use super::*;
1110    use rust_decimal::Decimal;
1111    use rust_decimal::MathematicalOps;
1112    use std::str::FromStr;
1113
1114    #[test]
1115    fn power_of_two_with_all_ones_exponent_is_positive() {
1116        let two_pow_127 =
1117            RationalInteger::from_bigint_strings("170141183460469231731687303715884105728", "1")
1118                .unwrap();
1119        assert!(two_pow_127.numer_is_positive(), "parsed 2^127 sign");
1120        let big =
1121            BigInt::try_from_str_radix("170141183460469231731687303715884105728", 10).unwrap();
1122        assert!(big.is_positive(), "BigInt 2^127 sign");
1123        let product = try_mul(&rational_new(1 << 62, 1), &rational_new(1 << 62, 1)).unwrap();
1124        let product = try_mul(&product, &rational_new(8, 1)).unwrap();
1125        assert!(product.numer_is_positive(), "2^62 * 2^62 * 8 sign");
1126        let powered = try_pow_integer(&rational_new(2, 1), &BigInt::from_i64(127)).unwrap();
1127        assert!(powered.numer_is_positive(), "2^127 via power sign");
1128        assert_eq!(powered, two_pow_127);
1129    }
1130
1131    #[test]
1132    fn i128_min_numerator_promotes_instead_of_failing() {
1133        let two_pow_127 =
1134            RationalInteger::from_bigint_strings("170141183460469231731687303715884105728", "1")
1135                .unwrap();
1136        let min = RationalInteger::from_small(i128::MIN, 1).unwrap();
1137        assert_eq!(rational_abs(&min).unwrap(), two_pow_127);
1138        assert_eq!(try_div(&min, &rational_new(-1, 1)).unwrap(), two_pow_127);
1139        let reciprocal = try_div(&rational_new(1, 1), &min).unwrap();
1140        assert_eq!(
1141            reciprocal,
1142            RationalInteger::from_bigint_strings("-1", "170141183460469231731687303715884105728")
1143                .unwrap()
1144        );
1145        assert_eq!(
1146            RationalInteger::from_small(i128::MIN, -2).unwrap(),
1147            RationalInteger::from_bigint_strings("85070591730234615865843651857942052864", "1")
1148                .unwrap()
1149        );
1150        assert_eq!(
1151            RationalInteger::from_small(1, i128::MIN).unwrap(),
1152            RationalInteger::from_bigint_strings("-1", "170141183460469231731687303715884105728")
1153                .unwrap()
1154        );
1155        assert_eq!(
1156            RationalInteger::from_small(i128::MIN, i128::MIN).unwrap(),
1157            rational_one()
1158        );
1159        assert_eq!(
1160            RationalInteger::from_small(3, i128::MIN).unwrap(),
1161            RationalInteger::from_bigint_strings("-3", "170141183460469231731687303715884105728")
1162                .unwrap()
1163        );
1164    }
1165
1166    fn two_pow(exponent: i64) -> RationalInteger {
1167        try_pow_integer(&rational_new(2, 1), &BigInt::from_i64(exponent)).unwrap()
1168    }
1169
1170    fn operate(
1171        left: &RationalInteger,
1172        operation: NumericOperation,
1173        right: &RationalInteger,
1174    ) -> Result<RationalInteger, NumericFailure> {
1175        rational_operation(left, operation, right)
1176    }
1177
1178    fn negate(value: &RationalInteger) -> RationalInteger {
1179        try_sub(&rational_zero(), value).unwrap()
1180    }
1181
1182    const LIMIT: i64 = MAX_RATIONAL_BITS as i64;
1183
1184    #[test]
1185    fn rational_operation_accepts_results_at_the_bit_limit() {
1186        let at_limit = two_pow(LIMIT - 1);
1187        assert_eq!(at_limit.numer_magnitude_bits(), MAX_RATIONAL_BITS);
1188        let half_limit = two_pow(LIMIT / 2 - 1);
1189        assert_eq!(
1190            operate(&half_limit, NumericOperation::Multiply, &two_pow(LIMIT / 2)).unwrap(),
1191            at_limit
1192        );
1193        assert_eq!(
1194            operate(
1195                &two_pow(LIMIT - 2),
1196                NumericOperation::Add,
1197                &two_pow(LIMIT - 2)
1198            )
1199            .unwrap(),
1200            at_limit
1201        );
1202        let reciprocal = operate(&rational_one(), NumericOperation::Divide, &at_limit).unwrap();
1203        assert_eq!(reciprocal.denom_magnitude_bits(), MAX_RATIONAL_BITS);
1204    }
1205
1206    #[test]
1207    fn rational_operation_rejects_numerator_beyond_the_bit_limit() {
1208        let at_limit = two_pow(LIMIT - 1);
1209        let two = rational_new(2, 1);
1210        for (operation, right) in [
1211            (NumericOperation::Add, at_limit.clone()),
1212            (NumericOperation::Subtract, negate(&at_limit)),
1213            (NumericOperation::Multiply, two.clone()),
1214            (NumericOperation::Divide, rational_new(1, 2)),
1215            (NumericOperation::Power, two.clone()),
1216        ] {
1217            assert_eq!(
1218                operate(&at_limit, operation, &right).unwrap_err(),
1219                NumericFailure::MagnitudeLimit,
1220                "{operation:?}"
1221            );
1222        }
1223    }
1224
1225    #[test]
1226    fn rational_operation_rejects_denominator_beyond_the_bit_limit() {
1227        let reciprocal_at_limit = try_div(&rational_one(), &two_pow(LIMIT - 1)).unwrap();
1228        assert_eq!(
1229            operate(
1230                &reciprocal_at_limit,
1231                NumericOperation::Divide,
1232                &rational_new(2, 1)
1233            )
1234            .unwrap_err(),
1235            NumericFailure::MagnitudeLimit
1236        );
1237        assert_eq!(
1238            operate(
1239                &reciprocal_at_limit,
1240                NumericOperation::Multiply,
1241                &rational_new(1, 2)
1242            )
1243            .unwrap_err(),
1244            NumericFailure::MagnitudeLimit
1245        );
1246    }
1247
1248    #[test]
1249    fn power_precheck_is_exact_at_the_bit_limit() {
1250        let two = rational_new(2, 1);
1251        assert_eq!(
1252            operate(&two, NumericOperation::Power, &rational_new(LIMIT - 1, 1)).unwrap(),
1253            two_pow(LIMIT - 1)
1254        );
1255        assert!(!power_exceeds_magnitude_limit(
1256            &two,
1257            &BigUint::try_from_u32(LIMIT as u32 - 1).unwrap()
1258        ));
1259        assert!(power_exceeds_magnitude_limit(
1260            &two,
1261            &BigUint::try_from_u32(LIMIT as u32).unwrap()
1262        ));
1263        let two_pow_sixteen = rational_new(1 << 16, 1);
1264        let largest_fitting_exponent = (LIMIT as u32 - 1) / 16;
1265        assert!(!power_exceeds_magnitude_limit(
1266            &two_pow_sixteen,
1267            &BigUint::try_from_u32(largest_fitting_exponent).unwrap()
1268        ));
1269        assert_eq!(
1270            operate(
1271                &two_pow_sixteen,
1272                NumericOperation::Power,
1273                &rational_new(i64::from(largest_fitting_exponent), 1)
1274            )
1275            .unwrap(),
1276            two_pow(16 * i64::from(largest_fitting_exponent))
1277        );
1278    }
1279
1280    #[test]
1281    fn power_precheck_rejects_huge_exponents_before_allocating() {
1282        let huge = RationalInteger::from_bigint_strings("1180591620717411303424", "1").unwrap();
1283        for base in [rational_new(2, 1), rational_new(1, 2), rational_new(-3, 7)] {
1284            assert_eq!(
1285                operate(&base, NumericOperation::Power, &huge).unwrap_err(),
1286                NumericFailure::MagnitudeLimit,
1287                "{base:?}"
1288            );
1289            assert_eq!(
1290                operate(&base, NumericOperation::Power, &negate(&huge)).unwrap_err(),
1291                NumericFailure::MagnitudeLimit,
1292                "{base:?} negative exponent"
1293            );
1294        }
1295        assert_eq!(
1296            operate(&rational_new(-1, 1), NumericOperation::Power, &huge).unwrap(),
1297            rational_one()
1298        );
1299    }
1300
1301    #[test]
1302    fn power_precheck_applies_to_the_root_of_fractional_exponents() {
1303        let four = rational_new(4, 1);
1304        assert_eq!(
1305            operate(&four, NumericOperation::Power, &rational_new(LIMIT - 1, 2)).unwrap(),
1306            two_pow(LIMIT - 1)
1307        );
1308        assert_eq!(
1309            operate(&four, NumericOperation::Power, &rational_new(LIMIT + 1, 2)).unwrap_err(),
1310            NumericFailure::MagnitudeLimit
1311        );
1312    }
1313
1314    use crate::computation::bigint::work_counters;
1315
1316    fn big(value: i64) -> BigInt {
1317        BigInt::from_i64(value)
1318    }
1319
1320    fn big_pow(base: i64, exponent: u32) -> BigInt {
1321        big(base)
1322            .try_pow(&BigUint::try_from_u32(exponent).unwrap())
1323            .unwrap()
1324    }
1325
1326    fn big_add(left: &BigInt, right: &BigInt) -> BigInt {
1327        left.try_add(right).unwrap()
1328    }
1329
1330    fn fraction(numer: BigInt, denom: BigInt) -> RationalInteger {
1331        try_rational_new(numer, denom).unwrap()
1332    }
1333
1334    fn assert_canonical(value: &RationalInteger) {
1335        match &value.repr {
1336            Repr::Small { numer, denom } => {
1337                assert!(*denom > 0, "{value:?} denominator must be positive");
1338                assert_eq!(
1339                    reduce_small(*numer, *denom),
1340                    (*numer, *denom),
1341                    "{value:?} is not reduced"
1342                );
1343            }
1344            Repr::Big { numer, denom } => {
1345                assert!(
1346                    denom.is_positive(),
1347                    "{value:?} denominator must be positive"
1348                );
1349                assert!(
1350                    numer.to_i128().is_none() || denom.to_i128().is_none(),
1351                    "{value:?} fits i128 and must be Small"
1352                );
1353                let reduced = RationalInteger::try_reduce_ref(value).expect("reducible");
1354                assert!(&reduced == value, "{value:?} is not reduced");
1355            }
1356        }
1357    }
1358
1359    fn reference_mul(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(&rn).unwrap(), ld.try_mul(&rd).unwrap())
1363    }
1364
1365    fn reference_div(left: &RationalInteger, right: &RationalInteger) -> RationalInteger {
1366        let (ln, ld) = left.as_big_pair();
1367        let (rn, rd) = right.as_big_pair();
1368        fraction(ln.try_mul(&rd).unwrap(), ld.try_mul(&rn).unwrap())
1369    }
1370
1371    fn reference_pow(base: &RationalInteger, exponent: i64) -> RationalInteger {
1372        let (n, d) = base.as_big_pair();
1373        let magnitude = BigUint::try_from_u64(exponent.unsigned_abs()).unwrap();
1374        let (n, d) = (
1375            n.try_pow(&magnitude).unwrap(),
1376            d.try_pow(&magnitude).unwrap(),
1377        );
1378        if exponent < 0 {
1379            fraction(d, n)
1380        } else {
1381            fraction(n, d)
1382        }
1383    }
1384
1385    /// `2^32767 - 1` and `2^32767 + 1`: odd, two apart, hence coprime; 32767 and 32768 bits.
1386    fn cap_sized_coprime_pair() -> (BigInt, BigInt) {
1387        let power = big_pow(2, 32_767);
1388        (big_add(&power, &big(-1)), big_add(&power, &big(1)))
1389    }
1390
1391    fn arithmetic_fixtures() -> Vec<RationalInteger> {
1392        vec![
1393            rational_zero(),
1394            rational_one(),
1395            rational_new(-1, 1),
1396            rational_new(3, 7),
1397            rational_new(-22, 15),
1398            RationalInteger::from_small(i128::MAX, 1).unwrap(),
1399            RationalInteger::from_small(i128::MIN, 1).unwrap(),
1400            RationalInteger::from_small(1, i128::MAX).unwrap(),
1401            RationalInteger::from_small(i128::MIN, i128::MAX).unwrap(),
1402            fraction(big_add(&big_pow(2, 200), &big(1)), big_pow(3, 100)),
1403            fraction(big_pow(-5, 91), big_pow(2, 150)),
1404            fraction(big_pow(6, 80), big_pow(35, 40)),
1405            fraction(big_pow(35, 40), big_pow(-6, 81)),
1406            fraction(big_pow(2, 130), big(3)),
1407        ]
1408    }
1409
1410    #[test]
1411    fn mul_div_pow_match_reduction_of_the_raw_result() {
1412        let fixtures = arithmetic_fixtures();
1413        for left in &fixtures {
1414            for right in &fixtures {
1415                let product = try_mul(left, right).unwrap();
1416                assert_canonical(&product);
1417                assert_eq!(product, reference_mul(left, right), "{left:?} * {right:?}");
1418                if right.numer_is_zero() {
1419                    continue;
1420                }
1421                let quotient = try_div(left, right).unwrap();
1422                assert_canonical(&quotient);
1423                assert_eq!(quotient, reference_div(left, right), "{left:?} / {right:?}");
1424            }
1425            for exponent in -3..=3 {
1426                if exponent < 0 && left.numer_is_zero() {
1427                    continue;
1428                }
1429                let power = try_pow_integer(left, &BigInt::from_i64(exponent)).unwrap();
1430                assert_canonical(&power);
1431                assert_eq!(
1432                    power,
1433                    reference_pow(left, exponent),
1434                    "{left:?} ^ {exponent}"
1435                );
1436            }
1437        }
1438    }
1439
1440    #[test]
1441    fn try_div_keeps_denominator_positive_for_every_sign() {
1442        let (a, b) = cap_sized_coprime_pair();
1443        let positive = fraction(a.try_clone().unwrap(), big_pow(3, 50));
1444        let other = fraction(big_pow(7, 60), b);
1445        let negate = |value: &RationalInteger| try_sub(&rational_zero(), value).unwrap();
1446        for (left, right) in [
1447            (positive.clone(), negate(&other)),
1448            (negate(&positive), other.clone()),
1449            (negate(&positive), negate(&other)),
1450            (positive.clone(), other.clone()),
1451        ] {
1452            let quotient = try_div(&left, &right).unwrap();
1453            assert_canonical(&quotient);
1454            assert_eq!(quotient, reference_div(&left, &right));
1455        }
1456        assert_eq!(
1457            try_div(&positive, &negate(&positive)).unwrap(),
1458            rational_new(-1, 1)
1459        );
1460    }
1461
1462    #[test]
1463    fn power_of_a_reduced_fraction_performs_no_gcd() {
1464        let bases = [
1465            rational_new(7301, 7300),
1466            rational_new(-3, 7),
1467            fraction(big_add(&big_pow(2, 200), &big(1)), big_pow(3, 130)),
1468            fraction(big_pow(-5, 91), big_pow(2, 150)),
1469        ];
1470        for base in &bases {
1471            for exponent in [-301, -2, 2, 5, 200] {
1472                work_counters::reset();
1473                let power = try_pow_integer(base, &BigInt::from_i64(exponent)).unwrap();
1474                let counts = work_counters::snapshot();
1475                assert_eq!(counts.gcd_calls, 0, "{base:?} ^ {exponent}");
1476                assert_canonical(&power);
1477                assert_eq!(
1478                    power,
1479                    reference_pow(base, exponent),
1480                    "{base:?} ^ {exponent}"
1481                );
1482            }
1483        }
1484    }
1485
1486    #[test]
1487    fn product_of_reciprocals_takes_gcds_of_the_operands_only() {
1488        let (a, b) = cap_sized_coprime_pair();
1489        let operand_bits = a.bits().max(b.bits());
1490        let left = fraction(a.try_clone().unwrap(), b.try_clone().unwrap());
1491        let right = fraction(b, a);
1492        work_counters::reset();
1493        assert_eq!(try_mul(&left, &right).unwrap(), rational_one());
1494        let counts = work_counters::snapshot();
1495        assert_eq!(counts.gcd_calls, 2);
1496        assert!(counts.gcd_max_operand_bits <= operand_bits);
1497        work_counters::reset();
1498        assert_eq!(try_div(&left, &left).unwrap(), rational_one());
1499        let counts = work_counters::snapshot();
1500        assert_eq!(counts.gcd_calls, 2);
1501        assert!(counts.gcd_max_operand_bits <= operand_bits);
1502    }
1503
1504    #[test]
1505    fn rational_zero_is_zero() {
1506        assert!(rational_is_zero(&rational_zero()));
1507    }
1508
1509    #[test]
1510    fn decimal_one_half_lifts_to_rational() {
1511        let decimal = Decimal::from_str("0.5").unwrap();
1512        let rational = decimal_to_rational(decimal).unwrap();
1513        assert_eq!(rational, rational_new(1, 2));
1514    }
1515
1516    #[test]
1517    fn try_to_decimal_one_third() {
1518        let rational = rational_new(1, 3);
1519        let decimal = rational.try_to_decimal().unwrap();
1520        let expected = Decimal::from_str("0.3333333333333333333333333333").unwrap();
1521        assert_eq!(decimal, expected);
1522    }
1523
1524    #[test]
1525    fn try_to_decimal_tiny_rounds_to_zero() {
1526        let rational =
1527            RationalInteger::from_bigint_strings("1", "1000000000000000000000000000000").unwrap();
1528        assert_eq!(rational.try_to_decimal().unwrap(), Decimal::ZERO);
1529    }
1530
1531    #[test]
1532    fn try_to_decimal_magnitude_beyond_max_returns_overflow() {
1533        let max = Decimal::MAX.normalize();
1534        let max_rational = decimal_to_rational(max).unwrap();
1535        let twice = try_mul(&max_rational, &rational_new(2, 1)).unwrap();
1536        assert_eq!(
1537            twice.try_to_decimal().unwrap_err(),
1538            NumericFailure::Overflow,
1539        );
1540    }
1541
1542    #[test]
1543    fn try_to_decimal_string_integer() {
1544        let rational = rational_new(37, 1);
1545        assert_eq!(rational.try_to_decimal_string().unwrap(), "37");
1546    }
1547
1548    #[test]
1549    fn display_str_shows_decimal_when_try_to_decimal_succeeds() {
1550        let rational = rational_new(355, 113);
1551        let display = rational.display_str();
1552        assert!(
1553            !display.contains('/'),
1554            "rational with successful try_to_decimal must display as decimal, got {display}"
1555        );
1556    }
1557
1558    #[test]
1559    fn try_to_decimal_huge_cancelling_rationals() {
1560        let numer = BigInt::try_from_str_radix("1", 10)
1561            .unwrap()
1562            .try_pow(&BigUint::try_from_u32(100).unwrap())
1563            .unwrap();
1564        let rational = try_rational_new(numer.clone(), numer).unwrap();
1565        assert_eq!(rational.try_to_decimal().unwrap(), Decimal::ONE);
1566    }
1567
1568    #[test]
1569    fn try_mul_integer() {
1570        let left = rational_new(50, 1);
1571        let right = rational_new(86400, 1);
1572        let product = try_mul(&left, &right).unwrap();
1573        assert_eq!(product, rational_new(4_320_000, 1));
1574    }
1575
1576    #[test]
1577    fn try_pow_negative_exponent_inverts_base() {
1578        let hour_factor = rational_new(3600, 1);
1579        let inverse = try_pow_integer(&hour_factor, &BigInt::from_i64(-1)).unwrap();
1580        assert_eq!(inverse, rational_new(1, 3600));
1581    }
1582
1583    #[test]
1584    fn rational_operation_divide_by_zero() {
1585        let left = rational_new(1, 1);
1586        let right = rational_new(0, 1);
1587        let failure = rational_operation(&left, NumericOperation::Divide, &right).unwrap_err();
1588        assert_eq!(failure, NumericFailure::DivisionByZero);
1589    }
1590
1591    #[test]
1592    fn rational_operation_power_irrational() {
1593        let base = rational_new(2, 1);
1594        let exponent = rational_new(1, 2);
1595        let failure = rational_operation(&base, NumericOperation::Power, &exponent).unwrap_err();
1596        assert_eq!(failure, NumericFailure::Irrational);
1597    }
1598
1599    #[test]
1600    fn rational_operation_power_exact() {
1601        let base = rational_new(4, 1);
1602        let exponent = rational_new(1, 2);
1603        let result = rational_operation(&base, NumericOperation::Power, &exponent).unwrap();
1604        assert_eq!(result, rational_new(2, 1));
1605    }
1606
1607    #[test]
1608    fn rational_power_exact_roots_of_rationals() {
1609        let power = |base: RationalInteger, exponent: RationalInteger| {
1610            rational_operation(&base, NumericOperation::Power, &exponent)
1611        };
1612        assert_eq!(
1613            power(rational_new(9, 4), rational_new(1, 2)).unwrap(),
1614            rational_new(3, 2)
1615        );
1616        assert_eq!(
1617            power(rational_new(9, 4), rational_new(-1, 2)).unwrap(),
1618            rational_new(2, 3)
1619        );
1620        assert_eq!(
1621            power(rational_new(8, 1), rational_new(2, 3)).unwrap(),
1622            rational_new(4, 1)
1623        );
1624        assert_eq!(
1625            power(rational_new(-8, 1), rational_new(1, 3)).unwrap(),
1626            rational_new(-2, 1)
1627        );
1628        assert_eq!(
1629            power(rational_new(-4, 1), rational_new(2, 1)).unwrap(),
1630            rational_new(16, 1)
1631        );
1632        assert_eq!(
1633            power(rational_new(1, 1), rational_new(49_595, 100_000)).unwrap(),
1634            rational_new(1, 1)
1635        );
1636    }
1637
1638    #[test]
1639    fn power_with_huge_integer_exponent_of_one_and_minus_one() {
1640        let even_huge =
1641            RationalInteger::from_bigint_strings("1000000000000000000000000000000", "1").unwrap();
1642        let odd_huge =
1643            RationalInteger::from_bigint_strings("1000000000000000000000000000001", "1").unwrap();
1644        assert_eq!(
1645            rational_operation(&rational_new(1, 1), NumericOperation::Power, &even_huge).unwrap(),
1646            rational_new(1, 1)
1647        );
1648        assert_eq!(
1649            rational_operation(&rational_new(-1, 1), NumericOperation::Power, &odd_huge).unwrap(),
1650            rational_new(-1, 1)
1651        );
1652    }
1653
1654    #[test]
1655    fn power_with_huge_root_degree_above_bit_length_is_irrational() {
1656        let degree = RationalInteger::from_bigint_strings("1", "1000000000000").unwrap();
1657        assert_eq!(
1658            rational_operation(&rational_new(2, 1), NumericOperation::Power, &degree).unwrap_err(),
1659            NumericFailure::Irrational
1660        );
1661    }
1662
1663    #[test]
1664    fn power_with_exponent_beyond_every_allocation_is_out_of_memory() {
1665        let exponent = BigInt::from_i128(1i128 << 70);
1666        assert_eq!(
1667            try_pow_integer(&rational_new(2, 1), &exponent).unwrap_err(),
1668            NumericFailure::OutOfMemory
1669        );
1670        assert_eq!(
1671            try_pow_integer(&rational_new(1, 1), &exponent).unwrap(),
1672            rational_new(1, 1)
1673        );
1674    }
1675
1676    #[test]
1677    fn rational_power_irrational_fails_fast_for_large_degree() {
1678        let failure = rational_operation(
1679            &rational_new(480, 1),
1680            NumericOperation::Power,
1681            &rational_new(49_595, 100_000),
1682        )
1683        .unwrap_err();
1684        assert_eq!(failure, NumericFailure::Irrational);
1685        let failure = rational_operation(
1686            &rational_new(2, 1),
1687            NumericOperation::Power,
1688            &rational_new(49_595, 100_000),
1689        )
1690        .unwrap_err();
1691        assert_eq!(failure, NumericFailure::Irrational);
1692    }
1693
1694    #[test]
1695    fn rational_power_negative_base_even_root_is_irrational() {
1696        let failure = rational_operation(
1697            &rational_new(-4, 1),
1698            NumericOperation::Power,
1699            &rational_new(1, 2),
1700        )
1701        .unwrap_err();
1702        assert_eq!(failure, NumericFailure::Irrational);
1703    }
1704
1705    #[test]
1706    fn rational_operation_add() {
1707        let left = rational_new(1, 3);
1708        let right = rational_new(1, 6);
1709        let sum = rational_operation(&left, NumericOperation::Add, &right).unwrap();
1710        assert_eq!(sum, rational_new(1, 2));
1711    }
1712
1713    #[test]
1714    fn rational_operation_with_fallback_add_exact_rational() {
1715        let left = rational_new(1, 3);
1716        let right = rational_new(1, 6);
1717        let sum = rational_operation_with_fallback(&left, NumericOperation::Add, &right).unwrap();
1718        assert_eq!(sum, rational_new(1, 2));
1719    }
1720
1721    #[test]
1722    fn rational_operation_with_fallback_power_sqrt_via_decimal() {
1723        let result = rational_operation_with_fallback(
1724            &rational_new(2, 1),
1725            NumericOperation::Power,
1726            &rational_new(1, 2),
1727        )
1728        .unwrap();
1729        let expected = rational_new(2, 1).try_to_decimal().unwrap().sqrt().unwrap();
1730        assert_eq!(
1731            result.try_to_decimal().unwrap().round_dp(27),
1732            expected.round_dp(27),
1733        );
1734    }
1735
1736    #[test]
1737    fn rational_operation_with_fallback_power_compound_day_fraction() {
1738        let base = decimal_to_rational(Decimal::from_str("1.04").expect("1.04")).expect("rational");
1739        let result = rational_operation_with_fallback(
1740            &base,
1741            NumericOperation::Power,
1742            &rational_new(28, 365),
1743        )
1744        .expect("1.04^(28/365) must use Decimal fallback");
1745        let decimal = result
1746            .try_to_decimal()
1747            .expect("fallback power must fit Decimal::MAX_SCALE");
1748        let expected = base
1749            .try_to_decimal()
1750            .expect("1.04")
1751            .checked_powd(Decimal::from_str("28").expect("28") / Decimal::from(365))
1752            .expect("checked_powd");
1753        // ℚ round-trip can move the last Decimal digit by 1 ULP.
1754        let diff = (decimal - expected).abs();
1755        assert!(
1756            diff <= Decimal::new(1, 27),
1757            "1.04^(28/365) ≈ checked_powd within 1e-27, got {decimal} vs {expected} (diff {diff})"
1758        );
1759    }
1760
1761    #[test]
1762    fn rational_operation_with_fallback_power_cube_root_irrational() {
1763        let base = decimal_to_rational(Decimal::from_str("1.04").expect("1.04")).expect("rational");
1764        let result =
1765            rational_operation_with_fallback(&base, NumericOperation::Power, &rational_new(1, 3))
1766                .expect("1.04^(1/3) must use Decimal fallback");
1767        assert!(result.try_to_decimal().is_ok());
1768    }
1769
1770    #[test]
1771    fn rational_operation_with_fallback_power_negative_half() {
1772        let result = rational_operation_with_fallback(
1773            &rational_new(2, 1),
1774            NumericOperation::Power,
1775            &rational_new(-1, 2),
1776        )
1777        .expect("2^(-1/2) must use Decimal fallback");
1778        let actual = result.try_to_decimal().expect("fits Decimal");
1779        let expected = Decimal::ONE
1780            / rational_new(2, 1)
1781                .try_to_decimal()
1782                .expect("2")
1783                .sqrt()
1784                .expect("sqrt(2)");
1785        assert_eq!(actual.round_dp(26), expected.round_dp(26));
1786    }
1787
1788    #[test]
1789    fn rational_operation_with_fallback_power_negative_base_fraction_stays_irrational() {
1790        let err = rational_operation_with_fallback(
1791            &rational_new(-2, 1),
1792            NumericOperation::Power,
1793            &rational_new(1, 3),
1794        )
1795        .expect_err("negative base with non-integer exponent stays irrational on R");
1796        assert_eq!(err, NumericFailure::Irrational);
1797    }
1798
1799    #[test]
1800    fn rational_operation_exact_cube_root_unchanged() {
1801        let result = rational_operation(
1802            &rational_new(8, 1),
1803            NumericOperation::Power,
1804            &rational_new(1, 3),
1805        )
1806        .expect("8^(1/3) is exact in Q");
1807        assert_eq!(result, rational_new(2, 1));
1808    }
1809
1810    #[test]
1811    fn rational_modulo_is_floored_and_takes_divisor_sign() {
1812        assert_eq!(
1813            operate(
1814                &rational_new(-7, 1),
1815                NumericOperation::Modulo,
1816                &rational_new(3, 1)
1817            )
1818            .unwrap(),
1819            rational_new(2, 1)
1820        );
1821        assert_eq!(
1822            operate(
1823                &rational_new(7, 1),
1824                NumericOperation::Modulo,
1825                &rational_new(-3, 1)
1826            )
1827            .unwrap(),
1828            rational_new(-2, 1)
1829        );
1830        assert_eq!(
1831            operate(
1832                &rational_new(-7, 2),
1833                NumericOperation::Modulo,
1834                &rational_new(1, 1)
1835            )
1836            .unwrap(),
1837            rational_new(1, 2)
1838        );
1839        assert_eq!(
1840            operate(
1841                &rational_new(-7, 1),
1842                NumericOperation::Modulo,
1843                &rational_new(-3, 1)
1844            )
1845            .unwrap(),
1846            rational_new(-1, 1)
1847        );
1848    }
1849
1850    #[test]
1851    fn rational_abs_negates_negative_numerator() {
1852        let negative = rational_new(-172_800, 1);
1853        assert_eq!(rational_abs(&negative).unwrap(), rational_new(172_800, 1));
1854    }
1855
1856    #[test]
1857    fn try_to_decimal_string_rejects_magnitude_overflow() {
1858        let too_large =
1859            RationalInteger::from_bigint_strings("10000000000000000000000000000000", "1").unwrap();
1860        assert_eq!(
1861            too_large.try_to_decimal().unwrap_err(),
1862            NumericFailure::Overflow
1863        );
1864        assert_eq!(
1865            too_large.try_to_decimal_string().unwrap_err(),
1866            NumericFailure::Overflow,
1867        );
1868    }
1869
1870    #[test]
1871    fn decimal_max_times_decimal_max_stays_exact_without_decimal_fallback() {
1872        let max_decimal = Decimal::MAX.normalize();
1873        let left = decimal_to_rational(max_decimal).unwrap();
1874        let right = decimal_to_rational(max_decimal).unwrap();
1875        let product = rational_operation(&left, NumericOperation::Multiply, &right).unwrap();
1876        let expected = try_mul(&left, &right).unwrap();
1877        assert_eq!(product, expected);
1878    }
1879
1880    #[test]
1881    fn alloc_error_maps_to_out_of_memory() {
1882        assert_eq!(
1883            NumericFailure::from(crate::computation::bigint::AllocError),
1884            NumericFailure::OutOfMemory
1885        );
1886    }
1887
1888    #[test]
1889    fn small_path_basic_arithmetic() {
1890        let a = rational_new(3, 1);
1891        let b = rational_new(5, 1);
1892        assert_eq!(try_add(&a, &b).unwrap(), rational_new(8, 1));
1893        assert_eq!(try_sub(&a, &b).unwrap(), rational_new(-2, 1));
1894        assert_eq!(try_mul(&a, &b).unwrap(), rational_new(15, 1));
1895        assert_eq!(try_div(&a, &b).unwrap(), rational_new(3, 5));
1896    }
1897
1898    #[test]
1899    fn small_path_reduces() {
1900        let r = rational_new(6, 4);
1901        assert_eq!(r, rational_new(3, 2));
1902    }
1903
1904    #[test]
1905    fn small_path_try_to_decimal_integer() {
1906        let r = rational_new(42, 1);
1907        assert_eq!(r.try_to_decimal().unwrap(), Decimal::from(42));
1908    }
1909}