retrofire_core/math/
approx.rs

1//! Testing and asserting approximate equality.
2
3use core::iter::zip;
4
5/// Trait for testing approximate equality.
6///
7/// Floating-point types are only an approximation of real numbers due to their
8/// finite precision. The presence of rounding errors means that two floats may
9/// not compare equal even if their counterparts in ℝ would. Even such a simple
10/// expression as `0.1 + 0.2 == 0.3` will evaluate to false due to precision
11/// issues.
12///
13/// Approximate equality is a more robust way to compare floating-point values
14/// than strict equality. Two values are considered approximately equal if their
15/// absolute difference is less than some small value, "epsilon". The choice of
16/// the epsilon value is not an exact science, and depends on how much error
17/// has accrued in the computation of the values.
18///
19/// Moreover, due to the nature of floating point, a naive comparison against
20/// a fixed value does not work well. Rather, the epsilon should be *relative*
21/// to the magnitude of the values being compared.
22pub trait ApproxEq<Other: ?Sized = Self, Epsilon = Self> {
23    /// Returns whether `self` and `other` are approximately equal.
24    /// Uses the epsilon returned by [`Self::relative_epsilon`].
25    fn approx_eq(&self, other: &Other) -> bool {
26        self.approx_eq_eps(other, &Self::relative_epsilon())
27    }
28
29    /// Returns whether `self` and `other` are approximately equal,
30    /// using the relative epsilon `rel_eps`.
31    fn approx_eq_eps(&self, other: &Other, rel_eps: &Epsilon) -> bool;
32
33    /// Returns the default relative epsilon of type `E`.
34    fn relative_epsilon() -> Epsilon;
35}
36
37impl ApproxEq for f32 {
38    fn approx_eq_eps(&self, other: &Self, rel_eps: &Self) -> bool {
39        let diff = (self - other).abs();
40        diff <= *rel_eps * self.abs().max(1.0)
41    }
42
43    fn relative_epsilon() -> Self {
44        if cfg!(any(feature = "std", feature = "libm")) {
45            1e-6
46        } else {
47            5e-3
48        }
49    }
50}
51
52impl<E, T: Sized + ApproxEq<T, E>> ApproxEq<Self, E> for [T] {
53    fn approx_eq_eps(&self, other: &Self, rel_eps: &E) -> bool {
54        self.len() == other.len()
55            && zip(self, other).all(|(s, o)| s.approx_eq_eps(o, rel_eps))
56    }
57    fn relative_epsilon() -> E {
58        T::relative_epsilon()
59    }
60}
61
62impl<E, T: Sized + ApproxEq<T, E>, const N: usize> ApproxEq<Self, E>
63    for [T; N]
64{
65    fn approx_eq_eps(&self, other: &Self, rel_eps: &E) -> bool {
66        self.as_slice().approx_eq_eps(other, rel_eps)
67    }
68    fn relative_epsilon() -> E {
69        T::relative_epsilon()
70    }
71}
72
73impl<E, T: ApproxEq<T, E>> ApproxEq<Self, E> for Option<T> {
74    fn approx_eq_eps(&self, other: &Self, rel_eps: &E) -> bool {
75        match (self, other) {
76            (Some(s), Some(o)) => s.approx_eq_eps(o, rel_eps),
77            (Some(_), None) | (None, Some(_)) => false,
78            (None, None) => true,
79        }
80    }
81
82    fn relative_epsilon() -> E {
83        T::relative_epsilon()
84    }
85}
86
87/// Asserts that two values are approximately equal.
88/// Requires that the left operand has an applicable [`ApproxEq`] impl
89/// and that both operands impl `Debug` unless a custom message is given.
90///
91/// # Panics
92///
93/// If the given values are not approximately equal.
94///
95/// # Examples
96/// `assert_eq` would fail, but `assert_approx_eq` passes:
97/// ```
98/// # use retrofire_core::assert_approx_eq;
99/// assert_ne!(0.1 + 0.2, 0.3);
100/// assert_approx_eq!(0.1 + 0.2, 0.3);
101/// ```
102/// A relative epsilon is used:
103/// ```
104/// # use retrofire_core::assert_approx_eq;
105/// assert_ne!(1e7, 1e7 + 1.0);
106/// assert_approx_eq!(1e7, 1e7 + 1.0);
107/// ```
108/// A custom epsilon can be given:
109/// ```
110/// # use retrofire_core::assert_approx_eq;
111/// assert_approx_eq!(100.0, 101.0, eps = 0.01);
112/// ```
113/// Like `assert_eq`, this macro supports custom panic messages.
114/// The epsilon, if present, must come before the format string.
115/// ```should_panic
116/// # use std::f32;
117/// # use retrofire_core::assert_approx_eq;
118/// assert_approx_eq!(f32::sin(3.14), 0.0, eps = 0.001,
119///     "3.14 is not a good approximation of {}!", f32::consts::PI);
120/// ```
121#[macro_export]
122macro_rules! assert_approx_eq {
123    ($a:expr, $b:expr) => {
124        match (&$a, &$b) {
125            (a, b) => $crate::assert_approx_eq!(
126                *a, *b,
127                "assertion failed: `{a:?} ≅ {b:?}`"
128            )
129        }
130    };
131    ($a:expr, $b:expr, eps = $eps:literal) => {
132        match (&$a, &$b) {
133            (a, b) => $crate::assert_approx_eq!(
134                *a, *b, eps = $eps,
135                "assertion failed: `{a:?} ≅ {b:?}`"
136            )
137        }
138    };
139    ($a:expr, $b:expr, $fmt:literal $(, $args:expr)*) => {{
140        use $crate::math::ApproxEq;
141        match (&$a, &$b) {
142            (a, b) => assert!(ApproxEq::approx_eq(a, b), $fmt $(, $args)*)
143        }
144    }};
145    ($a:expr, $b:expr, eps = $eps:literal, $fmt:literal $(, $args:expr)*) => {{
146        use $crate::math::ApproxEq;
147        match (&$a, &$b) {
148            (a, b) => assert!(
149                ApproxEq::approx_eq_eps(a, b, &$eps),
150                $fmt $(, $args)*
151            )
152        }
153    }};
154}
155
156#[cfg(test)]
157mod tests {
158
159    mod f32 {
160        #[test]
161        fn approx_eq_zero() {
162            assert_approx_eq!(0.0, 0.0);
163            assert_approx_eq!(-0.0, 0.0);
164            assert_approx_eq!(0.0, -0.0);
165        }
166
167        #[test]
168        fn approx_eq_positive() {
169            assert_approx_eq!(0.0, 0.0000001);
170            assert_approx_eq!(0.0000001, 0.0);
171            assert_approx_eq!(0.9999999, 1.0);
172            assert_approx_eq!(1.0, 1.0000001);
173            assert_approx_eq!(1.0e10, 1.0000001e10);
174        }
175
176        #[test]
177        fn approx_eq_negative() {
178            assert_approx_eq!(0.0, -0.0000001);
179            assert_approx_eq!(-0.0000001, 0.0);
180            assert_approx_eq!(-1.0, -1.0000001);
181            assert_approx_eq!(-0.9999999, -1.0);
182            assert_approx_eq!(-1.0e10, -1.0000001e10);
183        }
184
185        #[test]
186        fn approx_eq_custom_epsilon() {
187            assert_approx_eq!(0.0, 0.001, eps = 0.01);
188            assert_approx_eq!(0.0, -0.001, eps = 0.01);
189            assert_approx_eq!(1.0, 0.999, eps = 0.01);
190            assert_approx_eq!(100.0, 99.9, eps = 0.01);
191        }
192
193        #[test]
194        #[should_panic]
195        fn zero_not_approx_eq_to_one() {
196            assert_approx_eq!(0.0, 1.0);
197        }
198
199        #[test]
200        #[should_panic]
201        fn one_not_approx_eq_to_1_01() {
202            if cfg!(any(feature = "std", feature = "libm")) {
203                assert_approx_eq!(1.0, 1.00001);
204            } else {
205                assert_approx_eq!(1.0, 1.01);
206            }
207        }
208
209        #[test]
210        #[should_panic]
211        fn inf_not_approx_eq_to_inf() {
212            assert_approx_eq!(f32::INFINITY, f32::INFINITY);
213        }
214
215        #[test]
216        #[should_panic]
217        fn nan_not_approx_eq_to_nan() {
218            assert_approx_eq!(f32::NAN, f32::NAN);
219        }
220    }
221}