vsrg 0.6.0

Data structures for vertical scrolling rhythm games
Documentation
use std::cmp::Ordering;

use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};

use super::Fraction;

/// A number represented either exactly as a [`Fraction`] or approximately as an `f64`.
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Number {
    Fraction(Fraction),
    Float(f64),
}

impl Number {
    /// Converts this number to an `f64`.
    pub fn to_f64(self) -> f64 {
        match self {
            Self::Fraction(value) => value.into(),
            Self::Float(value) => value,
        }
    }

    /// Returns whether this number is NaN.
    ///
    /// Since a fraction can't be NaN, this always returns false for [`Self::Fraction`] variant.
    pub fn is_nan(self) -> bool {
        matches!(self, Self::Float(value) if value.is_nan())
    }

    /// Adds two numbers, returning `None` on fraction overflow or a NaN result.
    pub fn checked_add(self, rhs: Self) -> Option<Self> {
        match (self, rhs) {
            (Self::Fraction(left), Self::Fraction(right)) => {
                left.checked_add(right).map(Self::Fraction)
            }
            (left, right) => Self::checked_float(left.to_f64() + right.to_f64()),
        }
    }

    /// Subtracts two numbers, returning `None` on fraction overflow or a NaN result.
    pub fn checked_sub(self, rhs: Self) -> Option<Self> {
        match (self, rhs) {
            (Self::Fraction(left), Self::Fraction(right)) => {
                left.checked_sub(right).map(Self::Fraction)
            }
            (left, right) => Self::checked_float(left.to_f64() - right.to_f64()),
        }
    }

    /// Multiplies two numbers, returning `None` on fraction overflow or a NaN result.
    pub fn checked_mul(self, rhs: Self) -> Option<Self> {
        match (self, rhs) {
            (Self::Fraction(left), Self::Fraction(right)) => {
                left.checked_mul(right).map(Self::Fraction)
            }
            (left, right) => Self::checked_float(left.to_f64() * right.to_f64()),
        }
    }

    /// Divides two numbers, returning `None` on fraction overflow, division by zero, or a NaN result.
    pub fn checked_div(self, rhs: Self) -> Option<Self> {
        match (self, rhs) {
            (Self::Fraction(left), Self::Fraction(right)) => {
                left.checked_div(right).map(Self::Fraction)
            }
            (left, right) => Self::checked_float(left.to_f64() / right.to_f64()),
        }
    }

    fn checked_float(value: f64) -> Option<Self> {
        (!value.is_nan()).then_some(Self::Float(value))
    }
}

impl Default for Number {
    fn default() -> Self {
        Self::Float(0.0)
    }
}

impl From<f64> for Number {
    fn from(value: f64) -> Self {
        Self::Float(value)
    }
}

impl From<Fraction> for Number {
    fn from(value: Fraction) -> Self {
        Self::Fraction(value)
    }
}

impl From<i32> for Number {
    fn from(value: i32) -> Self {
        Self::Fraction(Fraction::from(value))
    }
}

impl PartialEq for Number {
    fn eq(&self, other: &Self) -> bool {
        self.partial_cmp(other) == Some(Ordering::Equal)
    }
}

impl PartialOrd for Number {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        match (*self, *other) {
            (Self::Fraction(left), Self::Fraction(right)) => left.partial_cmp(&right),
            (Self::Fraction(left), Self::Float(right)) => left.partial_cmp_f64(right),
            (Self::Float(left), Self::Fraction(right)) => {
                right.partial_cmp_f64(left).map(Ordering::reverse)
            }
            (Self::Float(left), Self::Float(right)) => left.partial_cmp(&right),
        }
    }
}

impl Add for Number {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        match self.checked_add(rhs) {
            Some(result) => result,
            None if cfg!(debug_assertions) => {
                panic!("attempt to add numbers with overflow or NaN result")
            }
            None => Self::Float(self.to_f64() + rhs.to_f64()),
        }
    }
}

impl AddAssign for Number {
    fn add_assign(&mut self, rhs: Self) {
        *self = *self + rhs;
    }
}

impl Sub for Number {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self::Output {
        match self.checked_sub(rhs) {
            Some(result) => result,
            None if cfg!(debug_assertions) => {
                panic!("attempt to subtract numbers with overflow or NaN result")
            }
            None => Self::Float(self.to_f64() - rhs.to_f64()),
        }
    }
}

impl SubAssign for Number {
    fn sub_assign(&mut self, rhs: Self) {
        *self = *self - rhs;
    }
}

impl Mul for Number {
    type Output = Self;

    fn mul(self, rhs: Self) -> Self::Output {
        match self.checked_mul(rhs) {
            Some(result) => result,
            None if cfg!(debug_assertions) => {
                panic!("attempt to multiply numbers with overflow or NaN result")
            }
            None => Self::Float(self.to_f64() * rhs.to_f64()),
        }
    }
}

impl MulAssign for Number {
    fn mul_assign(&mut self, rhs: Self) {
        *self = *self * rhs;
    }
}

impl Div for Number {
    type Output = Self;

    fn div(self, rhs: Self) -> Self::Output {
        match self.checked_div(rhs) {
            Some(result) => result,
            None if cfg!(debug_assertions) => {
                panic!("attempt to divide numbers by zero, with overflow, or with NaN result")
            }
            None => Self::Float(self.to_f64() / rhs.to_f64()),
        }
    }
}

impl DivAssign for Number {
    fn div_assign(&mut self, rhs: Self) {
        *self = *self / rhs;
    }
}

impl Neg for Number {
    type Output = Self;

    fn neg(self) -> Self::Output {
        match self {
            Self::Fraction(f) => Self::Fraction(f.neg()),
            Self::Float(f) => Self::Float(-f),
        }
    }
}

impl std::fmt::Display for Number {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Number::Fraction(x) => x.fmt(f),
            Number::Float(x) => x.fmt(f),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Number;
    use crate::math::Fraction;

    fn fraction(numerator: i32, denominator: i32) -> Number {
        Number::Fraction(Fraction::new(numerator, denominator).unwrap())
    }

    #[test]
    fn converts_to_f64() {
        assert_eq!(fraction(1, 4).to_f64(), 0.25);
        assert_eq!(Number::from(0.5).to_f64(), 0.5);
    }

    #[test]
    fn checked_arithmetic_preserves_fraction_results() {
        assert_eq!(
            fraction(1, 2).checked_add(fraction(1, 3)),
            Some(fraction(5, 6))
        );
        assert_eq!(
            fraction(1, 2).checked_sub(fraction(1, 3)),
            Some(fraction(1, 6))
        );
        assert_eq!(
            fraction(2, 3).checked_mul(fraction(3, 4)),
            Some(fraction(1, 2))
        );
        assert_eq!(
            fraction(2, 3).checked_div(fraction(4, 3)),
            Some(fraction(1, 2))
        );
    }

    #[test]
    fn mixed_arithmetic_uses_float_representation() {
        let result = fraction(1, 2).checked_add(Number::from(0.25)).unwrap();
        assert!(matches!(result, Number::Float(0.75)));

        let mut assigned = fraction(1, 2);
        assigned += fraction(1, 2);
        assert_eq!(assigned, fraction(1, 1));
    }

    #[test]
    fn checked_arithmetic_rejects_overflow_zero_division_and_nan() {
        assert_eq!(fraction(i32::MAX, 1).checked_add(fraction(1, 1)), None);
        assert_eq!(fraction(1, 1).checked_div(fraction(0, 1)), None);
        assert_eq!(
            Number::from(f64::INFINITY).checked_sub(Number::from(f64::INFINITY)),
            None
        );
        assert_eq!(
            Number::from(0.0).checked_mul(Number::from(f64::INFINITY)),
            None
        );
    }

    #[test]
    fn compares_fractions_with_floats_without_rounding_them_equal() {
        let tenth = fraction(1, 10);
        let float_tenth = Number::from(0.1);

        assert_eq!(
            tenth.partial_cmp(&float_tenth),
            Some(std::cmp::Ordering::Less)
        );
        assert_ne!(tenth, float_tenth);
        assert_eq!(fraction(1, 2), Number::from(0.5));
    }

    #[test]
    fn compares_fraction_with_adjacent_floats() {
        let tenth = fraction(1, 10);
        let float_tenth = 0.1_f64;

        assert_eq!(
            tenth.partial_cmp(&Number::from(float_tenth.next_down())),
            Some(std::cmp::Ordering::Greater)
        );
        assert_eq!(
            tenth.partial_cmp(&Number::from(float_tenth.next_up())),
            Some(std::cmp::Ordering::Less)
        );
    }

    #[test]
    fn compares_negative_fractions_and_signed_zero() {
        let negative_tenth = fraction(-1, 10);
        let negative_float_tenth = Number::from(-0.1);

        assert_eq!(
            negative_tenth.partial_cmp(&negative_float_tenth),
            Some(std::cmp::Ordering::Greater)
        );
        assert_eq!(
            negative_float_tenth.partial_cmp(&negative_tenth),
            Some(std::cmp::Ordering::Less)
        );
        assert_eq!(fraction(0, 1), Number::from(-0.0));
    }

    #[test]
    fn compares_fractions_with_infinities_and_nan() {
        let half = fraction(1, 2);

        assert_eq!(
            half.partial_cmp(&Number::from(f64::INFINITY)),
            Some(std::cmp::Ordering::Less)
        );
        assert_eq!(
            half.partial_cmp(&Number::from(f64::NEG_INFINITY)),
            Some(std::cmp::Ordering::Greater)
        );
        assert_eq!(half.partial_cmp(&Number::from(f64::NAN)), None);
        assert_ne!(half, Number::from(f64::NAN));
    }

    #[test]
    fn checked_float_arithmetic_allows_infinity() {
        assert_eq!(
            Number::from(f64::MAX).checked_mul(Number::from(2.0)),
            Some(Number::from(f64::INFINITY))
        );
    }

    #[test]
    #[cfg(debug_assertions)]
    #[should_panic(expected = "attempt to add numbers")]
    fn operator_panics_when_checked_operation_fails_in_debug_mode() {
        let _ = fraction(i32::MAX, 1) + fraction(1, 1);
    }

    #[test]
    #[cfg(not(debug_assertions))]
    fn operator_dont_panics_when_checked_operation_fails_in_release_mode() {
        let _ = fraction(i32::MAX, 1) + fraction(1, 1);
    }
}