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