Skip to main content

simplex_trace/
rational.rs

1//! The exact rational number used for every value in the crate.
2
3use std::fmt;
4use std::iter::Sum;
5use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
6use std::str::FromStr;
7
8use num_bigint::BigInt;
9use num_rational::BigRational;
10use num_traits::{One, Signed, ToPrimitive, Zero};
11
12/// An arbitrary-precision exact rational number, always kept in lowest terms.
13///
14/// It supports the usual arithmetic (`+ - * /`, on values and references), comparisons,
15/// [`Display`](fmt::Display) (`3/2`, `-4`, never a decimal) and parsing from the same
16/// notation.
17///
18/// ```
19/// use simplex_trace::{Rational, ratio};
20///
21/// let a = ratio(3, 2);
22/// let b: Rational = "7/4".parse().unwrap();
23/// let c = Rational::from(5);
24/// let d = &a + &b * c;
25/// assert_eq!(d, ratio(41, 4));
26/// assert_eq!(d.to_string(), "41/4");
27/// assert_eq!(d.to_i64_parts(), Some((41, 4)));
28/// assert_eq!(d.to_f64(), 10.25);
29/// ```
30///
31/// The representation is an implementation detail, so the crate can change it (for example
32/// to speed up small numbers) without breaking users. With the `num-interop` feature it
33/// converts to and from [`num_rational::BigRational`].
34///
35/// With the `serde` feature it is a JSON string such as `"3/2"`. JSON numbers are rejected,
36/// so nothing is ever rounded.
37#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub struct Rational(BigRational);
39
40impl Rational {
41    /// The fraction `numer / denom`, reduced. Panics if `denom == 0`.
42    pub fn new(numer: i64, denom: i64) -> Self {
43        assert!(denom != 0, "denominator is zero");
44        Rational(BigRational::new(BigInt::from(numer), BigInt::from(denom)))
45    }
46
47    pub fn zero() -> Self {
48        Rational(BigRational::zero())
49    }
50
51    pub fn one() -> Self {
52        Rational(BigRational::one())
53    }
54
55    pub fn is_zero(&self) -> bool {
56        self.0.is_zero()
57    }
58
59    pub fn is_positive(&self) -> bool {
60        self.0.is_positive()
61    }
62
63    pub fn is_negative(&self) -> bool {
64        self.0.is_negative()
65    }
66
67    /// `true` when the denominator is 1.
68    pub fn is_integer(&self) -> bool {
69        self.0.is_integer()
70    }
71
72    pub fn abs(&self) -> Self {
73        Rational(self.0.abs())
74    }
75
76    /// Numerator and (positive) denominator in lowest terms, if both fit in an `i64`: for
77    /// example to draw a stacked fraction.
78    pub fn to_i64_parts(&self) -> Option<(i64, i64)> {
79        Some((self.0.numer().to_i64()?, self.0.denom().to_i64()?))
80    }
81
82    /// The nearest `f64`, for display or plotting only; computations should stay exact.
83    pub fn to_f64(&self) -> f64 {
84        self.0.to_f64().unwrap_or(f64::NAN)
85    }
86}
87
88impl Default for Rational {
89    fn default() -> Self {
90        Rational::zero()
91    }
92}
93
94impl From<i32> for Rational {
95    fn from(n: i32) -> Self {
96        Rational(BigRational::from_integer(BigInt::from(n)))
97    }
98}
99
100impl From<i64> for Rational {
101    fn from(n: i64) -> Self {
102        Rational(BigRational::from_integer(BigInt::from(n)))
103    }
104}
105
106impl fmt::Display for Rational {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        fmt::Display::fmt(&self.0, f)
109    }
110}
111
112/// Same as [`Display`](fmt::Display): `3/2`.
113impl fmt::Debug for Rational {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        fmt::Display::fmt(&self.0, f)
116    }
117}
118
119/// The error of parsing a [`Rational`] from a string that is not `n` or `n/d` with integers
120/// `n` and `d != 0`.
121#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct ParseRationalError {
123    reason: String,
124}
125
126impl fmt::Display for ParseRationalError {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        f.write_str(&self.reason)
129    }
130}
131
132impl std::error::Error for ParseRationalError {}
133
134/// Parses `n` or `n/d` (integers, `d != 0`), such as `"3"`, `"-1/2"`. Decimals such as
135/// `"0.5"` are rejected: write `"1/2"`.
136impl FromStr for Rational {
137    type Err = ParseRationalError;
138
139    fn from_str(s: &str) -> Result<Self, Self::Err> {
140        s.parse::<BigRational>()
141            .map(Rational)
142            .map_err(|e| ParseRationalError {
143                reason: e.to_string(),
144            })
145    }
146}
147
148macro_rules! binary_op {
149    ($trait:ident, $method:ident, $assign_trait:ident, $assign_method:ident) => {
150        impl $trait<Rational> for Rational {
151            type Output = Rational;
152            fn $method(self, rhs: Rational) -> Rational {
153                Rational(self.0.$method(rhs.0))
154            }
155        }
156        impl $trait<&Rational> for Rational {
157            type Output = Rational;
158            fn $method(self, rhs: &Rational) -> Rational {
159                Rational(self.0.$method(&rhs.0))
160            }
161        }
162        impl $trait<Rational> for &Rational {
163            type Output = Rational;
164            fn $method(self, rhs: Rational) -> Rational {
165                Rational((&self.0).$method(rhs.0))
166            }
167        }
168        impl $trait<&Rational> for &Rational {
169            type Output = Rational;
170            fn $method(self, rhs: &Rational) -> Rational {
171                Rational((&self.0).$method(&rhs.0))
172            }
173        }
174        impl $assign_trait<Rational> for Rational {
175            fn $assign_method(&mut self, rhs: Rational) {
176                self.0.$assign_method(rhs.0);
177            }
178        }
179        impl $assign_trait<&Rational> for Rational {
180            fn $assign_method(&mut self, rhs: &Rational) {
181                self.0.$assign_method(&rhs.0);
182            }
183        }
184    };
185}
186
187binary_op!(Add, add, AddAssign, add_assign);
188binary_op!(Sub, sub, SubAssign, sub_assign);
189binary_op!(Mul, mul, MulAssign, mul_assign);
190// Division by zero panics, as for integers.
191binary_op!(Div, div, DivAssign, div_assign);
192
193impl Neg for Rational {
194    type Output = Rational;
195    fn neg(self) -> Rational {
196        Rational(-self.0)
197    }
198}
199
200impl Neg for &Rational {
201    type Output = Rational;
202    fn neg(self) -> Rational {
203        Rational(-&self.0)
204    }
205}
206
207impl Sum for Rational {
208    fn sum<I: Iterator<Item = Rational>>(iter: I) -> Rational {
209        iter.fold(Rational::zero(), |acc, x| acc + x)
210    }
211}
212
213impl<'a> Sum<&'a Rational> for Rational {
214    fn sum<I: Iterator<Item = &'a Rational>>(iter: I) -> Rational {
215        iter.fold(Rational::zero(), |acc, x| acc + x)
216    }
217}
218
219#[cfg(feature = "num-interop")]
220impl From<BigRational> for Rational {
221    fn from(r: BigRational) -> Self {
222        Rational(r)
223    }
224}
225
226#[cfg(feature = "num-interop")]
227impl From<Rational> for BigRational {
228    fn from(r: Rational) -> Self {
229        r.0
230    }
231}
232
233#[cfg(feature = "num-interop")]
234impl Rational {
235    /// The underlying [`num_rational::BigRational`].
236    pub fn as_big_rational(&self) -> &BigRational {
237        &self.0
238    }
239}
240
241#[cfg(feature = "serde")]
242impl serde::Serialize for Rational {
243    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
244        s.collect_str(self)
245    }
246}
247
248#[cfg(feature = "serde")]
249impl<'de> serde::Deserialize<'de> for Rational {
250    fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
251        let s = String::deserialize(d)?;
252        s.trim()
253            .parse()
254            .map_err(|e| serde::de::Error::custom(format!("invalid rational {s:?}: {e}")))
255    }
256}