use std::cmp::Ordering;
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
use super::Fraction;
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Number {
Fraction(Fraction),
Float(f64),
}
impl Number {
pub fn to_f64(self) -> f64 {
match self {
Self::Fraction(value) => value.into(),
Self::Float(value) => value,
}
}
pub fn is_nan(self) -> bool {
matches!(self, Self::Float(value) if value.is_nan())
}
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()),
}
}
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()),
}
}
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()),
}
}
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);
}
}