1use core::fmt;
2
3use crate::RationalError;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct Rational {
8 numerator: i128,
9 denominator: i128,
10}
11
12impl Rational {
13 #[must_use]
15 pub const fn from_integer(value: i128) -> Self {
16 Self {
17 numerator: value,
18 denominator: 1,
19 }
20 }
21
22 #[must_use]
24 pub const fn zero() -> Self {
25 Self::from_integer(0)
26 }
27
28 #[must_use]
30 pub const fn one() -> Self {
31 Self::from_integer(1)
32 }
33
34 pub fn try_new(numerator: i128, denominator: i128) -> Result<Self, RationalError> {
57 normalize(numerator, denominator)
58 }
59
60 #[must_use]
62 pub const fn numerator(&self) -> i128 {
63 self.numerator
64 }
65
66 #[must_use]
68 pub const fn denominator(&self) -> i128 {
69 self.denominator
70 }
71
72 #[must_use]
74 pub const fn is_integer(&self) -> bool {
75 self.denominator == 1
76 }
77
78 #[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 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 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 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 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 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 #[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}