diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 95eb8264..5f35692d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,6 +20,7 @@ jobs: 1.44.0, # has_to_int_unchecked 1.46.0, # has_leading_trailing_ones 1.53.0, # has_is_subnormal + 1.62.0, # has_total_cmp stable, beta, nightly, diff --git a/bors.toml b/bors.toml index 7314c271..20713ee2 100644 --- a/bors.toml +++ b/bors.toml @@ -6,6 +6,7 @@ status = [ "Test (1.44.0)", "Test (1.46.0)", "Test (1.53.0)", + "Test (1.62.0)", "Test (stable)", "Test (beta)", "Test (nightly)", diff --git a/build.rs b/build.rs index bb783281..11bf090a 100644 --- a/build.rs +++ b/build.rs @@ -16,6 +16,7 @@ fn main() { ac.emit_expression_cfg("1f64.copysign(-1f64)", "has_copysign"); } ac.emit_expression_cfg("1f64.is_subnormal()", "has_is_subnormal"); + ac.emit_expression_cfg("1f64.total_cmp(&2f64)", "has_total_cmp"); ac.emit_expression_cfg("1u32.to_ne_bytes()", "has_int_to_from_bytes"); ac.emit_expression_cfg("3.14f64.to_ne_bytes()", "has_float_to_from_bytes"); diff --git a/ci/rustup.sh b/ci/rustup.sh index 56a8df10..26c8c3db 100755 --- a/ci/rustup.sh +++ b/ci/rustup.sh @@ -5,6 +5,6 @@ set -ex ci=$(dirname $0) -for version in 1.31.0 1.35.0 1.37.0 1.38.0 1.44.0 1.46.0 1.53.0 stable beta nightly; do +for version in 1.31.0 1.35.0 1.37.0 1.38.0 1.44.0 1.46.0 1.53.0 1.62.0 stable beta nightly; do rustup run "$version" "$ci/test_full.sh" done diff --git a/src/float.rs b/src/float.rs index 87f8387b..844b89ae 100644 --- a/src/float.rs +++ b/src/float.rs @@ -1,3 +1,4 @@ +use core::cmp::Ordering; use core::num::FpCategory; use core::ops::{Add, Div, Neg}; @@ -2210,6 +2211,89 @@ float_const_impl! { SQRT_2, } +/// Trait for floating point numbers that provide an implementation +/// of the `totalOrder` predicate as defined in the IEEE 754 (2008 revision) +/// floating point standard. +pub trait TotalOrder { + /// Return the ordering between `self` and `other`. + /// + /// Unlike the standard partial comparison between floating point numbers, + /// this comparison always produces an ordering in accordance to + /// the `totalOrder` predicate as defined in the IEEE 754 (2008 revision) + /// floating point standard. The values are ordered in the following sequence: + /// + /// - negative quiet NaN + /// - negative signaling NaN + /// - negative infinity + /// - negative numbers + /// - negative subnormal numbers + /// - negative zero + /// - positive zero + /// - positive subnormal numbers + /// - positive numbers + /// - positive infinity + /// - positive signaling NaN + /// - positive quiet NaN. + /// + /// The ordering established by this function does not always agree with the + /// [`PartialOrd`] and [`PartialEq`] implementations. For example, + /// they consider negative and positive zero equal, while `total_cmp` + /// doesn't. + /// + /// The interpretation of the signaling NaN bit follows the definition in + /// the IEEE 754 standard, which may not match the interpretation by some of + /// the older, non-conformant (e.g. MIPS) hardware implementations. + /// + /// # Examples + /// ``` + /// use num_traits::float::TotalOrder; + /// use std::cmp::Ordering; + /// use std::{f32, f64}; + /// + /// fn check_eq(x: T, y: T) { + /// assert_eq!(x.total_cmp(&y), Ordering::Equal); + /// } + /// + /// check_eq(f64::NAN, f64::NAN); + /// check_eq(f32::NAN, f32::NAN); + /// + /// fn check_lt(x: T, y: T) { + /// assert_eq!(x.total_cmp(&y), Ordering::Less); + /// } + /// + /// check_lt(-f64::NAN, f64::NAN); + /// check_lt(f64::INFINITY, f64::NAN); + /// check_lt(-0.0_f64, 0.0_f64); + /// ``` + fn total_cmp(&self, other: &Self) -> Ordering; +} +macro_rules! totalorder_impl { + ($T:ident, $I:ident, $U:ident, $bits:expr) => { + impl TotalOrder for $T { + #[inline] + #[cfg(has_total_cmp)] + fn total_cmp(&self, other: &Self) -> Ordering { + // Forward to the core implementation + Self::total_cmp(&self, other) + } + #[inline] + #[cfg(not(has_total_cmp))] + fn total_cmp(&self, other: &Self) -> Ordering { + // Backport the core implementation (since 1.62) + let mut left = self.to_bits() as $I; + let mut right = other.to_bits() as $I; + + left ^= (((left >> ($bits - 1)) as $U) >> 1) as $I; + right ^= (((right >> ($bits - 1)) as $U) >> 1) as $I; + + left.cmp(&right) + } + } + }; +} +totalorder_impl!(f64, i64, u64, 64); +totalorder_impl!(f32, i32, u32, 32); + #[cfg(test)] mod tests { use core::f64::consts; @@ -2341,4 +2425,58 @@ mod tests { test_subnormal::(); test_subnormal::(); } + + #[test] + fn total_cmp() { + use crate::float::TotalOrder; + use core::cmp::Ordering; + use core::{f32, f64}; + + fn check_eq(x: T, y: T) { + assert_eq!(x.total_cmp(&y), Ordering::Equal); + } + fn check_lt(x: T, y: T) { + assert_eq!(x.total_cmp(&y), Ordering::Less); + } + fn check_gt(x: T, y: T) { + assert_eq!(x.total_cmp(&y), Ordering::Greater); + } + + check_eq(f64::NAN, f64::NAN); + check_eq(f32::NAN, f32::NAN); + + check_lt(-0.0_f64, 0.0_f64); + check_lt(-0.0_f32, 0.0_f32); + + // x87 registers don't preserve the exact value of signaling NaN: + // https://github.com/rust-lang/rust/issues/115567 + #[cfg(not(target_arch = "x86"))] + { + let s_nan = f64::from_bits(0x7ff4000000000000); + let q_nan = f64::from_bits(0x7ff8000000000000); + check_lt(s_nan, q_nan); + + let neg_s_nan = f64::from_bits(0xfff4000000000000); + let neg_q_nan = f64::from_bits(0xfff8000000000000); + check_lt(neg_q_nan, neg_s_nan); + + let s_nan = f32::from_bits(0x7fa00000); + let q_nan = f32::from_bits(0x7fc00000); + check_lt(s_nan, q_nan); + + let neg_s_nan = f32::from_bits(0xffa00000); + let neg_q_nan = f32::from_bits(0xffc00000); + check_lt(neg_q_nan, neg_s_nan); + } + + check_lt(-f64::NAN, f64::NEG_INFINITY); + check_gt(1.0_f64, -f64::NAN); + check_lt(f64::INFINITY, f64::NAN); + check_gt(f64::NAN, 1.0_f64); + + check_lt(-f32::NAN, f32::NEG_INFINITY); + check_gt(1.0_f32, -f32::NAN); + check_lt(f32::INFINITY, f32::NAN); + check_gt(f32::NAN, 1.0_f32); + } }