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}