From dde326c9ad3151ef401f5eeb2f772c10093abfbf Mon Sep 17 00:00:00 2001 From: Zac Harrold Date: Fri, 18 Apr 2025 21:27:04 +1000 Subject: [PATCH] Add `round_ties_even` to `Float`, `FloatCore`, and `Real` --- .github/workflows/ci.yaml | 1 + build.rs | 4 + ci/rustup.sh | 2 +- src/float.rs | 227 ++++++++++++++++++++++++++++++++++++++ src/macros.rs | 28 +++++ src/real.rs | 24 ++++ 6 files changed, 285 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e580e2c5..843a56e5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,6 +12,7 @@ jobs: 1.60.0, # MSRV 1.62.0, # has_total_cmp 1.74.0, # has_num_saturating + 1.77.0, # has_round_ties_even stable, beta, nightly, diff --git a/build.rs b/build.rs index 7da75312..0be9aeea 100644 --- a/build.rs +++ b/build.rs @@ -5,5 +5,9 @@ fn main() { ac.emit_expression_cfg("1f64.total_cmp(&2f64)", "has_total_cmp"); // 1.62 ac.emit_path_cfg("core::num::Saturating", "has_num_saturating"); // 1.74 + // round_ties_even is only available in `std` + ac.set_no_std(false); + ac.emit_expression_cfg("1.5f64.round_ties_even()", "has_round_ties_even"); // 1.77 + autocfg::rerun_path("build.rs"); } diff --git a/ci/rustup.sh b/ci/rustup.sh index e2679409..5f058ec8 100755 --- a/ci/rustup.sh +++ b/ci/rustup.sh @@ -5,6 +5,6 @@ set -ex ci=$(dirname $0) -for version in 1.60.0 1.62.0 1.74.0 stable beta nightly; do +for version in 1.60.0 1.62.0 1.74.0 1.77.0 stable beta nightly; do rustup run "$version" "$ci/test_full.sh" done diff --git a/src/float.rs b/src/float.rs index 4124e92c..a5b360d6 100644 --- a/src/float.rs +++ b/src/float.rs @@ -401,6 +401,29 @@ pub trait FloatCore: Num + NumCast + Neg + PartialOrd + Copy { } } + /// Rounds to the nearest integer, with ties biasing towards an even result. + /// + /// # Examples + /// + /// ``` + /// use num_traits::float::FloatCore; + /// + /// fn check(x: T, rounded: T) { + /// assert!(x.round_ties_even() == rounded); + /// } + /// + /// check(1.0f32, 1.0); + /// check(1.25f32, 1.0); + /// check(1.75f32, 2.0); + /// check(1.5f32, 2.0); + /// check(2.5f32, 2.0); + /// check(3.5f32, 4.0); + /// check(-3.5f32, -4.0); + /// ``` + fn round_ties_even(self) -> Self { + round_ties_even_impl!(self) + } + /// Return the integer part of a number. /// /// # Examples @@ -844,6 +867,11 @@ impl FloatCore for f32 { Self::powi(self, n: i32) -> Self; } + #[cfg(all(feature = "std", has_round_ties_even))] + forward! { + Self::round_ties_even(self) -> Self; + } + #[cfg(all(not(feature = "std"), feature = "libm"))] forward! { libm::floorf as floor(self) -> Self; @@ -906,6 +934,11 @@ impl FloatCore for f64 { Self::powi(self, n: i32) -> Self; } + #[cfg(all(feature = "std", has_round_ties_even))] + forward! { + Self::round_ties_even(self) -> Self; + } + #[cfg(all(not(feature = "std"), feature = "libm"))] forward! { libm::floor as floor(self) -> Self; @@ -1195,6 +1228,29 @@ pub trait Float: Num + Copy + NumCast + PartialOrd + Neg { /// ``` fn round(self) -> Self; + /// Rounds to the nearest integer, with ties biasing towards an even result. + /// + /// # Examples + /// + /// ``` + /// use num_traits::Float; + /// + /// fn check(x: T, rounded: T) { + /// assert!(x.round_ties_even() == rounded); + /// } + /// + /// check(1.0f32, 1.0); + /// check(1.25f32, 1.0); + /// check(1.75f32, 2.0); + /// check(1.5f32, 2.0); + /// check(2.5f32, 2.0); + /// check(3.5f32, 4.0); + /// check(-3.5f32, -4.0); + /// ``` + fn round_ties_even(self) -> Self { + round_ties_even_impl!(self) + } + /// Return the integer part of a number. /// /// ``` @@ -1989,6 +2045,11 @@ macro_rules! float_impl_std { Self::atanh(self) -> Self; Self::copysign(self, sign: Self) -> Self; } + + #[cfg(has_round_ties_even)] + forward! { + Self::round_ties_even(self) -> Self; + } } }; } @@ -2510,4 +2571,170 @@ mod tests { check_lt(f32::INFINITY, f32::NAN); check_gt(f32::NAN, 1.0_f32); } + + /// Compares the fallback implementation of [`round_ties_even`] to the one provided by `f32`.` + /// + /// [`round_ties_even`]: crate::float::FloatCore::round_ties_even + #[cfg(has_round_ties_even)] + #[test] + fn round_ties_even() { + mod wrapped_f32 { + use crate::{float::FloatCore, Num, NumCast, One, ToPrimitive, Zero}; + use core::ops::{Add, Div, Mul, Neg, Rem, Sub}; + + #[derive(Clone, Copy, PartialEq, PartialOrd, Debug)] + pub struct WrappedF32(pub f32); + + impl ToPrimitive for WrappedF32 { + fn to_i64(&self) -> Option { + f32::to_i64(&self.0) + } + + fn to_u64(&self) -> Option { + f32::to_u64(&self.0) + } + } + + impl NumCast for WrappedF32 { + fn from(n: T) -> Option { + Some(Self(::from(n)?)) + } + } + + impl Neg for WrappedF32 { + type Output = Self; + + fn neg(self) -> Self::Output { + Self(self.0.neg()) + } + } + + impl Mul for WrappedF32 { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self(f32::mul(self.0, rhs.0)) + } + } + + impl Add for WrappedF32 { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(f32::add(self.0, rhs.0)) + } + } + + impl Rem for WrappedF32 { + type Output = Self; + + fn rem(self, rhs: Self) -> Self::Output { + Self(f32::rem(self.0, rhs.0)) + } + } + + impl Div for WrappedF32 { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + Self(f32::div(self.0, rhs.0)) + } + } + + impl Sub for WrappedF32 { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self(f32::sub(self.0, rhs.0)) + } + } + + impl One for WrappedF32 { + fn one() -> Self { + Self(f32::one()) + } + } + + impl Zero for WrappedF32 { + fn zero() -> Self { + Self(f32::zero()) + } + + fn is_zero(&self) -> bool { + self.0.is_zero() + } + } + + impl Num for WrappedF32 { + type FromStrRadixErr = ::FromStrRadixErr; + + fn from_str_radix(str: &str, radix: u32) -> Result { + Ok(Self(f32::from_str_radix(str, radix)?)) + } + } + + impl FloatCore for WrappedF32 { + fn infinity() -> Self { + Self(f32::infinity()) + } + + fn neg_infinity() -> Self { + Self(f32::neg_infinity()) + } + + fn nan() -> Self { + Self(f32::nan()) + } + + fn neg_zero() -> Self { + Self(f32::neg_zero()) + } + + fn min_value() -> Self { + Self(f32::min_value()) + } + + fn min_positive_value() -> Self { + Self(f32::min_positive_value()) + } + + fn epsilon() -> Self { + Self(f32::epsilon()) + } + + fn max_value() -> Self { + Self(f32::max_value()) + } + + fn classify(self) -> core::num::FpCategory { + f32::classify(self.0) + } + + fn to_degrees(self) -> Self { + Self(f32::to_degrees(self.0)) + } + + fn to_radians(self) -> Self { + Self(f32::to_radians(self.0)) + } + + fn integer_decode(self) -> (u64, i16, i8) { + f32::integer_decode(self.0) + } + } + } + + use crate::float::FloatCore; + use wrapped_f32::WrappedF32; + + for x in [ + -5.0, -4.5, -4.0, -3.5, -3.0, -2.5, -2.0, -1.5, -1.0, -0.5, 0.0, 0.5, 1.0, 1.5, 2.0, + 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, + ] { + for dx in -250_000..=250_000 { + let y = x + (dx as f32 / 1_000_000.0); + assert_eq!(WrappedF32(y).round_ties_even().0, y.round_ties_even()); + } + } + } } diff --git a/src/macros.rs b/src/macros.rs index b97758e4..c75d9ae4 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -42,3 +42,31 @@ macro_rules! constant { } )*}; } + +/// Pulling out the inner implementation of a default `round_ties_even` to allow +/// reuse across the various relevant traits. +macro_rules! round_ties_even_impl { + ($self:ident) => {{ + let half = (Self::one() + Self::one()).recip(); + + if $self.fract().abs() != half { + $self.round() + } else { + let i = $self.abs().trunc(); + + let value = if (i * half).fract() == half { + // -1.5, 1.5, 3.5, ... + $self.abs() + half + } else { + // -0.5, 0.5, 2.5, ... + $self.abs() - half + }; + + if $self.signum() != value.signum() { + -value + } else { + value + } + } + }}; +} diff --git a/src/real.rs b/src/real.rs index 25ec8732..a755d247 100644 --- a/src/real.rs +++ b/src/real.rs @@ -107,6 +107,29 @@ pub trait Real: Num + Copy + NumCast + PartialOrd + Neg { /// ``` fn round(self) -> Self; + /// Rounds to the nearest integer, with ties biasing towards an even result. + /// + /// # Examples + /// + /// ``` + /// use num_traits::real::Real; + /// + /// fn check(x: T, rounded: T) { + /// assert!(x.round_ties_even() == rounded); + /// } + /// + /// check(1.0f32, 1.0); + /// check(1.25f32, 1.0); + /// check(1.75f32, 2.0); + /// check(1.5f32, 2.0); + /// check(2.5f32, 2.0); + /// check(3.5f32, 4.0); + /// check(-3.5f32, -4.0); + /// ``` + fn round_ties_even(self) -> Self { + round_ties_even_impl!(self) + } + /// Return the integer part of a number. /// /// ``` @@ -830,5 +853,6 @@ impl Real for T { Float::asinh(self) -> Self; Float::acosh(self) -> Self; Float::atanh(self) -> Self; + Float::round_ties_even(self) -> Self; } }