Skip to content

Commit

Permalink
Rollup merge of rust-lang#72568 - golddranks:add_total_cmp_to_floats,…
Browse files Browse the repository at this point in the history
… r=sfackler

Implement total_cmp for f32, f64

# Overview
* Implements method `total_cmp` on `f32` and `f64`. This method implements a float comparison that, unlike the standard `partial_cmp`, is total (defined on all values) in accordance to the IEEE 754 (rev 2008) §5.10 `totalOrder` predicate.
* The method has an API similar to `cmp`: `pub fn total_cmp(&self, other: &Self) -> crate::cmp::Ordering { ... }`.
* Implements tests.
* Has documentation.

# Justification for the API
* Total ordering for `f32` and `f64` has been discussed many time before:
  * https://internals.rust-lang.org/t/pre-pre-rfc-range-restricting-wrappers-for-floating-point-types/6701
  * rust-lang/rfcs#1249
  * rust-lang#53938
  * rust-lang#5585
* The lack of total ordering leads to frequent complaints, especially from people new to Rust.
  * This is an ergonomics issue that needs to be addressed.
  * However, the default behaviour of implementing only `PartialOrd` is intentional, as relaxing it might lead to correctness issues.
* Most earlier implementations and discussions have been focusing on a wrapper type that implements trait `Ord`. Such a wrapper type is, however not easy to add because of the large API surface added.
* As a minimal step that hopefully proves uncontroversial, we can implement a stand-alone method `total_cmp` on floating point types.
  * I expect adding such methods should be uncontroversial because...
    * Similar methods on `f32` and `f64` would be warranted even in case stdlib would provide a wrapper type that implements `Ord` some day.
    * It implements functionality that is standardised. (IEEE 754, 2008 rev. §5.10 Note, that the 2019 revision relaxes the ordering. The way we do ordering in this method conforms to the stricter 2008 standard.)
* With stdlib APIs such as `slice::sort_by` and `slice::binary_search_by` that allow users to provide a custom ordering criterion, providing additional helper methods is a minimal way of adding ordering functionality.
  * Not also does it allow easily using aforementioned APIs, it also provides an easy and well-tested primitive for the users and library authors to implement an `Ord`-implementing wrapper, if needed.
  • Loading branch information
Dylan-DPC authored May 29, 2020
2 parents 9ef6227 + 66da735 commit c09f0eb
Show file tree
Hide file tree
Showing 5 changed files with 435 additions and 0 deletions.
74 changes: 74 additions & 0 deletions src/libcore/num/f32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -810,4 +810,78 @@ impl f32 {
pub fn from_ne_bytes(bytes: [u8; 4]) -> Self {
Self::from_bits(u32::from_ne_bytes(bytes))
}

/// Returns an ordering between self and other values.
/// Unlike the standard partial comparison between floating point numbers,
/// this comparison always produces an ordering in accordance to
/// the totalOrder predicate as defined in IEEE 754 (2008 revision)
/// floating point standard. The values are ordered in following order:
/// - 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
///
/// # Example
/// ```
/// #![feature(total_cmp)]
/// struct GoodBoy {
/// name: String,
/// weight: f32,
/// }
///
/// let mut bois = vec![
/// GoodBoy { name: "Pucci".to_owned(), weight: 0.1 },
/// GoodBoy { name: "Woofer".to_owned(), weight: 99.0 },
/// GoodBoy { name: "Yapper".to_owned(), weight: 10.0 },
/// GoodBoy { name: "Chonk".to_owned(), weight: f32::INFINITY },
/// GoodBoy { name: "Abs. Unit".to_owned(), weight: f32::NAN },
/// GoodBoy { name: "Floaty".to_owned(), weight: -5.0 },
/// ];
///
/// bois.sort_by(|a, b| a.weight.total_cmp(&b.weight));
/// # assert!(bois.into_iter().map(|b| b.weight)
/// # .zip([-5.0, 0.1, 10.0, 99.0, f32::INFINITY, f32::NAN].iter())
/// # .all(|(a, b)| a.to_bits() == b.to_bits()))
/// ```
#[unstable(feature = "total_cmp", issue = "72599")]
#[inline]
pub fn total_cmp(&self, other: &Self) -> crate::cmp::Ordering {
let mut left = self.to_bits() as i32;
let mut right = other.to_bits() as i32;

// In case of negatives, flip all the bits except the sign
// to achieve a similar layout as two's complement integers
//
// Why does this work? IEEE 754 floats consist of three fields:
// Sign bit, exponent and mantissa. The set of exponent and mantissa
// fields as a whole have the property that their bitwise order is
// equal to the numeric magnitude where the magnitude is defined.
// The magnitude is not normally defined on NaN values, but
// IEEE 754 totalOrder defines the NaN values also to follow the
// bitwise order. This leads to order explained in the doc comment.
// However, the representation of magnitude is the same for negative
// and positive numbers – only the sign bit is different.
// To easily compare the floats as signed integers, we need to
// flip the exponent and mantissa bits in case of negative numbers.
// We effectively convert the numbers to "two's complement" form.
//
// To do the flipping, we construct a mask and XOR against it.
// We branchlessly calculate an "all-ones except for the sign bit"
// mask from negative-signed values: right shifting sign-extends
// the integer, so we "fill" the mask with sign bits, and then
// convert to unsigned to push one more zero bit.
// On positive values, the mask is all zeros, so it's a no-op.
left ^= (((left >> 31) as u32) >> 1) as i32;
right ^= (((right >> 31) as u32) >> 1) as i32;

left.cmp(&right)
}
}
74 changes: 74 additions & 0 deletions src/libcore/num/f64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -824,4 +824,78 @@ impl f64 {
pub fn from_ne_bytes(bytes: [u8; 8]) -> Self {
Self::from_bits(u64::from_ne_bytes(bytes))
}

/// Returns an ordering between self and other values.
/// Unlike the standard partial comparison between floating point numbers,
/// this comparison always produces an ordering in accordance to
/// the totalOrder predicate as defined in IEEE 754 (2008 revision)
/// floating point standard. The values are ordered in following order:
/// - 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
///
/// # Example
/// ```
/// #![feature(total_cmp)]
/// struct GoodBoy {
/// name: String,
/// weight: f64,
/// }
///
/// let mut bois = vec![
/// GoodBoy { name: "Pucci".to_owned(), weight: 0.1 },
/// GoodBoy { name: "Woofer".to_owned(), weight: 99.0 },
/// GoodBoy { name: "Yapper".to_owned(), weight: 10.0 },
/// GoodBoy { name: "Chonk".to_owned(), weight: f64::INFINITY },
/// GoodBoy { name: "Abs. Unit".to_owned(), weight: f64::NAN },
/// GoodBoy { name: "Floaty".to_owned(), weight: -5.0 },
/// ];
///
/// bois.sort_by(|a, b| a.weight.total_cmp(&b.weight));
/// # assert!(bois.into_iter().map(|b| b.weight)
/// # .zip([-5.0, 0.1, 10.0, 99.0, f64::INFINITY, f64::NAN].iter())
/// # .all(|(a, b)| a.to_bits() == b.to_bits()))
/// ```
#[unstable(feature = "total_cmp", issue = "72599")]
#[inline]
pub fn total_cmp(&self, other: &Self) -> crate::cmp::Ordering {
let mut left = self.to_bits() as i64;
let mut right = other.to_bits() as i64;

// In case of negatives, flip all the bits except the sign
// to achieve a similar layout as two's complement integers
//
// Why does this work? IEEE 754 floats consist of three fields:
// Sign bit, exponent and mantissa. The set of exponent and mantissa
// fields as a whole have the property that their bitwise order is
// equal to the numeric magnitude where the magnitude is defined.
// The magnitude is not normally defined on NaN values, but
// IEEE 754 totalOrder defines the NaN values also to follow the
// bitwise order. This leads to order explained in the doc comment.
// However, the representation of magnitude is the same for negative
// and positive numbers – only the sign bit is different.
// To easily compare the floats as signed integers, we need to
// flip the exponent and mantissa bits in case of negative numbers.
// We effectively convert the numbers to "two's complement" form.
//
// To do the flipping, we construct a mask and XOR against it.
// We branchlessly calculate an "all-ones except for the sign bit"
// mask from negative-signed values: right shifting sign-extends
// the integer, so we "fill" the mask with sign bits, and then
// convert to unsigned to push one more zero bit.
// On positive values, the mask is all zeros, so it's a no-op.
left ^= (((left >> 63) as u64) >> 1) as i64;
right ^= (((right >> 63) as u64) >> 1) as i64;

left.cmp(&right)
}
}
143 changes: 143 additions & 0 deletions src/libstd/f32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1531,4 +1531,147 @@ mod tests {
fn test_clamp_max_is_nan() {
let _ = 1.0f32.clamp(3.0, NAN);
}

#[test]
fn test_total_cmp() {
use core::cmp::Ordering;

fn quiet_bit_mask() -> u32 {
1 << (f32::MANTISSA_DIGITS - 2)
}

fn min_subnorm() -> f32 {
f32::MIN_POSITIVE / f32::powf(2.0, f32::MANTISSA_DIGITS as f32 - 1.0)
}

fn max_subnorm() -> f32 {
f32::MIN_POSITIVE - min_subnorm()
}

fn q_nan() -> f32 {
f32::from_bits(f32::NAN.to_bits() | quiet_bit_mask())
}

fn s_nan() -> f32 {
f32::from_bits((f32::NAN.to_bits() & !quiet_bit_mask()) + 42)
}

assert_eq!(Ordering::Equal, (-q_nan()).total_cmp(&-q_nan()));
assert_eq!(Ordering::Equal, (-s_nan()).total_cmp(&-s_nan()));
assert_eq!(Ordering::Equal, (-f32::INFINITY).total_cmp(&-f32::INFINITY));
assert_eq!(Ordering::Equal, (-f32::MAX).total_cmp(&-f32::MAX));
assert_eq!(Ordering::Equal, (-2.5_f32).total_cmp(&-2.5));
assert_eq!(Ordering::Equal, (-1.0_f32).total_cmp(&-1.0));
assert_eq!(Ordering::Equal, (-1.5_f32).total_cmp(&-1.5));
assert_eq!(Ordering::Equal, (-0.5_f32).total_cmp(&-0.5));
assert_eq!(Ordering::Equal, (-f32::MIN_POSITIVE).total_cmp(&-f32::MIN_POSITIVE));
assert_eq!(Ordering::Equal, (-max_subnorm()).total_cmp(&-max_subnorm()));
assert_eq!(Ordering::Equal, (-min_subnorm()).total_cmp(&-min_subnorm()));
assert_eq!(Ordering::Equal, (-0.0_f32).total_cmp(&-0.0));
assert_eq!(Ordering::Equal, 0.0_f32.total_cmp(&0.0));
assert_eq!(Ordering::Equal, min_subnorm().total_cmp(&min_subnorm()));
assert_eq!(Ordering::Equal, max_subnorm().total_cmp(&max_subnorm()));
assert_eq!(Ordering::Equal, f32::MIN_POSITIVE.total_cmp(&f32::MIN_POSITIVE));
assert_eq!(Ordering::Equal, 0.5_f32.total_cmp(&0.5));
assert_eq!(Ordering::Equal, 1.0_f32.total_cmp(&1.0));
assert_eq!(Ordering::Equal, 1.5_f32.total_cmp(&1.5));
assert_eq!(Ordering::Equal, 2.5_f32.total_cmp(&2.5));
assert_eq!(Ordering::Equal, f32::MAX.total_cmp(&f32::MAX));
assert_eq!(Ordering::Equal, f32::INFINITY.total_cmp(&f32::INFINITY));
assert_eq!(Ordering::Equal, s_nan().total_cmp(&s_nan()));
assert_eq!(Ordering::Equal, q_nan().total_cmp(&q_nan()));

assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-s_nan()));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-f32::INFINITY));
assert_eq!(Ordering::Less, (-f32::INFINITY).total_cmp(&-f32::MAX));
assert_eq!(Ordering::Less, (-f32::MAX).total_cmp(&-2.5));
assert_eq!(Ordering::Less, (-2.5_f32).total_cmp(&-1.5));
assert_eq!(Ordering::Less, (-1.5_f32).total_cmp(&-1.0));
assert_eq!(Ordering::Less, (-1.0_f32).total_cmp(&-0.5));
assert_eq!(Ordering::Less, (-0.5_f32).total_cmp(&-f32::MIN_POSITIVE));
assert_eq!(Ordering::Less, (-f32::MIN_POSITIVE).total_cmp(&-max_subnorm()));
assert_eq!(Ordering::Less, (-max_subnorm()).total_cmp(&-min_subnorm()));
assert_eq!(Ordering::Less, (-min_subnorm()).total_cmp(&-0.0));
assert_eq!(Ordering::Less, (-0.0_f32).total_cmp(&0.0));
assert_eq!(Ordering::Less, 0.0_f32.total_cmp(&min_subnorm()));
assert_eq!(Ordering::Less, min_subnorm().total_cmp(&max_subnorm()));
assert_eq!(Ordering::Less, max_subnorm().total_cmp(&f32::MIN_POSITIVE));
assert_eq!(Ordering::Less, f32::MIN_POSITIVE.total_cmp(&0.5));
assert_eq!(Ordering::Less, 0.5_f32.total_cmp(&1.0));
assert_eq!(Ordering::Less, 1.0_f32.total_cmp(&1.5));
assert_eq!(Ordering::Less, 1.5_f32.total_cmp(&2.5));
assert_eq!(Ordering::Less, 2.5_f32.total_cmp(&f32::MAX));
assert_eq!(Ordering::Less, f32::MAX.total_cmp(&f32::INFINITY));
assert_eq!(Ordering::Less, f32::INFINITY.total_cmp(&s_nan()));
assert_eq!(Ordering::Less, s_nan().total_cmp(&q_nan()));

assert_eq!(Ordering::Greater, (-s_nan()).total_cmp(&-q_nan()));
assert_eq!(Ordering::Greater, (-f32::INFINITY).total_cmp(&-s_nan()));
assert_eq!(Ordering::Greater, (-f32::MAX).total_cmp(&-f32::INFINITY));
assert_eq!(Ordering::Greater, (-2.5_f32).total_cmp(&-f32::MAX));
assert_eq!(Ordering::Greater, (-1.5_f32).total_cmp(&-2.5));
assert_eq!(Ordering::Greater, (-1.0_f32).total_cmp(&-1.5));
assert_eq!(Ordering::Greater, (-0.5_f32).total_cmp(&-1.0));
assert_eq!(Ordering::Greater, (-f32::MIN_POSITIVE).total_cmp(&-0.5));
assert_eq!(Ordering::Greater, (-max_subnorm()).total_cmp(&-f32::MIN_POSITIVE));
assert_eq!(Ordering::Greater, (-min_subnorm()).total_cmp(&-max_subnorm()));
assert_eq!(Ordering::Greater, (-0.0_f32).total_cmp(&-min_subnorm()));
assert_eq!(Ordering::Greater, 0.0_f32.total_cmp(&-0.0));
assert_eq!(Ordering::Greater, min_subnorm().total_cmp(&0.0));
assert_eq!(Ordering::Greater, max_subnorm().total_cmp(&min_subnorm()));
assert_eq!(Ordering::Greater, f32::MIN_POSITIVE.total_cmp(&max_subnorm()));
assert_eq!(Ordering::Greater, 0.5_f32.total_cmp(&f32::MIN_POSITIVE));
assert_eq!(Ordering::Greater, 1.0_f32.total_cmp(&0.5));
assert_eq!(Ordering::Greater, 1.5_f32.total_cmp(&1.0));
assert_eq!(Ordering::Greater, 2.5_f32.total_cmp(&1.5));
assert_eq!(Ordering::Greater, f32::MAX.total_cmp(&2.5));
assert_eq!(Ordering::Greater, f32::INFINITY.total_cmp(&f32::MAX));
assert_eq!(Ordering::Greater, s_nan().total_cmp(&f32::INFINITY));
assert_eq!(Ordering::Greater, q_nan().total_cmp(&s_nan()));

assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-s_nan()));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-f32::INFINITY));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-f32::MAX));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-2.5));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-1.5));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-1.0));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-0.5));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-f32::MIN_POSITIVE));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-max_subnorm()));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-min_subnorm()));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-0.0));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&0.0));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&min_subnorm()));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&max_subnorm()));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&f32::MIN_POSITIVE));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&0.5));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&1.0));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&1.5));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&2.5));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&f32::MAX));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&f32::INFINITY));
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&s_nan()));

assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-f32::INFINITY));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-f32::MAX));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-2.5));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-1.5));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-1.0));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-0.5));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-f32::MIN_POSITIVE));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-max_subnorm()));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-min_subnorm()));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-0.0));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&0.0));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&min_subnorm()));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&max_subnorm()));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&f32::MIN_POSITIVE));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&0.5));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&1.0));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&1.5));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&2.5));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&f32::MAX));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&f32::INFINITY));
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&s_nan()));
}
}
Loading

0 comments on commit c09f0eb

Please sign in to comment.