Skip to content

Commit c09f0eb

Browse files
authored
Rollup merge of #72568 - golddranks:add_total_cmp_to_floats, 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 * #53938 * #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.
2 parents 9ef6227 + 66da735 commit c09f0eb

File tree

5 files changed

+435
-0
lines changed

5 files changed

+435
-0
lines changed

src/libcore/num/f32.rs

+74
Original file line numberDiff line numberDiff line change
@@ -810,4 +810,78 @@ impl f32 {
810810
pub fn from_ne_bytes(bytes: [u8; 4]) -> Self {
811811
Self::from_bits(u32::from_ne_bytes(bytes))
812812
}
813+
814+
/// Returns an ordering between self and other values.
815+
/// Unlike the standard partial comparison between floating point numbers,
816+
/// this comparison always produces an ordering in accordance to
817+
/// the totalOrder predicate as defined in IEEE 754 (2008 revision)
818+
/// floating point standard. The values are ordered in following order:
819+
/// - Negative quiet NaN
820+
/// - Negative signaling NaN
821+
/// - Negative infinity
822+
/// - Negative numbers
823+
/// - Negative subnormal numbers
824+
/// - Negative zero
825+
/// - Positive zero
826+
/// - Positive subnormal numbers
827+
/// - Positive numbers
828+
/// - Positive infinity
829+
/// - Positive signaling NaN
830+
/// - Positive quiet NaN
831+
///
832+
/// # Example
833+
/// ```
834+
/// #![feature(total_cmp)]
835+
/// struct GoodBoy {
836+
/// name: String,
837+
/// weight: f32,
838+
/// }
839+
///
840+
/// let mut bois = vec![
841+
/// GoodBoy { name: "Pucci".to_owned(), weight: 0.1 },
842+
/// GoodBoy { name: "Woofer".to_owned(), weight: 99.0 },
843+
/// GoodBoy { name: "Yapper".to_owned(), weight: 10.0 },
844+
/// GoodBoy { name: "Chonk".to_owned(), weight: f32::INFINITY },
845+
/// GoodBoy { name: "Abs. Unit".to_owned(), weight: f32::NAN },
846+
/// GoodBoy { name: "Floaty".to_owned(), weight: -5.0 },
847+
/// ];
848+
///
849+
/// bois.sort_by(|a, b| a.weight.total_cmp(&b.weight));
850+
/// # assert!(bois.into_iter().map(|b| b.weight)
851+
/// # .zip([-5.0, 0.1, 10.0, 99.0, f32::INFINITY, f32::NAN].iter())
852+
/// # .all(|(a, b)| a.to_bits() == b.to_bits()))
853+
/// ```
854+
#[unstable(feature = "total_cmp", issue = "72599")]
855+
#[inline]
856+
pub fn total_cmp(&self, other: &Self) -> crate::cmp::Ordering {
857+
let mut left = self.to_bits() as i32;
858+
let mut right = other.to_bits() as i32;
859+
860+
// In case of negatives, flip all the bits except the sign
861+
// to achieve a similar layout as two's complement integers
862+
//
863+
// Why does this work? IEEE 754 floats consist of three fields:
864+
// Sign bit, exponent and mantissa. The set of exponent and mantissa
865+
// fields as a whole have the property that their bitwise order is
866+
// equal to the numeric magnitude where the magnitude is defined.
867+
// The magnitude is not normally defined on NaN values, but
868+
// IEEE 754 totalOrder defines the NaN values also to follow the
869+
// bitwise order. This leads to order explained in the doc comment.
870+
// However, the representation of magnitude is the same for negative
871+
// and positive numbers – only the sign bit is different.
872+
// To easily compare the floats as signed integers, we need to
873+
// flip the exponent and mantissa bits in case of negative numbers.
874+
// We effectively convert the numbers to "two's complement" form.
875+
//
876+
// To do the flipping, we construct a mask and XOR against it.
877+
// We branchlessly calculate an "all-ones except for the sign bit"
878+
// mask from negative-signed values: right shifting sign-extends
879+
// the integer, so we "fill" the mask with sign bits, and then
880+
// convert to unsigned to push one more zero bit.
881+
// On positive values, the mask is all zeros, so it's a no-op.
882+
left ^= (((left >> 31) as u32) >> 1) as i32;
883+
right ^= (((right >> 31) as u32) >> 1) as i32;
884+
885+
left.cmp(&right)
886+
}
813887
}

src/libcore/num/f64.rs

+74
Original file line numberDiff line numberDiff line change
@@ -824,4 +824,78 @@ impl f64 {
824824
pub fn from_ne_bytes(bytes: [u8; 8]) -> Self {
825825
Self::from_bits(u64::from_ne_bytes(bytes))
826826
}
827+
828+
/// Returns an ordering between self and other values.
829+
/// Unlike the standard partial comparison between floating point numbers,
830+
/// this comparison always produces an ordering in accordance to
831+
/// the totalOrder predicate as defined in IEEE 754 (2008 revision)
832+
/// floating point standard. The values are ordered in following order:
833+
/// - Negative quiet NaN
834+
/// - Negative signaling NaN
835+
/// - Negative infinity
836+
/// - Negative numbers
837+
/// - Negative subnormal numbers
838+
/// - Negative zero
839+
/// - Positive zero
840+
/// - Positive subnormal numbers
841+
/// - Positive numbers
842+
/// - Positive infinity
843+
/// - Positive signaling NaN
844+
/// - Positive quiet NaN
845+
///
846+
/// # Example
847+
/// ```
848+
/// #![feature(total_cmp)]
849+
/// struct GoodBoy {
850+
/// name: String,
851+
/// weight: f64,
852+
/// }
853+
///
854+
/// let mut bois = vec![
855+
/// GoodBoy { name: "Pucci".to_owned(), weight: 0.1 },
856+
/// GoodBoy { name: "Woofer".to_owned(), weight: 99.0 },
857+
/// GoodBoy { name: "Yapper".to_owned(), weight: 10.0 },
858+
/// GoodBoy { name: "Chonk".to_owned(), weight: f64::INFINITY },
859+
/// GoodBoy { name: "Abs. Unit".to_owned(), weight: f64::NAN },
860+
/// GoodBoy { name: "Floaty".to_owned(), weight: -5.0 },
861+
/// ];
862+
///
863+
/// bois.sort_by(|a, b| a.weight.total_cmp(&b.weight));
864+
/// # assert!(bois.into_iter().map(|b| b.weight)
865+
/// # .zip([-5.0, 0.1, 10.0, 99.0, f64::INFINITY, f64::NAN].iter())
866+
/// # .all(|(a, b)| a.to_bits() == b.to_bits()))
867+
/// ```
868+
#[unstable(feature = "total_cmp", issue = "72599")]
869+
#[inline]
870+
pub fn total_cmp(&self, other: &Self) -> crate::cmp::Ordering {
871+
let mut left = self.to_bits() as i64;
872+
let mut right = other.to_bits() as i64;
873+
874+
// In case of negatives, flip all the bits except the sign
875+
// to achieve a similar layout as two's complement integers
876+
//
877+
// Why does this work? IEEE 754 floats consist of three fields:
878+
// Sign bit, exponent and mantissa. The set of exponent and mantissa
879+
// fields as a whole have the property that their bitwise order is
880+
// equal to the numeric magnitude where the magnitude is defined.
881+
// The magnitude is not normally defined on NaN values, but
882+
// IEEE 754 totalOrder defines the NaN values also to follow the
883+
// bitwise order. This leads to order explained in the doc comment.
884+
// However, the representation of magnitude is the same for negative
885+
// and positive numbers – only the sign bit is different.
886+
// To easily compare the floats as signed integers, we need to
887+
// flip the exponent and mantissa bits in case of negative numbers.
888+
// We effectively convert the numbers to "two's complement" form.
889+
//
890+
// To do the flipping, we construct a mask and XOR against it.
891+
// We branchlessly calculate an "all-ones except for the sign bit"
892+
// mask from negative-signed values: right shifting sign-extends
893+
// the integer, so we "fill" the mask with sign bits, and then
894+
// convert to unsigned to push one more zero bit.
895+
// On positive values, the mask is all zeros, so it's a no-op.
896+
left ^= (((left >> 63) as u64) >> 1) as i64;
897+
right ^= (((right >> 63) as u64) >> 1) as i64;
898+
899+
left.cmp(&right)
900+
}
827901
}

src/libstd/f32.rs

+143
Original file line numberDiff line numberDiff line change
@@ -1531,4 +1531,147 @@ mod tests {
15311531
fn test_clamp_max_is_nan() {
15321532
let _ = 1.0f32.clamp(3.0, NAN);
15331533
}
1534+
1535+
#[test]
1536+
fn test_total_cmp() {
1537+
use core::cmp::Ordering;
1538+
1539+
fn quiet_bit_mask() -> u32 {
1540+
1 << (f32::MANTISSA_DIGITS - 2)
1541+
}
1542+
1543+
fn min_subnorm() -> f32 {
1544+
f32::MIN_POSITIVE / f32::powf(2.0, f32::MANTISSA_DIGITS as f32 - 1.0)
1545+
}
1546+
1547+
fn max_subnorm() -> f32 {
1548+
f32::MIN_POSITIVE - min_subnorm()
1549+
}
1550+
1551+
fn q_nan() -> f32 {
1552+
f32::from_bits(f32::NAN.to_bits() | quiet_bit_mask())
1553+
}
1554+
1555+
fn s_nan() -> f32 {
1556+
f32::from_bits((f32::NAN.to_bits() & !quiet_bit_mask()) + 42)
1557+
}
1558+
1559+
assert_eq!(Ordering::Equal, (-q_nan()).total_cmp(&-q_nan()));
1560+
assert_eq!(Ordering::Equal, (-s_nan()).total_cmp(&-s_nan()));
1561+
assert_eq!(Ordering::Equal, (-f32::INFINITY).total_cmp(&-f32::INFINITY));
1562+
assert_eq!(Ordering::Equal, (-f32::MAX).total_cmp(&-f32::MAX));
1563+
assert_eq!(Ordering::Equal, (-2.5_f32).total_cmp(&-2.5));
1564+
assert_eq!(Ordering::Equal, (-1.0_f32).total_cmp(&-1.0));
1565+
assert_eq!(Ordering::Equal, (-1.5_f32).total_cmp(&-1.5));
1566+
assert_eq!(Ordering::Equal, (-0.5_f32).total_cmp(&-0.5));
1567+
assert_eq!(Ordering::Equal, (-f32::MIN_POSITIVE).total_cmp(&-f32::MIN_POSITIVE));
1568+
assert_eq!(Ordering::Equal, (-max_subnorm()).total_cmp(&-max_subnorm()));
1569+
assert_eq!(Ordering::Equal, (-min_subnorm()).total_cmp(&-min_subnorm()));
1570+
assert_eq!(Ordering::Equal, (-0.0_f32).total_cmp(&-0.0));
1571+
assert_eq!(Ordering::Equal, 0.0_f32.total_cmp(&0.0));
1572+
assert_eq!(Ordering::Equal, min_subnorm().total_cmp(&min_subnorm()));
1573+
assert_eq!(Ordering::Equal, max_subnorm().total_cmp(&max_subnorm()));
1574+
assert_eq!(Ordering::Equal, f32::MIN_POSITIVE.total_cmp(&f32::MIN_POSITIVE));
1575+
assert_eq!(Ordering::Equal, 0.5_f32.total_cmp(&0.5));
1576+
assert_eq!(Ordering::Equal, 1.0_f32.total_cmp(&1.0));
1577+
assert_eq!(Ordering::Equal, 1.5_f32.total_cmp(&1.5));
1578+
assert_eq!(Ordering::Equal, 2.5_f32.total_cmp(&2.5));
1579+
assert_eq!(Ordering::Equal, f32::MAX.total_cmp(&f32::MAX));
1580+
assert_eq!(Ordering::Equal, f32::INFINITY.total_cmp(&f32::INFINITY));
1581+
assert_eq!(Ordering::Equal, s_nan().total_cmp(&s_nan()));
1582+
assert_eq!(Ordering::Equal, q_nan().total_cmp(&q_nan()));
1583+
1584+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-s_nan()));
1585+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-f32::INFINITY));
1586+
assert_eq!(Ordering::Less, (-f32::INFINITY).total_cmp(&-f32::MAX));
1587+
assert_eq!(Ordering::Less, (-f32::MAX).total_cmp(&-2.5));
1588+
assert_eq!(Ordering::Less, (-2.5_f32).total_cmp(&-1.5));
1589+
assert_eq!(Ordering::Less, (-1.5_f32).total_cmp(&-1.0));
1590+
assert_eq!(Ordering::Less, (-1.0_f32).total_cmp(&-0.5));
1591+
assert_eq!(Ordering::Less, (-0.5_f32).total_cmp(&-f32::MIN_POSITIVE));
1592+
assert_eq!(Ordering::Less, (-f32::MIN_POSITIVE).total_cmp(&-max_subnorm()));
1593+
assert_eq!(Ordering::Less, (-max_subnorm()).total_cmp(&-min_subnorm()));
1594+
assert_eq!(Ordering::Less, (-min_subnorm()).total_cmp(&-0.0));
1595+
assert_eq!(Ordering::Less, (-0.0_f32).total_cmp(&0.0));
1596+
assert_eq!(Ordering::Less, 0.0_f32.total_cmp(&min_subnorm()));
1597+
assert_eq!(Ordering::Less, min_subnorm().total_cmp(&max_subnorm()));
1598+
assert_eq!(Ordering::Less, max_subnorm().total_cmp(&f32::MIN_POSITIVE));
1599+
assert_eq!(Ordering::Less, f32::MIN_POSITIVE.total_cmp(&0.5));
1600+
assert_eq!(Ordering::Less, 0.5_f32.total_cmp(&1.0));
1601+
assert_eq!(Ordering::Less, 1.0_f32.total_cmp(&1.5));
1602+
assert_eq!(Ordering::Less, 1.5_f32.total_cmp(&2.5));
1603+
assert_eq!(Ordering::Less, 2.5_f32.total_cmp(&f32::MAX));
1604+
assert_eq!(Ordering::Less, f32::MAX.total_cmp(&f32::INFINITY));
1605+
assert_eq!(Ordering::Less, f32::INFINITY.total_cmp(&s_nan()));
1606+
assert_eq!(Ordering::Less, s_nan().total_cmp(&q_nan()));
1607+
1608+
assert_eq!(Ordering::Greater, (-s_nan()).total_cmp(&-q_nan()));
1609+
assert_eq!(Ordering::Greater, (-f32::INFINITY).total_cmp(&-s_nan()));
1610+
assert_eq!(Ordering::Greater, (-f32::MAX).total_cmp(&-f32::INFINITY));
1611+
assert_eq!(Ordering::Greater, (-2.5_f32).total_cmp(&-f32::MAX));
1612+
assert_eq!(Ordering::Greater, (-1.5_f32).total_cmp(&-2.5));
1613+
assert_eq!(Ordering::Greater, (-1.0_f32).total_cmp(&-1.5));
1614+
assert_eq!(Ordering::Greater, (-0.5_f32).total_cmp(&-1.0));
1615+
assert_eq!(Ordering::Greater, (-f32::MIN_POSITIVE).total_cmp(&-0.5));
1616+
assert_eq!(Ordering::Greater, (-max_subnorm()).total_cmp(&-f32::MIN_POSITIVE));
1617+
assert_eq!(Ordering::Greater, (-min_subnorm()).total_cmp(&-max_subnorm()));
1618+
assert_eq!(Ordering::Greater, (-0.0_f32).total_cmp(&-min_subnorm()));
1619+
assert_eq!(Ordering::Greater, 0.0_f32.total_cmp(&-0.0));
1620+
assert_eq!(Ordering::Greater, min_subnorm().total_cmp(&0.0));
1621+
assert_eq!(Ordering::Greater, max_subnorm().total_cmp(&min_subnorm()));
1622+
assert_eq!(Ordering::Greater, f32::MIN_POSITIVE.total_cmp(&max_subnorm()));
1623+
assert_eq!(Ordering::Greater, 0.5_f32.total_cmp(&f32::MIN_POSITIVE));
1624+
assert_eq!(Ordering::Greater, 1.0_f32.total_cmp(&0.5));
1625+
assert_eq!(Ordering::Greater, 1.5_f32.total_cmp(&1.0));
1626+
assert_eq!(Ordering::Greater, 2.5_f32.total_cmp(&1.5));
1627+
assert_eq!(Ordering::Greater, f32::MAX.total_cmp(&2.5));
1628+
assert_eq!(Ordering::Greater, f32::INFINITY.total_cmp(&f32::MAX));
1629+
assert_eq!(Ordering::Greater, s_nan().total_cmp(&f32::INFINITY));
1630+
assert_eq!(Ordering::Greater, q_nan().total_cmp(&s_nan()));
1631+
1632+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-s_nan()));
1633+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-f32::INFINITY));
1634+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-f32::MAX));
1635+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-2.5));
1636+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-1.5));
1637+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-1.0));
1638+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-0.5));
1639+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-f32::MIN_POSITIVE));
1640+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-max_subnorm()));
1641+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-min_subnorm()));
1642+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&-0.0));
1643+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&0.0));
1644+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&min_subnorm()));
1645+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&max_subnorm()));
1646+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&f32::MIN_POSITIVE));
1647+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&0.5));
1648+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&1.0));
1649+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&1.5));
1650+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&2.5));
1651+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&f32::MAX));
1652+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&f32::INFINITY));
1653+
assert_eq!(Ordering::Less, (-q_nan()).total_cmp(&s_nan()));
1654+
1655+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-f32::INFINITY));
1656+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-f32::MAX));
1657+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-2.5));
1658+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-1.5));
1659+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-1.0));
1660+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-0.5));
1661+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-f32::MIN_POSITIVE));
1662+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-max_subnorm()));
1663+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-min_subnorm()));
1664+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&-0.0));
1665+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&0.0));
1666+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&min_subnorm()));
1667+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&max_subnorm()));
1668+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&f32::MIN_POSITIVE));
1669+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&0.5));
1670+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&1.0));
1671+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&1.5));
1672+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&2.5));
1673+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&f32::MAX));
1674+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&f32::INFINITY));
1675+
assert_eq!(Ordering::Less, (-s_nan()).total_cmp(&s_nan()));
1676+
}
15341677
}

0 commit comments

Comments
 (0)