Skip to main content

qualia_core_db/solvers/exact/
rational.rs

1//! Exact rational numbers (`BigRational`) over [`BigInt`].
2//!
3//! A `BigRational` is a `numerator / denominator` pair that is **always** kept:
4//! - reduced to lowest terms via gcd, and
5//! - sign-normalised so the denominator is strictly positive (the sign lives in
6//!   the numerator).
7//!
8//! Construction with a zero denominator **fails closed** (`None`); division by a
9//! zero rational likewise returns `None`. No value is ever fabricated.
10
11use core::cmp::Ordering;
12use core::fmt;
13
14use super::bigint::BigInt;
15
16/// Exact rational number with arbitrary-precision numerator and denominator.
17#[derive(Clone, PartialEq, Eq)]
18pub struct BigRational {
19    num: BigInt,
20    den: BigInt, // invariant: den > 0, gcd(|num|, den) == 1
21}
22
23impl BigRational {
24    /// The rational zero (`0/1`).
25    pub fn zero() -> Self {
26        BigRational {
27            num: BigInt::zero(),
28            den: BigInt::one(),
29        }
30    }
31
32    /// The rational one (`1/1`).
33    pub fn one() -> Self {
34        BigRational {
35            num: BigInt::one(),
36            den: BigInt::one(),
37        }
38    }
39
40    /// Construct from a `BigInt` (`n/1`).
41    pub fn from_bigint(n: BigInt) -> Self {
42        BigRational {
43            num: n,
44            den: BigInt::one(),
45        }
46    }
47
48    /// Construct from an `i64` (`n/1`).
49    pub fn from_i64(n: i64) -> Self {
50        BigRational::from_bigint(BigInt::from_i64(n))
51    }
52
53    /// Construct from a numerator/denominator pair of `i64`. Fails closed on a
54    /// zero denominator.
55    pub fn from_i64s(num: i64, den: i64) -> Option<Self> {
56        Self::new(BigInt::from_i64(num), BigInt::from_i64(den))
57    }
58
59    /// Construct from arbitrary [`BigInt`] numerator and denominator, reducing
60    /// and sign-normalising. Returns `None` if `den` is zero.
61    pub fn new(num: BigInt, den: BigInt) -> Option<Self> {
62        if den.is_zero() {
63            return None; // fail closed
64        }
65        let mut num = num;
66        let mut den = den;
67        // Move the sign onto the numerator; keep denominator positive.
68        if den.is_negative() {
69            num = num.neg();
70            den = den.neg();
71        }
72        if num.is_zero() {
73            return Some(BigRational {
74                num: BigInt::zero(),
75                den: BigInt::one(),
76            });
77        }
78        let g = num.gcd(&den); // non-negative
79        let num = num.div(&g).expect("g non-zero");
80        let den = den.div(&g).expect("g non-zero");
81        Some(BigRational { num, den })
82    }
83
84    /// Numerator (sign-bearing).
85    pub fn numerator(&self) -> &BigInt {
86        &self.num
87    }
88
89    /// Denominator (always positive).
90    pub fn denominator(&self) -> &BigInt {
91        &self.den
92    }
93
94    /// True if this is exactly zero.
95    pub fn is_zero(&self) -> bool {
96        self.num.is_zero()
97    }
98
99    /// Sign: `-1`, `0`, or `+1`.
100    pub fn signum(&self) -> i8 {
101        self.num.signum()
102    }
103
104    /// Absolute value.
105    pub fn abs(&self) -> Self {
106        BigRational {
107            num: self.num.abs(),
108            den: self.den.clone(),
109        }
110    }
111
112    /// Arithmetic negation.
113    pub fn neg(&self) -> Self {
114        BigRational {
115            num: self.num.neg(),
116            den: self.den.clone(),
117        }
118    }
119
120    /// Multiplicative inverse `den/num`. Fails closed if `self` is zero.
121    pub fn recip(&self) -> Option<Self> {
122        if self.is_zero() {
123            return None;
124        }
125        BigRational::new(self.den.clone(), self.num.clone())
126    }
127
128    /// Sum `self + other`. Computes `(a*d + c*b) / (b*d)` then reduces.
129    pub fn add(&self, other: &BigRational) -> BigRational {
130        let num = self.num.mul(&other.den).add(&other.num.mul(&self.den));
131        let den = self.den.mul(&other.den);
132        BigRational::new(num, den).expect("product of positive denominators is non-zero")
133    }
134
135    /// Difference `self - other`.
136    pub fn sub(&self, other: &BigRational) -> BigRational {
137        self.add(&other.neg())
138    }
139
140    /// Product `self * other`.
141    pub fn mul(&self, other: &BigRational) -> BigRational {
142        let num = self.num.mul(&other.num);
143        let den = self.den.mul(&other.den);
144        BigRational::new(num, den).expect("product of positive denominators is non-zero")
145    }
146
147    /// Quotient `self / other`. Fails closed if `other` is zero.
148    pub fn div(&self, other: &BigRational) -> Option<BigRational> {
149        if other.is_zero() {
150            return None;
151        }
152        let num = self.num.mul(&other.den);
153        let den = self.den.mul(&other.num);
154        BigRational::new(num, den)
155    }
156
157    /// Convert to the nearest `f64`. (Exact for small values; rounded otherwise.)
158    pub fn to_f64(&self) -> f64 {
159        // Parse the decimal string round-trips reliably for the magnitudes used
160        // here; for very large values fall back to limb-wise scaling.
161        let n = parse_bigint_f64(&self.num);
162        let d = parse_bigint_f64(&self.den);
163        n / d
164    }
165}
166
167/// Best-effort `BigInt` → `f64`. Uses the decimal rendering, which `f64`'s
168/// `from_str` rounds correctly to nearest.
169fn parse_bigint_f64(b: &BigInt) -> f64 {
170    b.to_string().parse::<f64>().unwrap_or_else(|_| {
171        // Should not happen for decimal output, but never panic in a numeric path.
172        if b.is_negative() {
173            f64::NEG_INFINITY
174        } else {
175            f64::INFINITY
176        }
177    })
178}
179
180impl PartialOrd for BigRational {
181    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
182        Some(self.cmp(other))
183    }
184}
185
186impl Ord for BigRational {
187    fn cmp(&self, other: &Self) -> Ordering {
188        // a/b vs c/d  (b,d > 0)  ⇔  a*d vs c*b
189        let lhs = self.num.mul(&other.den);
190        let rhs = other.num.mul(&self.den);
191        lhs.cmp(&rhs)
192    }
193}
194
195impl fmt::Debug for BigRational {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        write!(f, "BigRational({}/{})", self.num, self.den)
198    }
199}
200
201impl fmt::Display for BigRational {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        if self.den == BigInt::one() {
204            write!(f, "{}", self.num)
205        } else {
206            write!(f, "{}/{}", self.num, self.den)
207        }
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn reduces_on_construction() {
217        // 2/4 → 1/2
218        let r = BigRational::from_i64s(2, 4).unwrap();
219        assert_eq!(r.numerator().to_string(), "1");
220        assert_eq!(r.denominator().to_string(), "2");
221    }
222
223    #[test]
224    fn sign_normalisation() {
225        // 1 / -2 → -1/2 (denominator positive)
226        let r = BigRational::from_i64s(1, -2).unwrap();
227        assert_eq!(r.numerator().to_string(), "-1");
228        assert_eq!(r.denominator().to_string(), "2");
229        // -3 / -6 → 1/2
230        let r2 = BigRational::from_i64s(-3, -6).unwrap();
231        assert_eq!(r2.numerator().to_string(), "1");
232        assert_eq!(r2.denominator().to_string(), "2");
233    }
234
235    #[test]
236    fn third_plus_sixth_is_half() {
237        let a = BigRational::from_i64s(1, 3).unwrap();
238        let b = BigRational::from_i64s(1, 6).unwrap();
239        let s = a.add(&b);
240        assert_eq!(s, BigRational::from_i64s(1, 2).unwrap());
241    }
242
243    #[test]
244    fn third_plus_two_thirds_is_one() {
245        let a = BigRational::from_i64s(1, 3).unwrap();
246        let b = BigRational::from_i64s(2, 3).unwrap();
247        let s = a.add(&b);
248        assert_eq!(s, BigRational::one());
249        assert_eq!(s.numerator().to_string(), "1");
250        assert_eq!(s.denominator().to_string(), "1");
251    }
252
253    #[test]
254    fn sub_mul_div() {
255        let a = BigRational::from_i64s(3, 4).unwrap();
256        let b = BigRational::from_i64s(1, 4).unwrap();
257        assert_eq!(a.sub(&b), BigRational::from_i64s(1, 2).unwrap());
258        assert_eq!(a.mul(&b), BigRational::from_i64s(3, 16).unwrap());
259        assert_eq!(a.div(&b).unwrap(), BigRational::from_i64(3));
260    }
261
262    #[test]
263    fn div_by_zero_fails_closed() {
264        let a = BigRational::from_i64(5);
265        assert!(a.div(&BigRational::zero()).is_none());
266        assert!(BigRational::new(BigInt::from_i64(1), BigInt::zero()).is_none());
267        assert!(BigRational::from_i64s(1, 0).is_none());
268        assert!(BigRational::zero().recip().is_none());
269    }
270
271    #[test]
272    fn ordering() {
273        let third = BigRational::from_i64s(1, 3).unwrap();
274        let half = BigRational::from_i64s(1, 2).unwrap();
275        assert!(third < half);
276        assert!(half.neg() < third);
277        assert_eq!(
278            third.cmp(&BigRational::from_i64s(2, 6).unwrap()),
279            Ordering::Equal
280        );
281    }
282
283    #[test]
284    fn to_f64_values() {
285        assert!((BigRational::from_i64s(1, 2).unwrap().to_f64() - 0.5).abs() < 1e-15);
286        assert!((BigRational::from_i64s(1, 4).unwrap().to_f64() - 0.25).abs() < 1e-15);
287        assert!((BigRational::from_i64s(-3, 4).unwrap().to_f64() + 0.75).abs() < 1e-15);
288    }
289
290    #[test]
291    fn exact_large_arithmetic() {
292        // 1/3 summed three times == 1, exactly (no float drift).
293        let third = BigRational::from_i64s(1, 3).unwrap();
294        let s = third.add(&third).add(&third);
295        assert_eq!(s, BigRational::one());
296    }
297}