cubecl-common 0.11.0-pre.3

Common crate for CubeCL
Documentation
use core::fmt::{Display, Formatter};

/// An exact ratio of two integers, reduced to lowest terms on construction.
///
/// Unlike a float, a `Ratio` compares and hashes exactly: two ratios built
/// from different numerator/denominator pairs are equal whenever they denote
/// the same fraction (`Ratio::new(1, 2) == Ratio::new(2, 4)`), with no
/// rounding or bit-pattern comparison involved. This makes it a good fit for
/// comptime kernel parameters that are always derived from integers (tensor
/// shapes, tile sizes, and the like).
///
/// For a value that is not exactly the ratio of two integers, use
/// [`ComptimeFloat`](crate::ComptimeFloat) instead.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Ratio {
    numerator: isize,
    denominator: usize,
}

impl Ratio {
    /// Create a new [`Ratio`], reduced to lowest terms.
    ///
    /// # Panics
    ///
    /// Panics if `denominator` is zero.
    pub fn new(numerator: isize, denominator: usize) -> Self {
        assert!(denominator != 0, "ratio denominator must not be zero");
        let divisor = gcd(numerator.unsigned_abs(), denominator);
        let reduced_mag = (numerator.unsigned_abs() / divisor) as isize;
        Self {
            numerator: if numerator < 0 {
                reduced_mag.wrapping_neg()
            } else {
                reduced_mag
            },
            denominator: denominator / divisor,
        }
    }

    /// The reduced numerator.
    pub fn numerator(self) -> isize {
        self.numerator
    }

    /// The reduced denominator.
    pub fn denominator(self) -> usize {
        self.denominator
    }

    /// Convert to an [`f32`].
    pub fn as_f32(self) -> f32 {
        self.numerator as f32 / self.denominator as f32
    }

    /// Convert to an [`f64`].
    pub fn as_f64(self) -> f64 {
        self.numerator as f64 / self.denominator as f64
    }
}

impl Display for Ratio {
    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
        write!(f, "{}/{}", self.numerator, self.denominator)
    }
}

fn gcd(a: usize, b: usize) -> usize {
    let (mut a, mut b) = (a, b);
    while b != 0 {
        (a, b) = (b, a % b);
    }
    a
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloc::format;

    #[test]
    fn reduces_to_lowest_terms() {
        let r = Ratio::new(2, 4);
        assert_eq!(r.numerator(), 1);
        assert_eq!(r.denominator(), 2);
    }

    #[test]
    fn reduces_negative_to_lowest_terms() {
        let r = Ratio::new(-2, 4);
        assert_eq!(r.numerator(), -1);
        assert_eq!(r.denominator(), 2);
    }

    #[test]
    fn equal_fractions_are_equal() {
        assert_eq!(Ratio::new(1, 2), Ratio::new(2, 4));
        assert_eq!(Ratio::new(3, 9), Ratio::new(1, 3));
        assert_eq!(Ratio::new(-1, 2), Ratio::new(-2, 4));
        assert_eq!(Ratio::new(-3, 9), Ratio::new(-1, 3));
    }

    #[test]
    fn zero_numerator_reduces_to_zero_over_one() {
        let r = Ratio::new(0, 5);
        assert_eq!(r.numerator(), 0);
        assert_eq!(r.denominator(), 1);
    }

    #[test]
    fn handles_isize_min_without_overflow() {
        let r = Ratio::new(isize::MIN, 2);
        assert_eq!(r.numerator(), isize::MIN / 2);
        assert_eq!(r.denominator(), 1);

        let r2 = Ratio::new(isize::MIN, 1);
        assert_eq!(r2.numerator(), isize::MIN);
        assert_eq!(r2.denominator(), 1);

        let r3 = Ratio::new(isize::MIN, isize::MIN.unsigned_abs());
        assert_eq!(r3.numerator(), -1);
        assert_eq!(r3.denominator(), 1);
    }

    #[test]
    #[should_panic(expected = "denominator must not be zero")]
    fn zero_denominator_panics() {
        Ratio::new(1, 0);
    }

    #[test]
    fn conversions_are_accurate() {
        let r = Ratio::new(1, 4);
        assert_eq!(r.as_f32(), 0.25);
        assert_eq!(r.as_f64(), 0.25);

        let neg_r = Ratio::new(-1, 4);
        assert_eq!(neg_r.as_f32(), -0.25);
        assert_eq!(neg_r.as_f64(), -0.25);
    }

    #[test]
    fn display_shows_reduced_form() {
        let r = Ratio::new(6, 8);
        assert_eq!(format!("{}", r), "3/4");

        let neg_r = Ratio::new(-6, 8);
        assert_eq!(format!("{}", neg_r), "-3/4");
    }
}