Skip to main content

proto_types/common/
fraction.rs

1use core::{cmp::Ordering, fmt::Display};
2
3use thiserror::Error;
4
5use crate::common::Fraction;
6
7impl Display for Fraction {
8	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
9		write!(f, "{}/{}", self.numerator, self.denominator)
10	}
11}
12
13/// Errors that can occur during the creation, conversion or validation of a [`Fraction`].
14#[derive(Debug, Error, PartialEq, Eq, Clone)]
15#[non_exhaustive]
16pub enum FractionError {
17	#[error("Denominator cannot be zero")]
18	ZeroDenominator,
19	#[error("Fraction arithmetic operation resulted in an overflow")]
20	Overflow,
21	#[error("Fraction arithmetic operation resulted in an undefined state")]
22	Undefined,
23}
24
25impl Fraction {
26	/// Helper to calculate Greatest Common Divisor (GCD).
27	///
28	/// Returns `u64` to correctly handle the edge case where the GCD is `i64::MIN.abs()` (2^63),
29	/// which cannot be represented in a positive `i64`.
30	#[must_use]
31	#[inline]
32	pub const fn gcd(a: i64, b: i64) -> u64 {
33		let mut ua = a.unsigned_abs();
34		let mut ub = b.unsigned_abs();
35
36		while ub != 0 {
37			let temp = ub;
38			ub = ua % ub;
39			ua = temp;
40		}
41		ua
42	}
43
44	/// Helper to calculate Least Common Multiple (LCM)
45	#[inline]
46	pub fn lcm(a: i64, b: i64) -> Result<i128, FractionError> {
47		if a == 0 || b == 0 {
48			return Err(FractionError::ZeroDenominator);
49		}
50		let common_divisor = i128::from(Self::gcd(a, b));
51		let val_a = i128::from(a);
52		let val_b = i128::from(b);
53
54		let term1 = val_a
55			.checked_div(common_divisor)
56			.ok_or(FractionError::Overflow)?;
57		term1
58			.checked_mul(val_b)
59			.ok_or(FractionError::Overflow)
60	}
61
62	/// Creates a new Fraction, ensuring the denominator is positive
63	/// and the fraction is reduced to its simplest form.
64	#[inline]
65	pub const fn new(numerator: i64, denominator: i64) -> Result<Self, FractionError> {
66		if denominator == 0 {
67			return Err(FractionError::ZeroDenominator);
68		}
69
70		// Safety Check: i64::MIN cannot be negated to make denominator positive
71		if denominator == i64::MIN {
72			return Err(FractionError::Overflow);
73		}
74		// Safety Check: If denominator is negative, we must flip numerator.
75		// If numerator is i64::MIN, it cannot be flipped.
76		if denominator < 0 && numerator == i64::MIN {
77			return Err(FractionError::Overflow);
78		}
79
80		let (mut num, mut den) = (numerator, denominator);
81
82		// Ensure denominator is positive
83		if den < 0 {
84			num = -num;
85			den = -den;
86		}
87
88		let common_divisor = Self::gcd(num, den);
89
90		// We cast to i64 here.
91		// Since 'den' is positive i64 (checked above), the GCD cannot exceed 'den'.
92		// Therefore common_divisor fits in i64.
93		let cd = common_divisor.cast_signed();
94
95		Ok(Self {
96			numerator: num / cd,
97			denominator: den / cd,
98		})
99	}
100
101	/// Reduces the fraction to its simplest form.
102	///
103	/// # Safety behavior
104	/// If the fraction contains `i64::MIN` in the denominator (which is invalid state),
105	/// or `i64::MIN` in the numerator with a negative denominator, this function
106	/// will **silently return** without modifying the struct, to avoid panicking.
107	#[inline]
108	pub const fn reduce(&mut self) {
109		if self.denominator == 0 {
110			return;
111		}
112
113		// Edge Case Protection:
114		// We cannot normalize signs if values are i64::MIN
115		if self.denominator == i64::MIN {
116			return;
117		}
118		if self.denominator < 0 && self.numerator == i64::MIN {
119			return;
120		}
121
122		// Ensure denominator is positive
123		if self.denominator < 0 {
124			self.numerator = -self.numerator;
125			self.denominator = -self.denominator;
126		}
127
128		let common_divisor = Self::gcd(self.numerator, self.denominator);
129
130		// Since self.denominator is a valid positive i64, the GCD fits in i64.
131		let cd = common_divisor.cast_signed();
132
133		self.numerator /= cd;
134		self.denominator /= cd;
135	}
136
137	/// Returns a new, reduced Fraction.
138	#[must_use]
139	#[inline]
140	pub const fn reduced(mut self) -> Self {
141		self.reduce();
142		self
143	}
144
145	/// Checked addition for [`Fraction`]s.
146	#[inline]
147	pub fn checked_add(self, other: Self) -> Result<Self, FractionError> {
148		let common_denominator_i128 = Self::lcm(self.denominator, other.denominator)?;
149
150		let factor_self = common_denominator_i128
151			.checked_div(i128::from(self.denominator))
152			.ok_or(FractionError::Overflow)?;
153
154		let factor_other = common_denominator_i128
155			.checked_div(i128::from(other.denominator))
156			.ok_or(FractionError::Overflow)?;
157
158		let new_numerator_left = i128::from(self.numerator)
159			.checked_mul(factor_self)
160			.ok_or(FractionError::Overflow)?;
161
162		let new_numerator_right = i128::from(other.numerator)
163			.checked_mul(factor_other)
164			.ok_or(FractionError::Overflow)?;
165
166		let new_numerator = new_numerator_left
167			.checked_add(new_numerator_right)
168			.ok_or(FractionError::Overflow)?;
169
170		let num_i64 = i64::try_from(new_numerator).map_err(|_| FractionError::Overflow)?;
171		let den_i64 =
172			i64::try_from(common_denominator_i128).map_err(|_| FractionError::Overflow)?;
173
174		Self::new(num_i64, den_i64)
175	}
176
177	/// Checked subtraction for [`Fraction`]s.
178	#[inline]
179	pub fn checked_sub(self, other: Self) -> Result<Self, FractionError> {
180		let common_denominator_i128 = Self::lcm(self.denominator, other.denominator)?;
181
182		let factor_self = common_denominator_i128
183			.checked_div(i128::from(self.denominator))
184			.ok_or(FractionError::Overflow)?;
185
186		let factor_other = common_denominator_i128
187			.checked_div(i128::from(other.denominator))
188			.ok_or(FractionError::Overflow)?;
189
190		let new_numerator_left = i128::from(self.numerator)
191			.checked_mul(factor_self)
192			.ok_or(FractionError::Overflow)?;
193
194		let new_numerator_right = i128::from(other.numerator)
195			.checked_mul(factor_other)
196			.ok_or(FractionError::Overflow)?;
197
198		let new_numerator = new_numerator_left
199			.checked_sub(new_numerator_right)
200			.ok_or(FractionError::Overflow)?;
201
202		let num_i64 = i64::try_from(new_numerator).map_err(|_| FractionError::Overflow)?;
203		let den_i64 =
204			i64::try_from(common_denominator_i128).map_err(|_| FractionError::Overflow)?;
205
206		Self::new(num_i64, den_i64)
207	}
208
209	/// Checked multiplication for [`Fraction`]s.
210	#[inline]
211	pub fn checked_mul(self, other: Self) -> Result<Self, FractionError> {
212		let new_numerator = i128::from(self.numerator)
213			.checked_mul(i128::from(other.numerator))
214			.ok_or(FractionError::Overflow)?;
215
216		let new_denominator = i128::from(self.denominator)
217			.checked_mul(i128::from(other.denominator))
218			.ok_or(FractionError::Overflow)?;
219
220		let num_i64 = i64::try_from(new_numerator).map_err(|_| FractionError::Overflow)?;
221		let den_i64 = i64::try_from(new_denominator).map_err(|_| FractionError::Overflow)?;
222
223		Self::new(num_i64, den_i64)
224	}
225
226	/// Checked division for [`Fraction`]s.
227	#[inline]
228	pub fn checked_div(self, other: Self) -> Result<Self, FractionError> {
229		if other.numerator == 0 {
230			return Err(FractionError::Undefined);
231		}
232
233		let new_numerator = i128::from(self.numerator)
234			.checked_mul(i128::from(other.denominator))
235			.ok_or(FractionError::Overflow)?;
236
237		let new_denominator = i128::from(self.denominator)
238			.checked_mul(i128::from(other.numerator))
239			.ok_or(FractionError::Overflow)?;
240
241		let num_i64 = i64::try_from(new_numerator).map_err(|_| FractionError::Overflow)?;
242		let den_i64 = i64::try_from(new_denominator).map_err(|_| FractionError::Overflow)?;
243
244		Self::new(num_i64, den_i64)
245	}
246
247	/// Converts the fraction to an `f64`.
248	///
249	/// # Panics
250	/// Panics if the denominator is zero. This should not happen for [`Fraction`]
251	/// instances created via [`Fraction::new()`] or other checked arithmetic,
252	/// but can occur if a [`Fraction`] is constructed directly in an invalid state.
253	///
254	/// For a fallible conversion that returns a `Result`, use `TryFrom<Fraction> for f64`.
255	#[must_use]
256	#[inline]
257	pub fn to_f64_unchecked(self) -> f64 {
258		self.try_into().unwrap()
259	}
260}
261
262impl PartialOrd for Fraction {
263	#[inline]
264	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
265		if self.denominator <= 0 || other.denominator <= 0 {
266			return None;
267		}
268		let self_val = i128::from(self.numerator) * i128::from(other.denominator);
269		let other_val = i128::from(other.numerator) * i128::from(self.denominator);
270
271		Some(self_val.cmp(&other_val))
272	}
273}
274
275impl TryFrom<Fraction> for f64 {
276	type Error = FractionError;
277	#[inline]
278	fn try_from(fraction: Fraction) -> Result<Self, Self::Error> {
279		if fraction.denominator == 0 {
280			return Err(FractionError::ZeroDenominator);
281		}
282
283		let num_f64 = fraction.numerator as Self;
284		let den_f64 = fraction.denominator as Self;
285
286		Ok(num_f64 / den_f64)
287	}
288}
289
290#[cfg(test)]
291mod tests {
292	use super::*;
293
294	fn frac(n: i64, d: i64) -> Result<Fraction, FractionError> {
295		Fraction::new(n, d)
296	}
297
298	#[test]
299	fn test_creation_and_reduction() {
300		// Standard reduction
301		let f = frac(2, 4).unwrap();
302		assert_eq!(f.numerator, 1);
303		assert_eq!(f.denominator, 2);
304
305		// Negative denominator normalization
306		let f = frac(1, -2).unwrap();
307		assert_eq!(f.numerator, -1);
308		assert_eq!(f.denominator, 2);
309
310		// Double negative
311		let f = frac(-2, -4).unwrap();
312		assert_eq!(f.numerator, 1);
313		assert_eq!(f.denominator, 2);
314
315		// Zero
316		let f = frac(0, 5).unwrap();
317		assert_eq!(f.numerator, 0);
318		assert_eq!(f.denominator, 1); // 0/5 reduces to 0/1
319	}
320
321	#[test]
322	fn test_creation_edge_cases() {
323		// Zero denom
324		assert_eq!(frac(1, 0), Err(FractionError::ZeroDenominator));
325
326		// i64::MIN Numerator (valid if denom positive)
327		let f = frac(i64::MIN, 1).unwrap();
328		assert_eq!(f.numerator, i64::MIN);
329		assert_eq!(f.denominator, 1);
330
331		// i64::MIN Denominator (Invalid, cannot be positive)
332		assert_eq!(frac(1, i64::MIN), Err(FractionError::Overflow));
333
334		// i64::MIN Numerator with Negative Denom (Invalid, needs to flip num sign)
335		assert_eq!(frac(i64::MIN, -1), Err(FractionError::Overflow));
336	}
337
338	#[test]
339	fn test_arithmetic_add() {
340		// 1/2 + 1/3 = 5/6
341		let f1 = frac(1, 2).unwrap();
342		let f2 = frac(1, 3).unwrap();
343		let res = f1.checked_add(f2).unwrap();
344		assert_eq!(res.numerator, 5);
345		assert_eq!(res.denominator, 6);
346
347		// 1/2 + 1/-2 = 0
348		let f1 = frac(1, 2).unwrap();
349		let f2 = frac(1, -2).unwrap();
350		let res = f1.checked_add(f2).unwrap();
351		assert_eq!(res.numerator, 0);
352		assert_eq!(res.denominator, 1);
353	}
354
355	#[test]
356	fn test_arithmetic_sub() {
357		// 1/2 - 1/3 = 1/6
358		let f1 = frac(1, 2).unwrap();
359		let f2 = frac(1, 3).unwrap();
360		let res = f1.checked_sub(f2).unwrap();
361		assert_eq!(res.numerator, 1);
362		assert_eq!(res.denominator, 6);
363	}
364
365	#[test]
366	fn test_arithmetic_mul() {
367		// 2/3 * 3/4 = 6/12 = 1/2
368		let f1 = frac(2, 3).unwrap();
369		let f2 = frac(3, 4).unwrap();
370		let res = f1.checked_mul(f2).unwrap();
371		assert_eq!(res.numerator, 1);
372		assert_eq!(res.denominator, 2);
373	}
374
375	#[test]
376	fn test_arithmetic_div() {
377		// (1/2) / (1/2) = 1
378		let f1 = frac(1, 2).unwrap();
379		let f2 = frac(1, 2).unwrap();
380		let res = f1.checked_div(f2).unwrap();
381		assert_eq!(res.numerator, 1);
382		assert_eq!(res.denominator, 1);
383
384		// Division by zero fraction
385		let f1 = frac(1, 2).unwrap();
386		let f2 = frac(0, 1).unwrap();
387		assert_eq!(f1.checked_div(f2), Err(FractionError::Undefined));
388	}
389
390	#[test]
391	fn test_ordering() {
392		let f1 = frac(1, 2).unwrap();
393		let f2 = frac(1, 3).unwrap();
394		let f3 = frac(2, 4).unwrap(); // Same as 1/2
395
396		assert!(f1 > f2); // 1/2 > 1/3
397		assert!(f2 < f1);
398		assert_eq!(f1.partial_cmp(&f3), Some(Ordering::Equal));
399
400		// Test with negative
401		let neg = frac(-1, 2).unwrap();
402		assert!(neg < f1);
403	}
404
405	#[test]
406	fn test_f64_conversion() {
407		let f = frac(1, 2).unwrap();
408		let val: f64 = f.try_into().unwrap();
409		assert!((val - 0.5).abs() < f64::EPSILON);
410
411		// Test panic/error on raw struct with 0 denom
412		let bad_frac = Fraction {
413			numerator: 1,
414			denominator: 0,
415		};
416		assert_eq!(f64::try_from(bad_frac), Err(FractionError::ZeroDenominator));
417	}
418
419	#[test]
420	fn test_overflow_checks() {
421		// Adding two huge fractions that overflow i64 but fit in i128 during calc,
422		// but the result numerator overflows i64.
423		let f1 = frac(i64::MAX - 1, 1).unwrap();
424		let f2 = frac(2, 1).unwrap();
425
426		// (MAX-1) + 2 = MAX + 1 -> Overflow i64
427		assert_eq!(f1.checked_add(f2), Err(FractionError::Overflow));
428	}
429
430	#[test]
431	fn test_gcd_edge_cases() {
432		// Standard
433		assert_eq!(Fraction::gcd(10, 5), 5);
434		assert_eq!(Fraction::gcd(-10, 5), 5);
435
436		// The "Impossible" GCD
437		// gcd(i64::MIN, 0) = 2^63. This fits in u64, but not i64.
438		assert_eq!(Fraction::gcd(i64::MIN, 0), 9_223_372_036_854_775_808);
439	}
440
441	#[test]
442	fn test_manual_corruption_resilience() {
443		// 1. Manually set Denominator to i64::MIN
444		let mut f = Fraction {
445			numerator: 1,
446			denominator: i64::MIN,
447		};
448		// Should not panic
449		f.reduce();
450		// Should remain unchanged (because it returned early)
451		assert_eq!(f.denominator, i64::MIN);
452
453		// 2. Manually set Num=MIN, Denom=-1
454		let mut f2 = Fraction {
455			numerator: i64::MIN,
456			denominator: -1,
457		};
458		// Normalizing would require flipping Num to +MIN (impossible).
459		// Should not panic.
460		f2.reduce();
461		assert_eq!(f2.denominator, -1);
462	}
463
464	#[test]
465	fn test_reduce_works_normally() {
466		let mut f = Fraction {
467			numerator: 2,
468			denominator: 4,
469		};
470		f.reduce();
471		assert_eq!(f.numerator, 1);
472		assert_eq!(f.denominator, 2);
473
474		let mut f2 = Fraction {
475			numerator: 2,
476			denominator: -4,
477		};
478		f2.reduce();
479		assert_eq!(f2.numerator, -1);
480		assert_eq!(f2.denominator, 2);
481	}
482}