Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TotalOrder trait for floating point numbers #295

Merged
merged 8 commits into from Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions bors.toml
Expand Up @@ -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)",
Expand Down
1 change: 1 addition & 0 deletions build.rs
Expand Up @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion ci/rustup.sh
Expand Up @@ -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
138 changes: 138 additions & 0 deletions src/float.rs
@@ -1,3 +1,4 @@
use core::cmp::Ordering;
use core::num::FpCategory;
use core::ops::{Add, Div, Neg};

Expand Down Expand Up @@ -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<T: TotalOrder>(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<T: TotalOrder>(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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to need a fallback implementation for Rust < 1.62, or else this errors as unconditional recursion. I can add that myself if you're not sure how to go about that...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what sort of cfg tricks could be used to conditionally provide a fallback based on language version. Or maybe cfg is not the answer? I will learn from you.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've pushed that and a few other fixups -- please review in return!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, the answer is autocfg... no surprise it has 200 million downloads. Very interesting.

Excellent idea to replace std::cmp::Ordering with core::cmp::Ordering, so that this PR is compatible with no_std by default.

Looks good to me.

Tangentially, thank you for the opportunity to contribute; I learned several useful things.

}
#[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;
Expand Down Expand Up @@ -2341,4 +2425,58 @@ mod tests {
test_subnormal::<f64>();
test_subnormal::<f32>();
}

#[test]
fn total_cmp() {
use crate::float::TotalOrder;
use core::cmp::Ordering;
use core::{f32, f64};

fn check_eq<T: TotalOrder>(x: T, y: T) {
assert_eq!(x.total_cmp(&y), Ordering::Equal);
}
fn check_lt<T: TotalOrder>(x: T, y: T) {
assert_eq!(x.total_cmp(&y), Ordering::Less);
}
fn check_gt<T: TotalOrder>(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);
}
}