Skip to main content

oxinum_rational/native/
rational.rs

1//! `BigRational` struct definition, invariants, constructors, accessors,
2//! basic predicates, [`Neg`], [`Display`], and `From<primitive>` impls.
3//!
4//! The arithmetic operators (`Add`/`Sub`/`Mul`/`Div`/`Rem` and their
5//! `*Assign` partners) plus `PartialOrd`/`Ord`/`Hash` live in
6//! [`super::rational_ops`].
7
8use core::cmp::Ordering;
9use core::ops::Neg;
10use std::fmt;
11
12use oxinum_core::{OxiNumError, OxiNumResult, Sign};
13use oxinum_int::native::{gcd, BigInt, BigUint};
14
15/// Native arbitrary-precision rational number, always stored in lowest terms.
16///
17/// Internally represented as a signed numerator (`BigInt`) over a strictly
18/// positive denominator (`BigUint`). The sign always lives on the numerator;
19/// `den` is non-zero by invariant.
20///
21/// # Canonical form
22///
23/// - `gcd(|num|, den) == 1`
24/// - `den > 0`
25/// - Zero is the unique `{ num: BigInt::ZERO, den: BigUint::from_u64(1) }`.
26///
27/// # Examples
28///
29/// ```
30/// use oxinum_rational::native::BigRational;
31/// use oxinum_int::native::{BigInt, BigUint};
32///
33/// let half = BigRational::from_parts(BigInt::from(1i64), BigUint::from_u64(2))
34///     .expect("non-zero denominator");
35/// assert_eq!(half.to_string(), "1/2");
36/// ```
37#[derive(Clone, Debug)]
38pub struct BigRational {
39    pub(super) num: BigInt,
40    pub(super) den: BigUint,
41}
42
43// ---------------------------------------------------------------------------
44// Construction
45// ---------------------------------------------------------------------------
46
47impl BigRational {
48    /// Construct a `BigRational` from a numerator and a denominator.
49    ///
50    /// Reduces to lowest terms automatically. Returns
51    /// [`OxiNumError::DivByZero`] when `den` is zero.
52    ///
53    /// # Examples
54    ///
55    /// ```
56    /// use oxinum_rational::native::BigRational;
57    /// use oxinum_int::native::{BigInt, BigUint};
58    /// let r = BigRational::from_parts(BigInt::from(6i64), BigUint::from_u64(4))
59    ///     .expect("non-zero denominator");
60    /// assert_eq!(r.to_string(), "3/2");
61    /// ```
62    pub fn from_parts(num: BigInt, den: BigUint) -> OxiNumResult<Self> {
63        if den.is_zero() {
64            return Err(OxiNumError::DivByZero);
65        }
66        Ok(Self::reduce_unchecked(num, den))
67    }
68
69    /// Construct a `BigRational` representing the integer `n`.
70    ///
71    /// # Examples
72    ///
73    /// ```
74    /// use oxinum_rational::native::BigRational;
75    /// use oxinum_int::native::BigInt;
76    /// let r = BigRational::from_integer(BigInt::from(7i64));
77    /// assert_eq!(r.to_string(), "7");
78    /// ```
79    #[inline]
80    pub fn from_integer(n: BigInt) -> Self {
81        Self {
82            num: n,
83            den: BigUint::one(),
84        }
85    }
86
87    /// Construct a `BigRational` from a signed 64-bit integer.
88    #[inline]
89    pub fn from_i64(n: i64) -> Self {
90        Self::from_integer(BigInt::from(n))
91    }
92
93    /// The canonical zero, `0/1`.
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use oxinum_rational::native::BigRational;
99    /// assert!(BigRational::zero().is_zero());
100    /// ```
101    #[inline]
102    pub fn zero() -> Self {
103        Self {
104            num: BigInt::ZERO,
105            den: BigUint::one(),
106        }
107    }
108
109    /// The canonical one, `1/1`.
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use oxinum_rational::native::BigRational;
115    /// assert!(BigRational::one().is_one());
116    /// ```
117    #[inline]
118    pub fn one() -> Self {
119        Self {
120            num: BigInt::one(),
121            den: BigUint::one(),
122        }
123    }
124
125    // -------------------------------------------------------------------
126    // Internal: reduce-without-checking-zero-denominator
127    // -------------------------------------------------------------------
128
129    /// Reduce `(num, den)` to lowest terms. Caller MUST guarantee
130    /// `!den.is_zero()`.
131    pub(super) fn reduce_unchecked(num: BigInt, den: BigUint) -> Self {
132        // Fast path for zero numerator: canonical zero is `{0, 1}`.
133        if num.is_zero() {
134            return Self::zero();
135        }
136        // `gcd` takes ownership of both arguments; clone the magnitude and
137        // the denominator before consuming them.
138        let g = gcd(num.magnitude().clone(), den.clone());
139        if g.is_one() {
140            return Self { num, den };
141        }
142        // Divide both magnitude and denominator by the GCD.
143        let (sign, mag) = num.into_parts();
144        let new_mag = &mag / &g;
145        let new_den = &den / &g;
146        Self {
147            num: BigInt::from_parts(sign, new_mag),
148            den: new_den,
149        }
150    }
151}
152
153// ---------------------------------------------------------------------------
154// Accessors and predicates
155// ---------------------------------------------------------------------------
156
157impl BigRational {
158    /// Borrow the (signed) numerator.
159    #[inline]
160    pub fn num(&self) -> &BigInt {
161        &self.num
162    }
163
164    /// Borrow the (strictly positive) denominator.
165    #[inline]
166    pub fn den(&self) -> &BigUint {
167        &self.den
168    }
169
170    /// Returns `true` if this value equals zero.
171    #[inline]
172    pub fn is_zero(&self) -> bool {
173        self.num.is_zero()
174    }
175
176    /// Returns `true` if this value equals one.
177    #[inline]
178    pub fn is_one(&self) -> bool {
179        self.num.is_one() && self.den.is_one()
180    }
181
182    /// Returns `true` if this value represents an integer (denominator is one).
183    #[inline]
184    pub fn is_integer(&self) -> bool {
185        self.den.is_one()
186    }
187
188    /// Returns the sign as `+1`, `-1`, or `0`.
189    ///
190    /// Unlike piping [`BigInt::signum`] (which returns `Sign::Positive` for
191    /// zero by canonical-zero invariant), this method actively distinguishes
192    /// zero by checking the numerator first.
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// use oxinum_rational::native::BigRational;
198    /// use oxinum_int::native::{BigInt, BigUint};
199    /// let pos = BigRational::from_parts(BigInt::from(2i64), BigUint::from_u64(3))
200    ///     .expect("non-zero denominator");
201    /// let neg = BigRational::from_parts(BigInt::from(-2i64), BigUint::from_u64(3))
202    ///     .expect("non-zero denominator");
203    /// assert_eq!(pos.signum(), 1);
204    /// assert_eq!(neg.signum(), -1);
205    /// assert_eq!(BigRational::zero().signum(), 0);
206    /// ```
207    pub fn signum(&self) -> i32 {
208        if self.num.is_zero() {
209            0
210        } else {
211            match self.num.sign() {
212                Sign::Positive => 1,
213                Sign::Negative => -1,
214            }
215        }
216    }
217
218    /// Returns the absolute value (a non-negative copy).
219    pub fn abs(&self) -> Self {
220        Self {
221            num: self.num.abs(),
222            den: self.den.clone(),
223        }
224    }
225
226    /// Returns the reciprocal `1/self`.
227    ///
228    /// Returns [`OxiNumError::DivByZero`] when `self` is zero.
229    ///
230    /// The reciprocal is always already reduced because the original was; the
231    /// sign of the numerator is preserved (moving from the old numerator to
232    /// the new numerator slot since the new denominator must be positive).
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// use oxinum_rational::native::BigRational;
238    /// use oxinum_int::native::{BigInt, BigUint};
239    /// let r = BigRational::from_parts(BigInt::from(-2i64), BigUint::from_u64(3))
240    ///     .expect("non-zero denominator");
241    /// let recip = r.recip().expect("non-zero source");
242    /// assert_eq!(recip.to_string(), "-3/2");
243    /// ```
244    pub fn recip(&self) -> OxiNumResult<Self> {
245        if self.num.is_zero() {
246            return Err(OxiNumError::DivByZero);
247        }
248        // |num| becomes the new denominator, den becomes |new_num|, and the
249        // sign of the original numerator transfers to the new numerator.
250        let (sign, mag) = self.num.clone().into_parts();
251        let new_num = BigInt::from_parts(sign, self.den.clone());
252        let new_den = mag;
253        Ok(Self {
254            num: new_num,
255            den: new_den,
256        })
257    }
258
259    // -------------------------------------------------------------------
260    // Crate-internal comparison helpers (shared with rational_ops)
261    // -------------------------------------------------------------------
262
263    /// Compare two rationals using cross-multiplication.
264    ///
265    /// Since both denominators are strictly positive, the sign of `a/b - c/d`
266    /// equals the sign of `a*d - c*b`. We reduce to that single `BigInt`
267    /// comparison after lifting the (unsigned) denominators into `BigInt`.
268    pub(super) fn cmp_impl(&self, other: &Self) -> Ordering {
269        let lhs_den_i = BigInt::from(self.den.clone());
270        let rhs_den_i = BigInt::from(other.den.clone());
271        let lhs = &self.num * &rhs_den_i;
272        let rhs = &other.num * &lhs_den_i;
273        lhs.cmp(&rhs)
274    }
275}
276
277// ---------------------------------------------------------------------------
278// Equality (canonical form makes this trivial)
279// ---------------------------------------------------------------------------
280
281impl PartialEq for BigRational {
282    #[inline]
283    fn eq(&self, other: &Self) -> bool {
284        // Both sides are in canonical form, so structural equality is exact.
285        self.num == other.num && self.den == other.den
286    }
287}
288
289impl Eq for BigRational {}
290
291// ---------------------------------------------------------------------------
292// Display
293// ---------------------------------------------------------------------------
294
295impl fmt::Display for BigRational {
296    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
297        if self.is_integer() {
298            fmt::Display::fmt(&self.num, f)
299        } else {
300            write!(f, "{}/{}", self.num, self.den)
301        }
302    }
303}
304
305// ---------------------------------------------------------------------------
306// Neg
307// ---------------------------------------------------------------------------
308
309impl Neg for BigRational {
310    type Output = BigRational;
311    #[inline]
312    fn neg(self) -> BigRational {
313        BigRational {
314            num: -self.num,
315            den: self.den,
316        }
317    }
318}
319
320impl Neg for &BigRational {
321    type Output = BigRational;
322    #[inline]
323    fn neg(self) -> BigRational {
324        BigRational {
325            num: -&self.num,
326            den: self.den.clone(),
327        }
328    }
329}
330
331// ---------------------------------------------------------------------------
332// Default
333// ---------------------------------------------------------------------------
334
335impl Default for BigRational {
336    #[inline]
337    fn default() -> Self {
338        Self::zero()
339    }
340}
341
342// ---------------------------------------------------------------------------
343// From<primitive>
344// ---------------------------------------------------------------------------
345
346macro_rules! impl_from_signed_primitive {
347    ($($t:ty),*) => {
348        $(
349            impl From<$t> for BigRational {
350                #[inline]
351                fn from(value: $t) -> Self {
352                    Self::from_integer(BigInt::from(value))
353                }
354            }
355        )*
356    };
357}
358
359macro_rules! impl_from_unsigned_primitive {
360    ($($t:ty),*) => {
361        $(
362            impl From<$t> for BigRational {
363                #[inline]
364                fn from(value: $t) -> Self {
365                    Self::from_integer(BigInt::from(value))
366                }
367            }
368        )*
369    };
370}
371
372impl_from_signed_primitive!(i8, i16, i32, i64, i128, isize);
373impl_from_unsigned_primitive!(u8, u16, u32, u64, u128, usize);
374
375impl From<BigInt> for BigRational {
376    #[inline]
377    fn from(n: BigInt) -> Self {
378        Self::from_integer(n)
379    }
380}
381
382impl From<&BigInt> for BigRational {
383    #[inline]
384    fn from(n: &BigInt) -> Self {
385        Self::from_integer(n.clone())
386    }
387}
388
389// ---------------------------------------------------------------------------
390// BigRational → BigInt conversions
391// ---------------------------------------------------------------------------
392
393impl BigRational {
394    /// Converts this rational to a [`BigInt`] by truncating toward zero.
395    ///
396    /// Returns the integer part of `self` (`floor(|self|)` with the original
397    /// sign). Equivalent to T-division (C/Rust integer division).
398    ///
399    /// # Examples
400    ///
401    /// ```
402    /// use oxinum_rational::native::BigRational;
403    /// use oxinum_int::native::{BigInt, BigUint};
404    ///
405    /// let r = BigRational::from_parts(BigInt::from(7i64), BigUint::from_u64(2))
406    ///     .expect("7/2");
407    /// assert_eq!(r.to_bigint_trunc(), BigInt::from(3i64));
408    ///
409    /// let s = BigRational::from_parts(BigInt::from(-7i64), BigUint::from_u64(2))
410    ///     .expect("-7/2");
411    /// assert_eq!(s.to_bigint_trunc(), BigInt::from(-3i64));
412    /// ```
413    pub fn to_bigint_trunc(&self) -> BigInt {
414        if self.is_integer() {
415            return self.num.clone();
416        }
417        let den_int = BigInt::from(self.den.clone());
418        let (q, _) = oxinum_int::native::divrem_int(&self.num, &den_int);
419        q
420    }
421
422    /// Converts this rational to a [`BigInt`] by rounding toward negative
423    /// infinity (floor division).
424    ///
425    /// For negative non-integer values the result is one less than the
426    /// truncation.
427    ///
428    /// # Examples
429    ///
430    /// ```
431    /// use oxinum_rational::native::BigRational;
432    /// use oxinum_int::native::{BigInt, BigUint};
433    ///
434    /// let r = BigRational::from_parts(BigInt::from(7i64), BigUint::from_u64(2))
435    ///     .expect("7/2");
436    /// assert_eq!(r.to_bigint_floor(), BigInt::from(3i64));
437    ///
438    /// let s = BigRational::from_parts(BigInt::from(-7i64), BigUint::from_u64(2))
439    ///     .expect("-7/2");
440    /// assert_eq!(s.to_bigint_floor(), BigInt::from(-4i64));
441    /// ```
442    pub fn to_bigint_floor(&self) -> BigInt {
443        if self.is_integer() {
444            return self.num.clone();
445        }
446        let den_int = BigInt::from(self.den.clone());
447        let (q, r) = oxinum_int::native::divrem_int(&self.num, &den_int);
448        // If negative with non-zero remainder, the truncation is above the
449        // floor, so subtract 1.
450        if self.num.is_negative() && !r.is_zero() {
451            &q - &BigInt::one()
452        } else {
453            q
454        }
455    }
456
457    /// Converts this rational to a [`BigInt`] by rounding toward positive
458    /// infinity (ceiling division).
459    ///
460    /// For positive non-integer values the result is one more than the
461    /// truncation.
462    ///
463    /// # Examples
464    ///
465    /// ```
466    /// use oxinum_rational::native::BigRational;
467    /// use oxinum_int::native::{BigInt, BigUint};
468    ///
469    /// let r = BigRational::from_parts(BigInt::from(7i64), BigUint::from_u64(2))
470    ///     .expect("7/2");
471    /// assert_eq!(r.to_bigint_ceil(), BigInt::from(4i64));
472    ///
473    /// let s = BigRational::from_parts(BigInt::from(-7i64), BigUint::from_u64(2))
474    ///     .expect("-7/2");
475    /// assert_eq!(s.to_bigint_ceil(), BigInt::from(-3i64));
476    /// ```
477    pub fn to_bigint_ceil(&self) -> BigInt {
478        if self.is_integer() {
479            return self.num.clone();
480        }
481        let den_int = BigInt::from(self.den.clone());
482        let (q, r) = oxinum_int::native::divrem_int(&self.num, &den_int);
483        // If positive with non-zero remainder, the truncation is below the
484        // ceiling, so add 1.
485        if self.num.is_positive() && !r.is_zero() {
486            &q + &BigInt::one()
487        } else {
488            q
489        }
490    }
491}
492
493// ---------------------------------------------------------------------------
494// Unit tests
495// ---------------------------------------------------------------------------
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500
501    #[test]
502    fn from_parts_reduces_six_quarters() {
503        let r = BigRational::from_parts(BigInt::from(6i64), BigUint::from_u64(4))
504            .expect("non-zero denominator");
505        assert_eq!(r.num(), &BigInt::from(3i64));
506        assert_eq!(r.den(), &BigUint::from_u64(2));
507    }
508
509    #[test]
510    fn from_parts_handles_negative_numerator() {
511        let r = BigRational::from_parts(BigInt::from(-9i64), BigUint::from_u64(12))
512            .expect("non-zero denominator");
513        assert_eq!(r.to_string(), "-3/4");
514    }
515
516    #[test]
517    fn from_parts_zero_over_anything_is_canonical_zero() {
518        let r = BigRational::from_parts(BigInt::ZERO, BigUint::from_u64(5))
519            .expect("non-zero denominator");
520        assert_eq!(r.num(), &BigInt::ZERO);
521        assert_eq!(r.den(), &BigUint::one());
522    }
523
524    #[test]
525    fn from_parts_div_by_zero() {
526        let err = BigRational::from_parts(BigInt::from(1i64), BigUint::ZERO);
527        assert_eq!(err, Err(OxiNumError::DivByZero));
528    }
529
530    #[test]
531    fn is_integer_predicate() {
532        let i = BigRational::from_i64(7);
533        assert!(i.is_integer());
534        let f = BigRational::from_parts(BigInt::from(3i64), BigUint::from_u64(2))
535            .expect("non-zero denominator");
536        assert!(!f.is_integer());
537    }
538
539    #[test]
540    fn display_integer_form() {
541        let r = BigRational::from_i64(-7);
542        assert_eq!(r.to_string(), "-7");
543    }
544
545    #[test]
546    fn display_fraction_form() {
547        let r = BigRational::from_parts(BigInt::from(22i64), BigUint::from_u64(7))
548            .expect("non-zero denominator");
549        assert_eq!(r.to_string(), "22/7");
550    }
551
552    #[test]
553    fn signum_distinguishes_zero() {
554        assert_eq!(BigRational::zero().signum(), 0);
555        assert_eq!(BigRational::from_i64(5).signum(), 1);
556        assert_eq!(BigRational::from_i64(-5).signum(), -1);
557    }
558
559    #[test]
560    fn recip_of_zero_errors() {
561        assert_eq!(BigRational::zero().recip(), Err(OxiNumError::DivByZero));
562    }
563
564    #[test]
565    fn recip_preserves_sign() {
566        let r = BigRational::from_parts(BigInt::from(-2i64), BigUint::from_u64(3))
567            .expect("non-zero denominator");
568        let recip = r.recip().expect("non-zero source");
569        assert_eq!(recip.to_string(), "-3/2");
570    }
571
572    #[test]
573    fn neg_owned_and_borrowed() {
574        let r = BigRational::from_parts(BigInt::from(3i64), BigUint::from_u64(4))
575            .expect("non-zero denominator");
576        assert_eq!((-&r).to_string(), "-3/4");
577        assert_eq!((-r).to_string(), "-3/4");
578    }
579
580    #[test]
581    fn abs_works() {
582        let r = BigRational::from_parts(BigInt::from(-5i64), BigUint::from_u64(7))
583            .expect("non-zero denominator");
584        let a = r.abs();
585        assert_eq!(a.to_string(), "5/7");
586    }
587
588    #[test]
589    fn from_primitive_signed_and_unsigned() {
590        let a: BigRational = (-3i32).into();
591        let b: BigRational = 7u32.into();
592        assert_eq!(a.to_string(), "-3");
593        assert_eq!(b.to_string(), "7");
594    }
595
596    #[test]
597    fn default_is_zero() {
598        let r = BigRational::default();
599        assert!(r.is_zero());
600        assert_eq!(r.to_string(), "0");
601    }
602}