Skip to main content

oxideav_core/
rational.rs

1//! Rational number used for time bases and frame rates.
2
3use std::fmt;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
6pub struct Rational {
7    pub num: i64,
8    pub den: i64,
9}
10
11impl Rational {
12    pub const fn new(num: i64, den: i64) -> Self {
13        Self { num, den }
14    }
15
16    pub const fn zero() -> Self {
17        Self { num: 0, den: 1 }
18    }
19
20    pub fn is_zero(&self) -> bool {
21        self.num == 0
22    }
23
24    pub fn as_f64(&self) -> f64 {
25        self.num as f64 / self.den as f64
26    }
27
28    /// Reduce the fraction to lowest terms. Sign is normalized onto the numerator.
29    pub fn reduced(mut self) -> Self {
30        if self.den < 0 {
31            self.num = -self.num;
32            self.den = -self.den;
33        }
34        let g = gcd(self.num.unsigned_abs(), self.den.unsigned_abs()) as i64;
35        if g > 1 {
36            self.num /= g;
37            self.den /= g;
38        }
39        self
40    }
41
42    /// Invert the fraction (num/den → den/num).
43    pub fn invert(self) -> Self {
44        Self {
45            num: self.den,
46            den: self.num,
47        }
48    }
49}
50
51impl fmt::Display for Rational {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "{}/{}", self.num, self.den)
54    }
55}
56
57fn gcd(mut a: u64, mut b: u64) -> u64 {
58    while b != 0 {
59        let t = b;
60        b = a % b;
61        a = t;
62    }
63    a.max(1)
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn reduce() {
72        assert_eq!(Rational::new(10, 20).reduced(), Rational::new(1, 2));
73        assert_eq!(Rational::new(-6, 9).reduced(), Rational::new(-2, 3));
74        assert_eq!(Rational::new(6, -9).reduced(), Rational::new(-2, 3));
75    }
76
77    #[test]
78    fn invert() {
79        assert_eq!(Rational::new(1, 2).invert(), Rational::new(2, 1));
80    }
81}