Skip to main content

bevy_math/
float_ord.rs

1use core::{
2    cmp::Ordering,
3    hash::{Hash, Hasher},
4    ops::Neg,
5};
6
7#[cfg(feature = "bevy_reflect")]
8use bevy_reflect::Reflect;
9
10/// A wrapper for floats that implements [`Ord`], [`Eq`], and [`Hash`] traits.
11///
12/// Particularly useful if you want to sort floats or use them as part of map keys.
13///
14/// Floats in Rust follow the IEEE 754-2008 standard, the comparison operators
15/// of which are not defined when at least one of the operands are [`NaN`](f32::NAN).
16///
17/// `FloatOrd` defines an ordering for those cases by sorting all `NaN`s as less than all
18/// numbers and as equal to any other `NaN`.
19///
20/// `FloatOrd` is incompatible with the `totalOrder` predicate defined by IEEE 754,
21/// but stays consistent with normal float comparison: If a `a.partial_cmp(b)` is `Some`,
22/// the result is the same for `FloatOrd(a).cmp(FloatOrd(b))`.
23#[derive(Debug, Copy, Clone, Default)]
24#[cfg_attr(
25    feature = "bevy_reflect",
26    derive(Reflect),
27    reflect(Debug, PartialEq, Hash, Clone)
28)]
29pub struct FloatOrd(pub f32);
30
31impl PartialOrd for FloatOrd {
32    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
33        Some(self.cmp(other))
34    }
35
36    fn lt(&self, other: &Self) -> bool {
37        !other.le(self)
38    }
39    // If `self` is NaN, it is equal to another NaN and less than all other floats, so return true.
40    // If `self` isn't NaN and `other` is, the float comparison returns false, which match the `FloatOrd` ordering.
41    // Otherwise, a standard float comparison happens.
42    fn le(&self, other: &Self) -> bool {
43        self.0.is_nan() || self.0 <= other.0
44    }
45    fn gt(&self, other: &Self) -> bool {
46        !self.le(other)
47    }
48    fn ge(&self, other: &Self) -> bool {
49        other.le(self)
50    }
51}
52
53impl Ord for FloatOrd {
54    #[expect(
55        clippy::comparison_chain,
56        reason = "This can't be rewritten with `match` and `cmp`, as this is `cmp` itself."
57    )]
58    fn cmp(&self, other: &Self) -> Ordering {
59        if self > other {
60            Ordering::Greater
61        } else if self < other {
62            Ordering::Less
63        } else {
64            Ordering::Equal
65        }
66    }
67}
68
69impl PartialEq for FloatOrd {
70    fn eq(&self, other: &Self) -> bool {
71        if self.0.is_nan() {
72            other.0.is_nan()
73        } else {
74            self.0 == other.0
75        }
76    }
77}
78
79impl Eq for FloatOrd {}
80
81impl Hash for FloatOrd {
82    fn hash<H: Hasher>(&self, state: &mut H) {
83        if self.0.is_nan() {
84            // Ensure all NaN representations hash to the same value
85            state.write(&f32::to_ne_bytes(f32::NAN));
86        } else if self.0 == 0.0 {
87            // Ensure both zeroes hash to the same value
88            state.write(&f32::to_ne_bytes(0.0f32));
89        } else {
90            state.write(&f32::to_ne_bytes(self.0));
91        }
92    }
93}
94
95impl Neg for FloatOrd {
96    type Output = FloatOrd;
97
98    fn neg(self) -> Self::Output {
99        FloatOrd(-self.0)
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    const NAN: FloatOrd = FloatOrd(f32::NAN);
108    const ZERO: FloatOrd = FloatOrd(0.0);
109    const ONE: FloatOrd = FloatOrd(1.0);
110
111    #[test]
112    fn float_ord_eq() {
113        assert_eq!(NAN, NAN);
114
115        assert_ne!(NAN, ZERO);
116        assert_ne!(ZERO, NAN);
117
118        assert_eq!(ZERO, ZERO);
119    }
120
121    #[test]
122    fn float_ord_cmp() {
123        assert_eq!(NAN.cmp(&NAN), Ordering::Equal);
124
125        assert_eq!(NAN.cmp(&ZERO), Ordering::Less);
126        assert_eq!(ZERO.cmp(&NAN), Ordering::Greater);
127
128        assert_eq!(ZERO.cmp(&ZERO), Ordering::Equal);
129        assert_eq!(ONE.cmp(&ZERO), Ordering::Greater);
130        assert_eq!(ZERO.cmp(&ONE), Ordering::Less);
131    }
132
133    #[test]
134    #[expect(
135        clippy::nonminimal_bool,
136        reason = "This tests that all operators work as they should, and in the process requires some non-simplified boolean expressions."
137    )]
138    fn float_ord_cmp_operators() {
139        assert!(!(NAN < NAN));
140        assert!(NAN < ZERO);
141        assert!(!(ZERO < NAN));
142        assert!(!(ZERO < ZERO));
143        assert!(ZERO < ONE);
144        assert!(!(ONE < ZERO));
145
146        assert!(!(NAN > NAN));
147        assert!(!(NAN > ZERO));
148        assert!(ZERO > NAN);
149        assert!(!(ZERO > ZERO));
150        assert!(!(ZERO > ONE));
151        assert!(ONE > ZERO);
152
153        assert!(NAN <= NAN);
154        assert!(NAN <= ZERO);
155        assert!(!(ZERO <= NAN));
156        assert!(ZERO <= ZERO);
157        assert!(ZERO <= ONE);
158        assert!(!(ONE <= ZERO));
159
160        assert!(NAN >= NAN);
161        assert!(!(NAN >= ZERO));
162        assert!(ZERO >= NAN);
163        assert!(ZERO >= ZERO);
164        assert!(!(ZERO >= ONE));
165        assert!(ONE >= ZERO);
166    }
167
168    #[cfg(feature = "std")]
169    #[test]
170    fn float_ord_hash() {
171        let hash = |num| {
172            let mut h = std::hash::DefaultHasher::new();
173            FloatOrd(num).hash(&mut h);
174            h.finish()
175        };
176
177        assert_ne!((-0.0f32).to_bits(), 0.0f32.to_bits());
178        assert_eq!(hash(-0.0), hash(0.0));
179
180        let nan_1 = f32::from_bits(0b0111_1111_1000_0000_0000_0000_0000_0001);
181        assert!(nan_1.is_nan());
182        let nan_2 = f32::from_bits(0b0111_1111_1000_0000_0000_0000_0000_0010);
183        assert!(nan_2.is_nan());
184        assert_ne!(nan_1.to_bits(), nan_2.to_bits());
185        assert_eq!(hash(nan_1), hash(nan_2));
186    }
187}