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: isize,
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: isize, denominator: usize) -> Self {
27        assert!(denominator != 0, "ratio denominator must not be zero");
28        let divisor = gcd(numerator.unsigned_abs(), denominator);
29        let reduced_mag = (numerator.unsigned_abs() / divisor) as isize;
30        Self {
31            numerator: if numerator < 0 {
32                reduced_mag.wrapping_neg()
33            } else {
34                reduced_mag
35            },
36            denominator: denominator / divisor,
37        }
38    }
39
40    /// The reduced numerator.
41    pub fn numerator(self) -> isize {
42        self.numerator
43    }
44
45    /// The reduced denominator.
46    pub fn denominator(self) -> usize {
47        self.denominator
48    }
49
50    /// Convert to an [`f32`].
51    pub fn as_f32(self) -> f32 {
52        self.numerator as f32 / self.denominator as f32
53    }
54
55    /// Convert to an [`f64`].
56    pub fn as_f64(self) -> f64 {
57        self.numerator as f64 / self.denominator as f64
58    }
59}
60
61impl Display for Ratio {
62    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
63        write!(f, "{}/{}", self.numerator, self.denominator)
64    }
65}
66
67fn gcd(a: usize, b: usize) -> usize {
68    let (mut a, mut b) = (a, b);
69    while b != 0 {
70        (a, b) = (b, a % b);
71    }
72    a
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use alloc::format;
79
80    #[test]
81    fn reduces_to_lowest_terms() {
82        let r = Ratio::new(2, 4);
83        assert_eq!(r.numerator(), 1);
84        assert_eq!(r.denominator(), 2);
85    }
86
87    #[test]
88    fn reduces_negative_to_lowest_terms() {
89        let r = Ratio::new(-2, 4);
90        assert_eq!(r.numerator(), -1);
91        assert_eq!(r.denominator(), 2);
92    }
93
94    #[test]
95    fn equal_fractions_are_equal() {
96        assert_eq!(Ratio::new(1, 2), Ratio::new(2, 4));
97        assert_eq!(Ratio::new(3, 9), Ratio::new(1, 3));
98        assert_eq!(Ratio::new(-1, 2), Ratio::new(-2, 4));
99        assert_eq!(Ratio::new(-3, 9), Ratio::new(-1, 3));
100    }
101
102    #[test]
103    fn zero_numerator_reduces_to_zero_over_one() {
104        let r = Ratio::new(0, 5);
105        assert_eq!(r.numerator(), 0);
106        assert_eq!(r.denominator(), 1);
107    }
108
109    #[test]
110    fn handles_isize_min_without_overflow() {
111        let r = Ratio::new(isize::MIN, 2);
112        assert_eq!(r.numerator(), isize::MIN / 2);
113        assert_eq!(r.denominator(), 1);
114
115        let r2 = Ratio::new(isize::MIN, 1);
116        assert_eq!(r2.numerator(), isize::MIN);
117        assert_eq!(r2.denominator(), 1);
118
119        let r3 = Ratio::new(isize::MIN, isize::MIN.unsigned_abs());
120        assert_eq!(r3.numerator(), -1);
121        assert_eq!(r3.denominator(), 1);
122    }
123
124    #[test]
125    #[should_panic(expected = "denominator must not be zero")]
126    fn zero_denominator_panics() {
127        Ratio::new(1, 0);
128    }
129
130    #[test]
131    fn conversions_are_accurate() {
132        let r = Ratio::new(1, 4);
133        assert_eq!(r.as_f32(), 0.25);
134        assert_eq!(r.as_f64(), 0.25);
135
136        let neg_r = Ratio::new(-1, 4);
137        assert_eq!(neg_r.as_f32(), -0.25);
138        assert_eq!(neg_r.as_f64(), -0.25);
139    }
140
141    #[test]
142    fn display_shows_reduced_form() {
143        let r = Ratio::new(6, 8);
144        assert_eq!(format!("{}", r), "3/4");
145
146        let neg_r = Ratio::new(-6, 8);
147        assert_eq!(format!("{}", neg_r), "-3/4");
148    }
149}