Skip to main content

use_rational/
rational.rs

1use core::fmt;
2
3use crate::RationalError;
4
5/// A normalized exact rational number.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct Rational {
8    numerator: i128,
9    denominator: i128,
10}
11
12impl Rational {
13    /// Returns the exact integer `value / 1`.
14    #[must_use]
15    pub const fn from_integer(value: i128) -> Self {
16        Self {
17            numerator: value,
18            denominator: 1,
19        }
20    }
21
22    /// Returns `0 / 1`.
23    #[must_use]
24    pub const fn zero() -> Self {
25        Self::from_integer(0)
26    }
27
28    /// Returns `1 / 1`.
29    #[must_use]
30    pub const fn one() -> Self {
31        Self::from_integer(1)
32    }
33
34    /// Creates a normalized rational number from a numerator and denominator.
35    ///
36    /// # Errors
37    ///
38    /// Returns [`RationalError::ZeroDenominator`] when `denominator == 0`, and
39    /// returns overflow-related errors when canonical sign normalization cannot
40    /// be represented in the current integer type.
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// use use_rational::{Rational, RationalError};
46    ///
47    /// let rational = Rational::try_new(2, 4)?;
48    /// assert_eq!(rational, Rational::try_new(1, 2)?);
49    ///
50    /// assert!(matches!(
51    ///     Rational::try_new(1, 0),
52    ///     Err(RationalError::ZeroDenominator)
53    /// ));
54    /// # Ok::<(), RationalError>(())
55    /// ```
56    pub fn try_new(numerator: i128, denominator: i128) -> Result<Self, RationalError> {
57        normalize(numerator, denominator)
58    }
59
60    /// Returns the normalized numerator.
61    #[must_use]
62    pub const fn numerator(&self) -> i128 {
63        self.numerator
64    }
65
66    /// Returns the normalized positive denominator.
67    #[must_use]
68    pub const fn denominator(&self) -> i128 {
69        self.denominator
70    }
71
72    /// Returns `true` when the rational is an integer.
73    #[must_use]
74    pub const fn is_integer(&self) -> bool {
75        self.denominator == 1
76    }
77
78    /// Returns the exact integer value when the rational is integral.
79    #[must_use]
80    pub const fn to_integer(self) -> Option<i128> {
81        if self.is_integer() {
82            Some(self.numerator)
83        } else {
84            None
85        }
86    }
87
88    /// Returns the reciprocal when the rational is non-zero.
89    ///
90    /// # Errors
91    ///
92    /// Returns [`RationalError::DivisionByZero`] when `self == 0`.
93    pub fn reciprocal(self) -> Result<Self, RationalError> {
94        if self.numerator == 0 {
95            return Err(RationalError::DivisionByZero);
96        }
97
98        normalize(self.denominator, self.numerator)
99    }
100
101    /// Adds two rational numbers exactly.
102    ///
103    /// # Errors
104    ///
105    /// Returns [`RationalError::ArithmeticOverflow`] when the intermediate or
106    /// normalized result cannot be represented exactly.
107    pub fn checked_add(self, other: Self) -> Result<Self, RationalError> {
108        let left = self.numerator.checked_mul(other.denominator).ok_or(
109            RationalError::ArithmeticOverflow {
110                operation: "addition",
111            },
112        )?;
113        let right = other.numerator.checked_mul(self.denominator).ok_or(
114            RationalError::ArithmeticOverflow {
115                operation: "addition",
116            },
117        )?;
118        let numerator = left
119            .checked_add(right)
120            .ok_or(RationalError::ArithmeticOverflow {
121                operation: "addition",
122            })?;
123        let denominator = self.denominator.checked_mul(other.denominator).ok_or(
124            RationalError::ArithmeticOverflow {
125                operation: "addition",
126            },
127        )?;
128
129        normalize(numerator, denominator)
130    }
131
132    /// Subtracts two rational numbers exactly.
133    ///
134    /// # Errors
135    ///
136    /// Returns [`RationalError::ArithmeticOverflow`] when the intermediate or
137    /// normalized result cannot be represented exactly.
138    pub fn checked_sub(self, other: Self) -> Result<Self, RationalError> {
139        let left = self.numerator.checked_mul(other.denominator).ok_or(
140            RationalError::ArithmeticOverflow {
141                operation: "subtraction",
142            },
143        )?;
144        let right = other.numerator.checked_mul(self.denominator).ok_or(
145            RationalError::ArithmeticOverflow {
146                operation: "subtraction",
147            },
148        )?;
149        let numerator = left
150            .checked_sub(right)
151            .ok_or(RationalError::ArithmeticOverflow {
152                operation: "subtraction",
153            })?;
154        let denominator = self.denominator.checked_mul(other.denominator).ok_or(
155            RationalError::ArithmeticOverflow {
156                operation: "subtraction",
157            },
158        )?;
159
160        normalize(numerator, denominator)
161    }
162
163    /// Multiplies two rational numbers exactly.
164    ///
165    /// # Errors
166    ///
167    /// Returns [`RationalError::ArithmeticOverflow`] when the intermediate or
168    /// normalized result cannot be represented exactly.
169    pub fn checked_mul(self, other: Self) -> Result<Self, RationalError> {
170        let numerator = self.numerator.checked_mul(other.numerator).ok_or(
171            RationalError::ArithmeticOverflow {
172                operation: "multiplication",
173            },
174        )?;
175        let denominator = self.denominator.checked_mul(other.denominator).ok_or(
176            RationalError::ArithmeticOverflow {
177                operation: "multiplication",
178            },
179        )?;
180
181        normalize(numerator, denominator)
182    }
183
184    /// Divides two rational numbers exactly.
185    ///
186    /// # Errors
187    ///
188    /// Returns [`RationalError::DivisionByZero`] when `other == 0`, and returns
189    /// [`RationalError::ArithmeticOverflow`] when the exact result cannot be
190    /// represented.
191    pub fn checked_div(self, other: Self) -> Result<Self, RationalError> {
192        if other.numerator == 0 {
193            return Err(RationalError::DivisionByZero);
194        }
195
196        let numerator = self.numerator.checked_mul(other.denominator).ok_or(
197            RationalError::ArithmeticOverflow {
198                operation: "division",
199            },
200        )?;
201        let denominator = self.denominator.checked_mul(other.numerator).ok_or(
202            RationalError::ArithmeticOverflow {
203                operation: "division",
204            },
205        )?;
206
207        normalize(numerator, denominator)
208    }
209
210    /// Returns the rational as an approximate `f64`.
211    #[must_use]
212    #[allow(clippy::cast_precision_loss)]
213    pub fn as_f64(&self) -> f64 {
214        self.numerator as f64 / self.denominator as f64
215    }
216}
217
218impl fmt::Display for Rational {
219    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
220        if self.denominator == 1 {
221            write!(formatter, "{}", self.numerator)
222        } else {
223            write!(formatter, "{}/{}", self.numerator, self.denominator)
224        }
225    }
226}
227
228fn normalize(numerator: i128, denominator: i128) -> Result<Rational, RationalError> {
229    if denominator == 0 {
230        return Err(RationalError::ZeroDenominator);
231    }
232
233    if numerator == 0 {
234        return Ok(Rational::zero());
235    }
236
237    let mut numerator = numerator;
238    let mut denominator = denominator;
239
240    if denominator < 0 {
241        numerator = numerator
242            .checked_neg()
243            .ok_or(RationalError::NormalizationOverflow)?;
244        denominator = denominator
245            .checked_neg()
246            .ok_or(RationalError::NormalizationOverflow)?;
247    }
248
249    let divisor = gcd_u128(numerator.unsigned_abs(), denominator.cast_unsigned());
250    let divisor = i128::try_from(divisor).map_err(|_| RationalError::NormalizationOverflow)?;
251
252    Ok(Rational {
253        numerator: numerator / divisor,
254        denominator: denominator / divisor,
255    })
256}
257
258const fn gcd_u128(mut left: u128, mut right: u128) -> u128 {
259    while right != 0 {
260        let remainder = left % right;
261        left = right;
262        right = remainder;
263    }
264
265    left
266}
267
268#[cfg(test)]
269mod tests {
270    use super::Rational;
271    use crate::RationalError;
272
273    fn assert_close(left: f64, right: f64, tolerance: f64) {
274        assert!(
275            (left - right).abs() <= tolerance,
276            "expected {left} to be within {tolerance} of {right}"
277        );
278    }
279
280    #[test]
281    fn normalizes_signs_and_reduces_values() -> Result<(), RationalError> {
282        assert_eq!(Rational::try_new(2, 4)?, Rational::try_new(1, 2)?);
283        assert_eq!(Rational::try_new(3, -9)?, Rational::try_new(-1, 3)?);
284        assert_eq!(Rational::try_new(-3, -9)?, Rational::try_new(1, 3)?);
285
286        Ok(())
287    }
288
289    #[test]
290    fn exposes_integer_and_zero_helpers() {
291        assert_eq!(Rational::zero(), Rational::from_integer(0));
292        assert_eq!(Rational::one(), Rational::from_integer(1));
293        assert!(Rational::from_integer(7).is_integer());
294        assert_eq!(Rational::from_integer(7).to_integer(), Some(7));
295    }
296
297    #[test]
298    fn rejects_zero_denominators() {
299        assert!(matches!(
300            Rational::try_new(1, 0),
301            Err(RationalError::ZeroDenominator)
302        ));
303    }
304
305    #[test]
306    fn computes_checked_arithmetic() -> Result<(), RationalError> {
307        let half = Rational::try_new(1, 2)?;
308        let third = Rational::try_new(1, 3)?;
309
310        assert_eq!(half.checked_add(third)?, Rational::try_new(5, 6)?);
311        assert_eq!(half.checked_sub(third)?, Rational::try_new(1, 6)?);
312        assert_eq!(half.checked_mul(third)?, Rational::try_new(1, 6)?);
313        assert_eq!(half.checked_div(third)?, Rational::try_new(3, 2)?);
314
315        Ok(())
316    }
317
318    #[test]
319    fn rejects_division_by_zero() -> Result<(), RationalError> {
320        let half = Rational::try_new(1, 2)?;
321
322        assert!(matches!(
323            half.checked_div(Rational::zero()),
324            Err(RationalError::DivisionByZero)
325        ));
326        assert!(matches!(
327            Rational::zero().reciprocal(),
328            Err(RationalError::DivisionByZero)
329        ));
330
331        Ok(())
332    }
333
334    #[test]
335    fn converts_to_f64_explicitly() -> Result<(), RationalError> {
336        let rational = Rational::try_new(5, 6)?;
337
338        assert_close(rational.as_f64(), 5.0 / 6.0, 1.0e-12);
339        Ok(())
340    }
341}