From 622d42c445621ecd8c08de7decc1adbc5cb0828c Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 2 Aug 2026 22:58:15 -0400 Subject: [PATCH 1/4] Fix mixed arithmetic rounding --- src/number.rs | 136 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 93 insertions(+), 43 deletions(-) diff --git a/src/number.rs b/src/number.rs index ce3c74f..e6f96be 100644 --- a/src/number.rs +++ b/src/number.rs @@ -22,17 +22,19 @@ impl Number { #[must_use] pub fn add(&self, rhs: &Self, config: Config) -> Self { - if let (Self::Exact(lhs), Self::Exact(rhs)) = (self, rhs) { - Self::Exact((lhs + rhs).complete()) - } else { - Self::Approx( - Float::with_val_round( - config.precision(), - &self.to_float(config) + &rhs.to_float(config), - config.rounding_mode, - ) - .0, - ) + match (self, rhs) { + (Self::Approx(lhs), Self::Approx(rhs)) => { + Self::rounded_approx(lhs + rhs, config) + } + (Self::Approx(lhs), Self::Exact(rhs)) => { + Self::rounded_approx(lhs + rhs, config) + } + (Self::Exact(lhs), Self::Approx(rhs)) => { + Self::rounded_approx(lhs + rhs, config) + } + (Self::Exact(lhs), Self::Exact(rhs)) => { + Self::Exact((lhs + rhs).complete()) + } } } @@ -125,17 +127,21 @@ impl Number { ) -> std::result::Result { if rhs.is_zero() { Err(Error::DivisionByZero) - } else if let (Self::Exact(lhs), Self::Exact(rhs)) = (self, rhs) { - Ok(Self::Exact((lhs / rhs).complete())) } else { - Ok(Self::Approx( - Float::with_val_round( - config.precision(), - &self.to_float(config) / &rhs.to_float(config), - config.rounding_mode, - ) - .0, - )) + Ok(match (self, rhs) { + (Self::Approx(lhs), Self::Approx(rhs)) => { + Self::rounded_approx(lhs / rhs, config) + } + (Self::Approx(lhs), Self::Exact(rhs)) => { + Self::rounded_approx(lhs / rhs, config) + } + (Self::Exact(lhs), Self::Approx(rhs)) => { + Self::rounded_approx(lhs / rhs, config) + } + (Self::Exact(lhs), Self::Exact(rhs)) => { + Self::Exact((lhs / rhs).complete()) + } + }) } } @@ -192,17 +198,19 @@ impl Number { #[must_use] pub fn mul(&self, rhs: &Self, config: Config) -> Self { - if let (Self::Exact(lhs), Self::Exact(rhs)) = (self, rhs) { - Self::Exact((lhs * rhs).complete()) - } else { - Self::Approx( - Float::with_val_round( - config.precision(), - &self.to_float(config) * &rhs.to_float(config), - config.rounding_mode, - ) - .0, - ) + match (self, rhs) { + (Self::Approx(lhs), Self::Approx(rhs)) => { + Self::rounded_approx(lhs * rhs, config) + } + (Self::Approx(lhs), Self::Exact(rhs)) => { + Self::rounded_approx(lhs * rhs, config) + } + (Self::Exact(lhs), Self::Approx(rhs)) => { + Self::rounded_approx(lhs * rhs, config) + } + (Self::Exact(lhs), Self::Exact(rhs)) => { + Self::Exact((lhs * rhs).complete()) + } } } @@ -256,6 +264,15 @@ impl Number { Ok(self.sub(&self.div(rhs, config)?.floor().mul(rhs, config), config)) } + fn rounded_approx(value: T, config: Config) -> Self + where + Float: rug::ops::AssignRound, + { + Self::Approx( + Float::with_val_round(config.precision(), value, config.rounding_mode).0, + ) + } + #[must_use] pub fn sin(&self, config: Config) -> Self { self.approx_unary(config, Float::sin_round) @@ -287,17 +304,19 @@ impl Number { #[must_use] pub fn sub(&self, rhs: &Self, config: Config) -> Self { - if let (Self::Exact(lhs), Self::Exact(rhs)) = (self, rhs) { - Self::Exact((lhs - rhs).complete()) - } else { - Self::Approx( - Float::with_val_round( - config.precision(), - &self.to_float(config) - &rhs.to_float(config), - config.rounding_mode, - ) - .0, - ) + match (self, rhs) { + (Self::Approx(lhs), Self::Approx(rhs)) => { + Self::rounded_approx(lhs - rhs, config) + } + (Self::Approx(lhs), Self::Exact(rhs)) => { + Self::rounded_approx(lhs - rhs, config) + } + (Self::Exact(lhs), Self::Approx(rhs)) => { + Self::rounded_approx(lhs - rhs, config) + } + (Self::Exact(lhs), Self::Exact(rhs)) => { + Self::Exact((lhs - rhs).complete()) + } } } @@ -605,6 +624,37 @@ mod tests { assert!(greater > approx); } + #[test] + fn mixed_exact_approx_operations_round_once() { + #[track_caller] + fn case(actual: &Number, expected: f64) { + let expected = Number::Approx(Float::with_val(2, expected)); + assert_eq!(actual, &expected); + } + + let config = Config { + precision: 2, + ..Config::default() + }; + + let approx = |value| Number::Approx(Float::with_val(2, value)); + let exact = |numerator, denominator| { + Number::Exact(Rational::from((numerator, denominator))) + }; + + case(&approx(1.0).add(&exact(13, 50), config), 1.5); + case(&exact(13, 50).add(&approx(1.0), config), 1.5); + + case(&approx(1.0).sub(&exact(9, 25), config), 0.75); + case(&exact(9, 25).sub(&approx(1.0), config), -0.75); + + case(&approx(1.5).mul(&exact(21, 50), config), 0.75); + case(&exact(21, 50).mul(&approx(1.5), config), 0.75); + + case(&approx(1.0).div(&exact(13, 8), config).unwrap(), 0.5); + case(&exact(9, 10).div(&approx(1.5), config).unwrap(), 0.5); + } + #[test] fn undefined_exact_arithmetic_returns_error() { let zero = Number::from(0_i64); From 56a13090417f5d31de1cadc1854d75e297ede3b2 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 2 Aug 2026 23:01:37 -0400 Subject: [PATCH 2/4] Inline approximate rounding --- src/number.rs | 141 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 96 insertions(+), 45 deletions(-) diff --git a/src/number.rs b/src/number.rs index e6f96be..281c2b1 100644 --- a/src/number.rs +++ b/src/number.rs @@ -23,15 +23,30 @@ impl Number { #[must_use] pub fn add(&self, rhs: &Self, config: Config) -> Self { match (self, rhs) { - (Self::Approx(lhs), Self::Approx(rhs)) => { - Self::rounded_approx(lhs + rhs, config) - } - (Self::Approx(lhs), Self::Exact(rhs)) => { - Self::rounded_approx(lhs + rhs, config) - } - (Self::Exact(lhs), Self::Approx(rhs)) => { - Self::rounded_approx(lhs + rhs, config) - } + (Self::Approx(lhs), Self::Approx(rhs)) => Self::Approx( + Float::with_val_round( + config.precision(), + lhs + rhs, + config.rounding_mode, + ) + .0, + ), + (Self::Approx(lhs), Self::Exact(rhs)) => Self::Approx( + Float::with_val_round( + config.precision(), + lhs + rhs, + config.rounding_mode, + ) + .0, + ), + (Self::Exact(lhs), Self::Approx(rhs)) => Self::Approx( + Float::with_val_round( + config.precision(), + lhs + rhs, + config.rounding_mode, + ) + .0, + ), (Self::Exact(lhs), Self::Exact(rhs)) => { Self::Exact((lhs + rhs).complete()) } @@ -129,15 +144,30 @@ impl Number { Err(Error::DivisionByZero) } else { Ok(match (self, rhs) { - (Self::Approx(lhs), Self::Approx(rhs)) => { - Self::rounded_approx(lhs / rhs, config) - } - (Self::Approx(lhs), Self::Exact(rhs)) => { - Self::rounded_approx(lhs / rhs, config) - } - (Self::Exact(lhs), Self::Approx(rhs)) => { - Self::rounded_approx(lhs / rhs, config) - } + (Self::Approx(lhs), Self::Approx(rhs)) => Self::Approx( + Float::with_val_round( + config.precision(), + lhs / rhs, + config.rounding_mode, + ) + .0, + ), + (Self::Approx(lhs), Self::Exact(rhs)) => Self::Approx( + Float::with_val_round( + config.precision(), + lhs / rhs, + config.rounding_mode, + ) + .0, + ), + (Self::Exact(lhs), Self::Approx(rhs)) => Self::Approx( + Float::with_val_round( + config.precision(), + lhs / rhs, + config.rounding_mode, + ) + .0, + ), (Self::Exact(lhs), Self::Exact(rhs)) => { Self::Exact((lhs / rhs).complete()) } @@ -199,15 +229,30 @@ impl Number { #[must_use] pub fn mul(&self, rhs: &Self, config: Config) -> Self { match (self, rhs) { - (Self::Approx(lhs), Self::Approx(rhs)) => { - Self::rounded_approx(lhs * rhs, config) - } - (Self::Approx(lhs), Self::Exact(rhs)) => { - Self::rounded_approx(lhs * rhs, config) - } - (Self::Exact(lhs), Self::Approx(rhs)) => { - Self::rounded_approx(lhs * rhs, config) - } + (Self::Approx(lhs), Self::Approx(rhs)) => Self::Approx( + Float::with_val_round( + config.precision(), + lhs * rhs, + config.rounding_mode, + ) + .0, + ), + (Self::Approx(lhs), Self::Exact(rhs)) => Self::Approx( + Float::with_val_round( + config.precision(), + lhs * rhs, + config.rounding_mode, + ) + .0, + ), + (Self::Exact(lhs), Self::Approx(rhs)) => Self::Approx( + Float::with_val_round( + config.precision(), + lhs * rhs, + config.rounding_mode, + ) + .0, + ), (Self::Exact(lhs), Self::Exact(rhs)) => { Self::Exact((lhs * rhs).complete()) } @@ -264,15 +309,6 @@ impl Number { Ok(self.sub(&self.div(rhs, config)?.floor().mul(rhs, config), config)) } - fn rounded_approx(value: T, config: Config) -> Self - where - Float: rug::ops::AssignRound, - { - Self::Approx( - Float::with_val_round(config.precision(), value, config.rounding_mode).0, - ) - } - #[must_use] pub fn sin(&self, config: Config) -> Self { self.approx_unary(config, Float::sin_round) @@ -305,15 +341,30 @@ impl Number { #[must_use] pub fn sub(&self, rhs: &Self, config: Config) -> Self { match (self, rhs) { - (Self::Approx(lhs), Self::Approx(rhs)) => { - Self::rounded_approx(lhs - rhs, config) - } - (Self::Approx(lhs), Self::Exact(rhs)) => { - Self::rounded_approx(lhs - rhs, config) - } - (Self::Exact(lhs), Self::Approx(rhs)) => { - Self::rounded_approx(lhs - rhs, config) - } + (Self::Approx(lhs), Self::Approx(rhs)) => Self::Approx( + Float::with_val_round( + config.precision(), + lhs - rhs, + config.rounding_mode, + ) + .0, + ), + (Self::Approx(lhs), Self::Exact(rhs)) => Self::Approx( + Float::with_val_round( + config.precision(), + lhs - rhs, + config.rounding_mode, + ) + .0, + ), + (Self::Exact(lhs), Self::Approx(rhs)) => Self::Approx( + Float::with_val_round( + config.precision(), + lhs - rhs, + config.rounding_mode, + ) + .0, + ), (Self::Exact(lhs), Self::Exact(rhs)) => { Self::Exact((lhs - rhs).complete()) } From c9350d190d20115675733704f5c241e988e038ad Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 2 Aug 2026 23:02:56 -0400 Subject: [PATCH 3/4] Separate test closures --- src/number.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/number.rs b/src/number.rs index 281c2b1..c285ce5 100644 --- a/src/number.rs +++ b/src/number.rs @@ -689,6 +689,7 @@ mod tests { }; let approx = |value| Number::Approx(Float::with_val(2, value)); + let exact = |numerator, denominator| { Number::Exact(Rational::from((numerator, denominator))) }; From 11f93290c0a7573ece33aa09230febdd88724878 Mon Sep 17 00:00:00 2001 From: Liam Date: Sun, 2 Aug 2026 23:10:48 -0400 Subject: [PATCH 4/4] Refine mixed arithmetic handling --- src/number.rs | 52 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/src/number.rs b/src/number.rs index 773a2a5..6d85db0 100644 --- a/src/number.rs +++ b/src/number.rs @@ -31,15 +31,8 @@ impl Number { ) .0, ), - (Self::Approx(lhs), Self::Exact(rhs)) => Self::Approx( - Float::with_val_round( - config.precision(), - lhs + rhs, - config.rounding_mode, - ) - .0, - ), - (Self::Exact(lhs), Self::Approx(rhs)) => Self::Approx( + (Self::Approx(lhs), Self::Exact(rhs)) + | (Self::Exact(rhs), Self::Approx(lhs)) => Self::Approx( Float::with_val_round( config.precision(), lhs + rhs, @@ -237,15 +230,8 @@ impl Number { ) .0, ), - (Self::Approx(lhs), Self::Exact(rhs)) => Self::Approx( - Float::with_val_round( - config.precision(), - lhs * rhs, - config.rounding_mode, - ) - .0, - ), - (Self::Exact(lhs), Self::Approx(rhs)) => Self::Approx( + (Self::Approx(lhs), Self::Exact(rhs)) + | (Self::Exact(rhs), Self::Approx(lhs)) => Self::Approx( Float::with_val_round( config.precision(), lhs * rhs, @@ -444,10 +430,10 @@ impl Number { .0, ), (Self::Exact(lhs), Self::Approx(rhs)) => Self::Approx( - Float::with_val_round( + -Float::with_val_round( config.precision(), - lhs - rhs, - config.rounding_mode, + rhs - lhs, + config.rounding_mode.reverse(), ) .0, ), @@ -833,6 +819,30 @@ mod tests { case(&exact(9, 10).div(&approx(1.5), config).unwrap(), 0.5); } + #[test] + fn mixed_exact_approx_subtraction_signed_zero() { + #[track_caller] + fn case(rounding_mode: Round, negative: bool) { + let config = Config { + precision: 2, + rounding_mode, + ..Config::default() + }; + + let Number::Approx(result) = + Number::from(1_i64).sub(&Number::Approx(Float::with_val(2, 1)), config) + else { + panic!("expected approximate number"); + }; + + assert!(result.is_zero()); + assert_eq!(result.is_sign_negative(), negative); + } + + case(Round::Down, true); + case(Round::Up, false); + } + #[test] fn undefined_exact_arithmetic_returns_error() { let zero = Number::from(0_i64);