Skip to main content

cubecl_common/
ratio.rs

1use core::fmt::{Display, Formatter};
2
3/// An exact ratio of two integers, reduced to lowest terms on construction.
4///
5/// Unlike a float, a `Ratio` compares and hashes exactly: two ratios built
6/// from different numerator/denominator pairs are equal whenever they denote
7/// the same fraction (`Ratio::new(1, 2) == Ratio::new(2, 4)`), with no
8/// rounding or bit-pattern comparison involved. This makes it a good fit for
9/// comptime kernel parameters that are always derived from integers (tensor
10/// shapes, tile sizes, and the like).
11///
12/// For a value that is not exactly the ratio of two integers, use
13/// [`ComptimeFloat`](crate::ComptimeFloat) instead.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub struct Ratio {
16    numerator: usize,
17    denominator: usize,
18}
19
20impl Ratio {
21    /// Create a new [`Ratio`], reduced to lowest terms.
22    ///
23    /// # Panics
24    ///
25    /// Panics if `denominator` is zero.
26    pub fn new(numerator: usize, denominator: usize) -> Self {
27        assert!(denominator != 0, "ratio denominator must not be zero");
28        let divisor = gcd(numerator, denominator);
29        Self {
30            numerator: numerator / divisor,
31            denominator: denominator / divisor,
32        }
33    }
34
35    /// The reduced numerator.
36    pub fn numerator(self) -> usize {
37        self.numerator
38    }
39
40    /// The reduced denominator.
41    pub fn denominator(self) -> usize {
42        self.denominator
43    }
44
45    /// Convert to an [`f32`].
46    pub fn as_f32(self) -> f32 {
47        self.numerator as f32 / self.denominator as f32
48    }
49
50    /// Convert to an [`f64`].
51    pub fn as_f64(self) -> f64 {
52        self.numerator as f64 / self.denominator as f64
53    }
54}
55
56impl Display for Ratio {
57    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
58        write!(f, "{}/{}", self.numerator, self.denominator)
59    }
60}
61
62fn gcd(a: usize, b: usize) -> usize {
63    let (mut a, mut b) = (a, b);
64    while b != 0 {
65        (a, b) = (b, a % b);
66    }
67    a
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use alloc::format;
74
75    #[test]
76    fn reduces_to_lowest_terms() {
77        let r = Ratio::new(2, 4);
78        assert_eq!(r.numerator(), 1);
79        assert_eq!(r.denominator(), 2);
80    }
81
82    #[test]
83    fn equal_fractions_are_equal() {
84        assert_eq!(Ratio::new(1, 2), Ratio::new(2, 4));
85        assert_eq!(Ratio::new(3, 9), Ratio::new(1, 3));
86    }
87
88    #[test]
89    fn zero_numerator_reduces_to_zero_over_one() {
90        let r = Ratio::new(0, 5);
91        assert_eq!(r.numerator(), 0);
92        assert_eq!(r.denominator(), 1);
93    }
94
95    #[test]
96    #[should_panic(expected = "denominator must not be zero")]
97    fn zero_denominator_panics() {
98        Ratio::new(1, 0);
99    }
100
101    #[test]
102    fn conversions_are_accurate() {
103        let r = Ratio::new(1, 4);
104        assert_eq!(r.as_f32(), 0.25);
105        assert_eq!(r.as_f64(), 0.25);
106    }
107
108    #[test]
109    fn display_shows_reduced_form() {
110        let r = Ratio::new(6, 8);
111        assert_eq!(format!("{}", r), "3/4");
112    }
113}