Skip to main content

ff_format/time/
rational.rs

1//! [`Rational`] number type for representing fractions.
2
3// These casts are intentional for media timestamp arithmetic.
4// The values involved (PTS, time bases, frame rates) are well within
5// the safe ranges for these conversions in practical video/audio scenarios.
6#![allow(
7    clippy::cast_possible_truncation,
8    clippy::cast_possible_wrap,
9    clippy::cast_precision_loss,
10    clippy::cast_sign_loss
11)]
12
13use std::cmp::Ordering;
14use std::fmt;
15use std::ops::{Add, Div, Mul, Neg, Sub};
16
17/// A rational number represented as a fraction (numerator / denominator).
18///
19/// This type is commonly used to represent:
20/// - Time bases (e.g., 1/90000 for MPEG-TS, 1/1000 for milliseconds)
21/// - Frame rates (e.g., 30000/1001 for 29.97 fps)
22/// - Aspect ratios (e.g., 16/9)
23///
24/// # Invariants
25///
26/// - Denominator is always positive (sign is in numerator)
27/// - Zero denominator is handled gracefully (returns infinity/NaN for conversions)
28///
29/// # Examples
30///
31/// ```
32/// use ff_format::Rational;
33///
34/// // Common time base for MPEG-TS
35/// let time_base = Rational::new(1, 90000);
36///
37/// // 29.97 fps (NTSC)
38/// let fps = Rational::new(30000, 1001);
39/// assert!((fps.as_f64() - 29.97).abs() < 0.01);
40///
41/// // Invert to get frame duration
42/// let frame_duration = fps.invert();
43/// assert_eq!(frame_duration.num(), 1001);
44/// assert_eq!(frame_duration.den(), 30000);
45/// ```
46#[derive(Debug, Clone, Copy)]
47pub struct Rational {
48    num: i32,
49    den: i32,
50}
51
52impl PartialEq for Rational {
53    fn eq(&self, other: &Self) -> bool {
54        // a/b == c/d iff a*d == b*c (cross-multiplication)
55        // Use i64 to avoid overflow
56        i64::from(self.num) * i64::from(other.den) == i64::from(other.num) * i64::from(self.den)
57    }
58}
59
60impl Eq for Rational {}
61
62impl std::hash::Hash for Rational {
63    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
64        // Hash the reduced form to ensure equal values have equal hashes
65        let reduced = self.reduce();
66        reduced.num.hash(state);
67        reduced.den.hash(state);
68    }
69}
70
71impl Rational {
72    /// Creates a new rational number.
73    ///
74    /// The denominator is normalized to always be positive (the sign is moved
75    /// to the numerator).
76    ///
77    /// # Panics
78    ///
79    /// Does not panic. A zero denominator is allowed but will result in
80    /// infinity or NaN when converted to floating-point.
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use ff_format::Rational;
86    ///
87    /// let r = Rational::new(1, 2);
88    /// assert_eq!(r.num(), 1);
89    /// assert_eq!(r.den(), 2);
90    ///
91    /// // Negative denominator is normalized
92    /// let r = Rational::new(1, -2);
93    /// assert_eq!(r.num(), -1);
94    /// assert_eq!(r.den(), 2);
95    /// ```
96    #[must_use]
97    pub const fn new(num: i32, den: i32) -> Self {
98        // Normalize: denominator should always be positive
99        if den < 0 {
100            Self {
101                num: -num,
102                den: -den,
103            }
104        } else {
105            Self { num, den }
106        }
107    }
108
109    /// Creates a rational number representing zero (0/1).
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use ff_format::Rational;
115    ///
116    /// let zero = Rational::zero();
117    /// assert_eq!(zero.as_f64(), 0.0);
118    /// assert!(zero.is_zero());
119    /// ```
120    #[must_use]
121    pub const fn zero() -> Self {
122        Self { num: 0, den: 1 }
123    }
124
125    /// Creates a rational number representing one (1/1).
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use ff_format::Rational;
131    ///
132    /// let one = Rational::one();
133    /// assert_eq!(one.as_f64(), 1.0);
134    /// ```
135    #[must_use]
136    pub const fn one() -> Self {
137        Self { num: 1, den: 1 }
138    }
139
140    /// Returns the numerator.
141    ///
142    /// # Examples
143    ///
144    /// ```
145    /// use ff_format::Rational;
146    ///
147    /// let r = Rational::new(3, 4);
148    /// assert_eq!(r.num(), 3);
149    /// ```
150    #[must_use]
151    #[inline]
152    pub const fn num(&self) -> i32 {
153        self.num
154    }
155
156    /// Returns the denominator.
157    ///
158    /// The denominator is always non-negative.
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use ff_format::Rational;
164    ///
165    /// let r = Rational::new(3, 4);
166    /// assert_eq!(r.den(), 4);
167    /// ```
168    #[must_use]
169    #[inline]
170    pub const fn den(&self) -> i32 {
171        self.den
172    }
173
174    /// Converts the rational number to a floating-point value.
175    ///
176    /// Returns `f64::INFINITY`, `f64::NEG_INFINITY`, or `f64::NAN` for
177    /// edge cases (division by zero).
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use ff_format::Rational;
183    ///
184    /// let r = Rational::new(1, 4);
185    /// assert_eq!(r.as_f64(), 0.25);
186    ///
187    /// let r = Rational::new(1, 3);
188    /// assert!((r.as_f64() - 0.333333).abs() < 0.001);
189    /// ```
190    #[must_use]
191    #[inline]
192    pub fn as_f64(self) -> f64 {
193        if self.den == 0 {
194            match self.num.cmp(&0) {
195                Ordering::Greater => f64::INFINITY,
196                Ordering::Less => f64::NEG_INFINITY,
197                Ordering::Equal => f64::NAN,
198            }
199        } else {
200            f64::from(self.num) / f64::from(self.den)
201        }
202    }
203
204    /// Converts the rational number to a single-precision floating-point value.
205    ///
206    /// # Examples
207    ///
208    /// ```
209    /// use ff_format::Rational;
210    ///
211    /// let r = Rational::new(1, 2);
212    /// assert_eq!(r.as_f32(), 0.5);
213    /// ```
214    #[must_use]
215    #[inline]
216    pub fn as_f32(self) -> f32 {
217        self.as_f64() as f32
218    }
219
220    /// Returns the inverse (reciprocal) of this rational number.
221    ///
222    /// # Examples
223    ///
224    /// ```
225    /// use ff_format::Rational;
226    ///
227    /// let r = Rational::new(3, 4);
228    /// let inv = r.invert();
229    /// assert_eq!(inv.num(), 4);
230    /// assert_eq!(inv.den(), 3);
231    ///
232    /// // Negative values
233    /// let r = Rational::new(-3, 4);
234    /// let inv = r.invert();
235    /// assert_eq!(inv.num(), -4);
236    /// assert_eq!(inv.den(), 3);
237    /// ```
238    #[must_use]
239    pub const fn invert(self) -> Self {
240        // Handle sign normalization when inverting
241        if self.num < 0 {
242            Self {
243                num: -self.den,
244                den: -self.num,
245            }
246        } else {
247            Self {
248                num: self.den,
249                den: self.num,
250            }
251        }
252    }
253
254    /// Returns true if this rational number is zero.
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use ff_format::Rational;
260    ///
261    /// assert!(Rational::new(0, 1).is_zero());
262    /// assert!(Rational::new(0, 100).is_zero());
263    /// assert!(!Rational::new(1, 100).is_zero());
264    /// ```
265    #[must_use]
266    #[inline]
267    pub const fn is_zero(self) -> bool {
268        self.num == 0
269    }
270
271    /// Returns true if this rational number is positive.
272    ///
273    /// # Examples
274    ///
275    /// ```
276    /// use ff_format::Rational;
277    ///
278    /// assert!(Rational::new(1, 2).is_positive());
279    /// assert!(!Rational::new(-1, 2).is_positive());
280    /// assert!(!Rational::new(0, 1).is_positive());
281    /// ```
282    #[must_use]
283    #[inline]
284    pub const fn is_positive(self) -> bool {
285        self.num > 0 && self.den > 0
286    }
287
288    /// Returns true if this rational number is negative.
289    ///
290    /// # Examples
291    ///
292    /// ```
293    /// use ff_format::Rational;
294    ///
295    /// assert!(Rational::new(-1, 2).is_negative());
296    /// assert!(!Rational::new(1, 2).is_negative());
297    /// assert!(!Rational::new(0, 1).is_negative());
298    /// ```
299    #[must_use]
300    #[inline]
301    pub const fn is_negative(self) -> bool {
302        self.num < 0 && self.den > 0
303    }
304
305    /// Returns the absolute value of this rational number.
306    ///
307    /// # Examples
308    ///
309    /// ```
310    /// use ff_format::Rational;
311    ///
312    /// assert_eq!(Rational::new(-3, 4).abs(), Rational::new(3, 4));
313    /// assert_eq!(Rational::new(3, 4).abs(), Rational::new(3, 4));
314    /// ```
315    #[must_use]
316    pub const fn abs(self) -> Self {
317        Self {
318            num: if self.num < 0 { -self.num } else { self.num },
319            den: self.den,
320        }
321    }
322
323    /// Reduces the rational to its lowest terms using GCD.
324    ///
325    /// # Examples
326    ///
327    /// ```
328    /// use ff_format::Rational;
329    ///
330    /// let r = Rational::new(4, 8);
331    /// let reduced = r.reduce();
332    /// assert_eq!(reduced.num(), 1);
333    /// assert_eq!(reduced.den(), 2);
334    /// ```
335    #[must_use]
336    pub fn reduce(self) -> Self {
337        if self.num == 0 {
338            return Self::new(0, 1);
339        }
340        let g = gcd(self.num.unsigned_abs(), self.den.unsigned_abs());
341        Self {
342            num: self.num / g as i32,
343            den: self.den / g as i32,
344        }
345    }
346}
347
348/// Computes the greatest common divisor using Euclidean algorithm.
349fn gcd(mut a: u32, mut b: u32) -> u32 {
350    while b != 0 {
351        let temp = b;
352        b = a % b;
353        a = temp;
354    }
355    a
356}
357
358impl Default for Rational {
359    /// Returns the default rational number (1/1).
360    fn default() -> Self {
361        Self::one()
362    }
363}
364
365impl fmt::Display for Rational {
366    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367        write!(f, "{}/{}", self.num, self.den)
368    }
369}
370
371impl From<i32> for Rational {
372    fn from(n: i32) -> Self {
373        Self::new(n, 1)
374    }
375}
376
377impl From<(i32, i32)> for Rational {
378    fn from((num, den): (i32, i32)) -> Self {
379        Self::new(num, den)
380    }
381}
382
383// Arithmetic operations for Rational
384
385impl Add for Rational {
386    type Output = Self;
387
388    fn add(self, rhs: Self) -> Self::Output {
389        // a/b + c/d = (ad + bc) / bd
390        let num =
391            i64::from(self.num) * i64::from(rhs.den) + i64::from(rhs.num) * i64::from(self.den);
392        let den = i64::from(self.den) * i64::from(rhs.den);
393
394        // Try to reduce to fit in i32
395        let g = gcd(num.unsigned_abs() as u32, den.unsigned_abs() as u32);
396        Self::new((num / i64::from(g)) as i32, (den / i64::from(g)) as i32)
397    }
398}
399
400impl Sub for Rational {
401    type Output = Self;
402
403    fn sub(self, rhs: Self) -> Self::Output {
404        // a/b - c/d = (ad - bc) / bd
405        let num =
406            i64::from(self.num) * i64::from(rhs.den) - i64::from(rhs.num) * i64::from(self.den);
407        let den = i64::from(self.den) * i64::from(rhs.den);
408
409        let g = gcd(num.unsigned_abs() as u32, den.unsigned_abs() as u32);
410        Self::new((num / i64::from(g)) as i32, (den / i64::from(g)) as i32)
411    }
412}
413
414impl Mul for Rational {
415    type Output = Self;
416
417    fn mul(self, rhs: Self) -> Self::Output {
418        // a/b * c/d = ac / bd
419        let num = i64::from(self.num) * i64::from(rhs.num);
420        let den = i64::from(self.den) * i64::from(rhs.den);
421
422        let g = gcd(num.unsigned_abs() as u32, den.unsigned_abs() as u32);
423        Self::new((num / i64::from(g)) as i32, (den / i64::from(g)) as i32)
424    }
425}
426
427impl Div for Rational {
428    type Output = Self;
429
430    #[allow(clippy::suspicious_arithmetic_impl)]
431    fn div(self, rhs: Self) -> Self::Output {
432        // a/b / c/d = a/b * d/c = ad / bc
433        // Using multiplication by inverse is mathematically correct for rational division
434        self * rhs.invert()
435    }
436}
437
438impl Mul<i32> for Rational {
439    type Output = Self;
440
441    fn mul(self, rhs: i32) -> Self::Output {
442        let num = i64::from(self.num) * i64::from(rhs);
443        let g = gcd(num.unsigned_abs() as u32, self.den.unsigned_abs());
444        Self::new((num / i64::from(g)) as i32, self.den / g as i32)
445    }
446}
447
448impl Div<i32> for Rational {
449    type Output = Self;
450
451    fn div(self, rhs: i32) -> Self::Output {
452        let den = i64::from(self.den) * i64::from(rhs);
453        let g = gcd(self.num.unsigned_abs(), den.unsigned_abs() as u32);
454        Self::new(self.num / g as i32, (den / i64::from(g)) as i32)
455    }
456}
457
458impl Neg for Rational {
459    type Output = Self;
460
461    fn neg(self) -> Self::Output {
462        Self::new(-self.num, self.den)
463    }
464}
465
466impl PartialOrd for Rational {
467    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
468        Some(self.cmp(other))
469    }
470}
471
472impl Ord for Rational {
473    fn cmp(&self, other: &Self) -> Ordering {
474        // Compare a/b with c/d by comparing ad with bc
475        let left = i64::from(self.num) * i64::from(other.den);
476        let right = i64::from(other.num) * i64::from(self.den);
477        left.cmp(&right)
478    }
479}
480
481#[cfg(test)]
482#[allow(
483    clippy::unwrap_used,
484    clippy::float_cmp,
485    clippy::similar_names,
486    clippy::redundant_closure_for_method_calls
487)]
488mod tests {
489    use super::*;
490
491    /// Helper for approximate float comparison in tests
492    fn approx_eq(a: f64, b: f64) -> bool {
493        (a - b).abs() < 1e-9
494    }
495
496    mod rational_tests {
497        use super::*;
498
499        #[test]
500        fn test_new() {
501            let r = Rational::new(1, 2);
502            assert_eq!(r.num(), 1);
503            assert_eq!(r.den(), 2);
504        }
505
506        #[test]
507        fn test_new_negative_denominator() {
508            // Negative denominator should be normalized
509            let r = Rational::new(1, -2);
510            assert_eq!(r.num(), -1);
511            assert_eq!(r.den(), 2);
512
513            let r = Rational::new(-1, -2);
514            assert_eq!(r.num(), 1);
515            assert_eq!(r.den(), 2);
516        }
517
518        #[test]
519        fn test_zero_and_one() {
520            let zero = Rational::zero();
521            assert!(zero.is_zero());
522            assert!(approx_eq(zero.as_f64(), 0.0));
523
524            let one = Rational::one();
525            assert!(approx_eq(one.as_f64(), 1.0));
526            assert!(!one.is_zero());
527        }
528
529        #[test]
530        fn test_as_f64() {
531            assert!(approx_eq(Rational::new(1, 2).as_f64(), 0.5));
532            assert!(approx_eq(Rational::new(1, 4).as_f64(), 0.25));
533            assert!((Rational::new(1, 3).as_f64() - 0.333_333).abs() < 0.001);
534            assert!(approx_eq(Rational::new(-1, 2).as_f64(), -0.5));
535        }
536
537        #[test]
538        fn test_as_f64_division_by_zero() {
539            assert!(Rational::new(1, 0).as_f64().is_infinite());
540            assert!(Rational::new(1, 0).as_f64().is_sign_positive());
541            assert!(Rational::new(-1, 0).as_f64().is_infinite());
542            assert!(Rational::new(-1, 0).as_f64().is_sign_negative());
543            assert!(Rational::new(0, 0).as_f64().is_nan());
544        }
545
546        #[test]
547        fn test_as_f32() {
548            assert_eq!(Rational::new(1, 2).as_f32(), 0.5);
549        }
550
551        #[test]
552        fn test_invert() {
553            let r = Rational::new(3, 4);
554            let inv = r.invert();
555            assert_eq!(inv.num(), 4);
556            assert_eq!(inv.den(), 3);
557
558            // Negative value
559            let r = Rational::new(-3, 4);
560            let inv = r.invert();
561            assert_eq!(inv.num(), -4);
562            assert_eq!(inv.den(), 3);
563        }
564
565        #[test]
566        fn test_is_positive_negative() {
567            assert!(Rational::new(1, 2).is_positive());
568            assert!(!Rational::new(-1, 2).is_positive());
569            assert!(!Rational::new(0, 1).is_positive());
570
571            assert!(Rational::new(-1, 2).is_negative());
572            assert!(!Rational::new(1, 2).is_negative());
573            assert!(!Rational::new(0, 1).is_negative());
574        }
575
576        #[test]
577        fn test_abs() {
578            assert_eq!(Rational::new(-3, 4).abs(), Rational::new(3, 4));
579            assert_eq!(Rational::new(3, 4).abs(), Rational::new(3, 4));
580            assert_eq!(Rational::new(0, 4).abs(), Rational::new(0, 4));
581        }
582
583        #[test]
584        fn test_reduce() {
585            let r = Rational::new(4, 8);
586            let reduced = r.reduce();
587            assert_eq!(reduced.num(), 1);
588            assert_eq!(reduced.den(), 2);
589
590            let r = Rational::new(6, 9);
591            let reduced = r.reduce();
592            assert_eq!(reduced.num(), 2);
593            assert_eq!(reduced.den(), 3);
594
595            let r = Rational::new(0, 5);
596            let reduced = r.reduce();
597            assert_eq!(reduced.num(), 0);
598            assert_eq!(reduced.den(), 1);
599        }
600
601        #[test]
602        fn test_add() {
603            let a = Rational::new(1, 2);
604            let b = Rational::new(1, 4);
605            let result = a + b;
606            assert!((result.as_f64() - 0.75).abs() < 0.0001);
607        }
608
609        #[test]
610        fn test_sub() {
611            let a = Rational::new(1, 2);
612            let b = Rational::new(1, 4);
613            let result = a - b;
614            assert!((result.as_f64() - 0.25).abs() < 0.0001);
615        }
616
617        #[test]
618        fn test_mul() {
619            let a = Rational::new(1, 2);
620            let b = Rational::new(2, 3);
621            let result = a * b;
622            assert!((result.as_f64() - (1.0 / 3.0)).abs() < 0.0001);
623        }
624
625        #[test]
626        fn test_div() {
627            let a = Rational::new(1, 2);
628            let b = Rational::new(2, 3);
629            let result = a / b;
630            assert!((result.as_f64() - 0.75).abs() < 0.0001);
631        }
632
633        #[test]
634        fn test_mul_i32() {
635            let r = Rational::new(1, 4);
636            let result = r * 2;
637            assert!((result.as_f64() - 0.5).abs() < 0.0001);
638        }
639
640        #[test]
641        fn test_div_i32() {
642            let r = Rational::new(1, 2);
643            let result = r / 2;
644            assert!((result.as_f64() - 0.25).abs() < 0.0001);
645        }
646
647        #[test]
648        fn test_neg() {
649            let r = Rational::new(1, 2);
650            let neg = -r;
651            assert_eq!(neg.num(), -1);
652            assert_eq!(neg.den(), 2);
653        }
654
655        #[test]
656        fn test_ord() {
657            let a = Rational::new(1, 2);
658            let b = Rational::new(1, 3);
659            let c = Rational::new(2, 4);
660
661            assert!(a > b);
662            assert!(b < a);
663            assert_eq!(a, c);
664            assert!(a >= c);
665            assert!(a <= c);
666        }
667
668        #[test]
669        fn test_from_i32() {
670            let r: Rational = 5.into();
671            assert_eq!(r.num(), 5);
672            assert_eq!(r.den(), 1);
673        }
674
675        #[test]
676        fn test_from_tuple() {
677            let r: Rational = (3, 4).into();
678            assert_eq!(r.num(), 3);
679            assert_eq!(r.den(), 4);
680        }
681
682        #[test]
683        fn test_display() {
684            assert_eq!(format!("{}", Rational::new(1, 2)), "1/2");
685            assert_eq!(format!("{}", Rational::new(-3, 4)), "-3/4");
686        }
687
688        #[test]
689        fn test_default() {
690            assert_eq!(Rational::default(), Rational::one());
691        }
692
693        #[test]
694        fn test_common_frame_rates() {
695            // 23.976 fps (film)
696            let fps = Rational::new(24000, 1001);
697            assert!((fps.as_f64() - 23.976).abs() < 0.001);
698
699            // 29.97 fps (NTSC)
700            let fps = Rational::new(30000, 1001);
701            assert!((fps.as_f64() - 29.97).abs() < 0.01);
702
703            // 59.94 fps (NTSC interlaced as progressive)
704            let fps = Rational::new(60000, 1001);
705            assert!((fps.as_f64() - 59.94).abs() < 0.01);
706        }
707    }
708
709    // ==================== GCD Tests ====================
710
711    #[test]
712    fn test_gcd() {
713        assert_eq!(gcd(12, 8), 4);
714        assert_eq!(gcd(17, 13), 1);
715        assert_eq!(gcd(100, 25), 25);
716        assert_eq!(gcd(0, 5), 5);
717        assert_eq!(gcd(5, 0), 5);
718    }
719}