From eebf756368d5f8a08d31c72a9ce67b145077161b Mon Sep 17 00:00:00 2001 From: Louis David Date: Mon, 18 Aug 2025 22:49:44 +0200 Subject: [PATCH 1/2] Implements num_traits::real::Real for NotNan when T is Real and FloatCore Forwards the calls of Real trait methods to the underlying type, panicking when the result is NaN, as suggested in https://docs.rs/num-traits/latest/num_traits/real/trait.Real.html Also adds a few tests to ensure things actually panic when they should. --- src/lib.rs | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++ tests/test.rs | 128 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index f3f5b44..ace61ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ use core::ops::{ use core::str::FromStr; pub use num_traits::float::FloatCore; +use num_traits::real::Real; use num_traits::{ AsPrimitive, Bounded, FloatConst, FromPrimitive, Num, NumCast, One, Signed, ToPrimitive, Zero, }; @@ -1957,6 +1958,170 @@ impl NumCast for NotNan { } } +#[cfg(any(feature = "std", feature = "libm"))] +impl Real for NotNan { + fn min_value() -> Self { + NotNan(::min_value()) + } + fn min_positive_value() -> Self { + NotNan(::min_positive_value()) + } + fn epsilon() -> Self { + NotNan(Real::epsilon()) + } + fn max_value() -> Self { + NotNan(::max_value()) + } + fn floor(self) -> Self { + NotNan(Real::floor(self.0)) + } + fn ceil(self) -> Self { + NotNan(Real::ceil(self.0)) + } + fn round(self) -> Self { + NotNan(Real::round(self.0)) + } + fn trunc(self) -> Self { + NotNan(Real::trunc(self.0)) + } + fn fract(self) -> Self { + NotNan(Real::fract(self.0)) + } + fn abs(self) -> Self { + NotNan(Real::abs(self.0)) + } + fn signum(self) -> Self { + NotNan(Real::signum(self.0)) + } + fn is_sign_positive(self) -> bool { + Real::is_sign_positive(self.0) + } + fn is_sign_negative(self) -> bool { + Real::is_sign_negative(self.0) + } + fn mul_add(self, a: Self, b: Self) -> Self { + NotNan(self.0.mul_add(a.0, b.0)) + } + fn recip(self) -> Self { + NotNan(Real::recip(self.0)) + } + fn powi(self, n: i32) -> Self { + NotNan(Real::powi(self.0, n)) + } + fn powf(self, n: Self) -> Self { + // Panics if self < 0 and n is not an integer + NotNan::new(self.0.powf(n.0)).expect("Power resulted in NaN") + } + fn sqrt(self) -> Self { + // Panics if self < 0 + NotNan::new(self.0.sqrt()).expect("Square root resulted in NaN") + } + fn exp(self) -> Self { + NotNan(self.0.exp()) + } + fn exp2(self) -> Self { + NotNan(self.0.exp2()) + } + fn ln(self) -> Self { + // Panics if self <= 0 + NotNan::new(self.0.ln()).expect("Natural logarithm resulted in NaN") + } + fn log(self, base: Self) -> Self { + // Panics if self <= 0 or base <= 0 + NotNan::new(self.0.log(base.0)).expect("Logarithm resulted in NaN") + } + fn log2(self) -> Self { + // Panics if self <= 0 + NotNan::new(self.0.log2()).expect("Logarithm resulted in NaN") + } + fn log10(self) -> Self { + // Panics if self <= 0 + NotNan::new(self.0.log10()).expect("Logarithm resulted in NaN") + } + fn to_degrees(self) -> Self { + NotNan(Real::to_degrees(self.0)) + } + fn to_radians(self) -> Self { + NotNan(Real::to_radians(self.0)) + } + fn max(self, other: Self) -> Self { + NotNan(Real::max(self.0, other.0)) + } + fn min(self, other: Self) -> Self { + NotNan(Real::min(self.0, other.0)) + } + fn abs_sub(self, other: Self) -> Self { + NotNan(self.0.abs_sub(other.0)) + } + fn cbrt(self) -> Self { + NotNan(self.0.cbrt()) + } + fn hypot(self, other: Self) -> Self { + NotNan(self.0.hypot(other.0)) + } + fn sin(self) -> Self { + // Panics if self is +/-infinity + NotNan::new(self.0.sin()).expect("Sine resulted in NaN") + } + fn cos(self) -> Self { + // Panics if self is +/-infinity + NotNan::new(self.0.cos()).expect("Cosine resulted in NaN") + } + fn tan(self) -> Self { + // Panics if self is +/-infinity or self == pi/2 + k*pi + NotNan::new(self.0.tan()).expect("Tangent resulted in NaN") + } + fn asin(self) -> Self { + // Panics if self < -1.0 or self > 1.0 + NotNan::new(self.0.asin()).expect("Arcsine resulted in NaN") + } + fn acos(self) -> Self { + // Panics if self < -1.0 or self > 1.0 + NotNan::new(self.0.acos()).expect("Arccosine resulted in NaN") + } + fn atan(self) -> Self { + NotNan(self.0.atan()) + } + fn atan2(self, other: Self) -> Self { + NotNan(self.0.atan2(other.0)) + } + fn sin_cos(self) -> (Self, Self) { + // Panics if self is +/-infinity + let (a, b) = self.0.sin_cos(); + ( + NotNan::new(a).expect("Sine resulted in NaN"), + NotNan::new(b).expect("Cosine resulted in NaN"), + ) + } + fn exp_m1(self) -> Self { + NotNan(self.0.exp_m1()) + } + fn ln_1p(self) -> Self { + // Panics if self <= -1.0 + NotNan::new(self.0.ln_1p()).expect("Natural logarithm resulted in NaN") + } + fn sinh(self) -> Self { + NotNan(self.0.sinh()) + } + fn cosh(self) -> Self { + NotNan(self.0.cosh()) + } + fn tanh(self) -> Self { + NotNan(self.0.tanh()) + } + fn asinh(self) -> Self { + NotNan(self.0.asinh()) + } + fn acosh(self) -> Self { + // Panics if self < 1.0 + NotNan::new(self.0.acosh()).expect("Arccosh resulted in NaN") + } + fn atanh(self) -> Self { + // Panics if self < -1.0 or self > 1.0 + NotNan::new(self.0.atanh()).expect("Arctanh resulted in NaN") + } +} + macro_rules! impl_float_const_method { ($wrapper:expr, $method:ident) => { #[allow(non_snake_case)] diff --git a/tests/test.rs b/tests/test.rs index 6ae7295..06fd6eb 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -772,6 +772,134 @@ fn test_ref_ref_binop_regression() { assert_eq!(&x - &y, OrderedFloat(10.0)); } +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_powf_fails_on_negative() { + use num_traits::real::Real; + Real::powf(not_nan(-1.0), not_nan(-1.5)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_sqrt_fails_on_negative() { + use num_traits::real::Real; + Real::sqrt(not_nan(-1.0)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_ln_fails_on_negative() { + use num_traits::real::Real; + Real::ln(not_nan(-1.0)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_log_fails_on_negative() { + use num_traits::real::Real; + Real::log(not_nan(-1.0), not_nan(2.0)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_log_fails_on_negative_base() { + use num_traits::real::Real; + Real::log(not_nan(1.0), not_nan(-2.0)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_log2_fails_on_negative() { + use num_traits::real::Real; + Real::log2(not_nan(-1.0)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_log10_fails_on_negative() { + use num_traits::real::Real; + Real::log10(not_nan(-1.0)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_sin_fails_on_infinite() { + use num_traits::real::Real; + Real::sin(not_nan(f64::INFINITY)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_cos_fails_on_infinite() { + use num_traits::real::Real; + Real::cos(not_nan(f64::INFINITY)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_tan_fails_on_infinite() { + use num_traits::real::Real; + Real::tan(not_nan(f64::INFINITY)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_asin_fails_on_big() { + use num_traits::real::Real; + Real::asin(not_nan(10.0)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_acos_fails_on_big() { + use num_traits::real::Real; + Real::acos(not_nan(10.0)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_sin_cos_fails_on_infinite() { + use num_traits::real::Real; + Real::sin_cos(not_nan(f64::INFINITY)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_ln_1p_fails_on_negative() { + use num_traits::real::Real; + Real::ln_1p(not_nan(-1.1)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_acosh_fails_on_zero() { + use num_traits::real::Real; + Real::acosh(not_nan(-0.0)); +} + +#[cfg(any(feature = "std", feature = "libm"))] +#[test] +#[should_panic] +fn test_atanh_fails_on_big() { + use num_traits::real::Real; + Real::atanh(not_nan(10.0)); +} + #[cfg(feature = "arbitrary")] mod arbitrary_test { use super::{NotNan, OrderedFloat}; From 1ae638335e36a96dd2ea77954964adff54b62cb1 Mon Sep 17 00:00:00 2001 From: Louis David Date: Fri, 22 Aug 2025 21:46:53 +0200 Subject: [PATCH 2/2] add a missing feature check for use statement --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index ace61ca..dc44a4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ use core::ops::{ use core::str::FromStr; pub use num_traits::float::FloatCore; +#[cfg(any(feature = "std", feature = "libm"))] use num_traits::real::Real; use num_traits::{ AsPrimitive, Bounded, FloatConst, FromPrimitive, Num, NumCast, One, Signed, ToPrimitive, Zero,