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        use super::float::f32;
40        let diff = f32::abs(self - other);
41        diff <= *rel_eps * f32::abs(*self).max(1.0)
42    }
43
44    fn relative_epsilon() -> Self {
45        if cfg!(any(feature = "std", feature = "libm")) {
46            1e-6
47        } else {
48            5e-3
49        }
50    }
51}
52
53impl<E, T: Sized + ApproxEq<T, E>> ApproxEq<Self, E> for [T] {
54    fn approx_eq_eps(&self, other: &Self, rel_eps: &E) -> bool {
55        self.len() == other.len()
56            && zip(self, other).all(|(s, o)| s.approx_eq_eps(o, rel_eps))
57    }
58    fn relative_epsilon() -> E {
59        T::relative_epsilon()
60    }
61}
62
63impl<E, T: Sized + ApproxEq<T, E>, const N: usize> ApproxEq<Self, E>
64    for [T; N]
65{
66    fn approx_eq_eps(&self, other: &Self, rel_eps: &E) -> bool {
67        self.as_slice().approx_eq_eps(other, rel_eps)
68    }
69    fn relative_epsilon() -> E {
70        T::relative_epsilon()
71    }
72}
73
74impl<E, T: ApproxEq<T, E>> ApproxEq<Self, E> for Option<T> {
75    fn approx_eq_eps(&self, other: &Self, rel_eps: &E) -> bool {
76        match (self, other) {
77            (Some(s), Some(o)) => s.approx_eq_eps(o, rel_eps),
78            (Some(_), None) | (None, Some(_)) => false,
79            (None, None) => true,
80        }
81    }
82
83    fn relative_epsilon() -> E {
84        T::relative_epsilon()
85    }
86}
87
88/// Asserts that two values are approximately equal.
89/// Requires that the left operand has an applicable [`ApproxEq`] impl
90/// and that both operands impl `Debug` unless a custom message is given.
91///
92/// # Panics
93///
94/// If the given values are not approximately equal.
95///
96/// # Examples
97/// `assert_eq` would fail, but `assert_approx_eq` passes:
98/// ```
99/// # use retrofire_core::assert_approx_eq;
100/// assert_ne!(0.1 + 0.2, 0.3);
101/// assert_approx_eq!(0.1 + 0.2, 0.3);
102/// ```
103/// A relative epsilon is used:
104/// ```
105/// # use retrofire_core::assert_approx_eq;
106/// assert_ne!(1e7, 1e7 + 1.0);
107/// assert_approx_eq!(1e7, 1e7 + 1.0);
108/// ```
109/// A custom epsilon can be given:
110/// ```
111/// # use retrofire_core::assert_approx_eq;
112/// assert_approx_eq!(100.0, 101.0, eps = 0.01);
113/// ```
114/// Like `assert_eq`, this macro supports custom panic messages.
115/// The epsilon, if present, must come before the format string.
116/// ```should_panic
117/// # use std::f32;
118/// # use retrofire_core::assert_approx_eq;
119/// assert_approx_eq!(f32::sin(3.14), 0.0, eps = 0.001,
120///     "3.14 is not a good approximation of {}!", f32::consts::PI);
121/// ```
122#[macro_export]
123macro_rules! assert_approx_eq {
124    ($a:expr, $b:expr) => {
125        match (&$a, &$b) {
126            (a, b) => $crate::assert_approx_eq!(
127                *a, *b,
128                "assertion failed: `{a:?} ≅ {b:?}`"
129            )
130        }
131    };
132    ($a:expr, $b:expr, eps = $eps:literal) => {
133        match (&$a, &$b) {
134            (a, b) => $crate::assert_approx_eq!(
135                *a, *b, eps = $eps,
136                "assertion failed: `{a:?} ≅ {b:?}`"
137            )
138        }
139    };
140    ($a:expr, $b:expr, $fmt:literal $(, $args:expr)*) => {{
141        use $crate::math::approx::ApproxEq;
142        match (&$a, &$b) {
143            (a, b) => assert!(ApproxEq::approx_eq(a, b), $fmt $(, $args)*)
144        }
145    }};
146    ($a:expr, $b:expr, eps = $eps:literal, $fmt:literal $(, $args:expr)*) => {{
147        use $crate::math::approx::ApproxEq;
148        match (&$a, &$b) {
149            (a, b) => assert!(
150                ApproxEq::approx_eq_eps(a, b, &$eps),
151                $fmt $(, $args)*
152            )
153        }
154    }};
155}
156
157#[cfg(test)]
158mod tests {
159
160    mod f32 {
161        #[test]
162        fn approx_eq_zero() {
163            assert_approx_eq!(0.0, 0.0);
164            assert_approx_eq!(-0.0, 0.0);
165            assert_approx_eq!(0.0, -0.0);
166        }
167
168        #[test]
169        fn approx_eq_positive() {
170            assert_approx_eq!(0.0, 0.0000001);
171            assert_approx_eq!(0.0000001, 0.0);
172            assert_approx_eq!(0.9999999, 1.0);
173            assert_approx_eq!(1.0, 1.0000001);
174            assert_approx_eq!(1.0e10, 1.0000001e10);
175        }
176
177        #[test]
178        fn approx_eq_negative() {
179            assert_approx_eq!(0.0, -0.0000001);
180            assert_approx_eq!(-0.0000001, 0.0);
181            assert_approx_eq!(-1.0, -1.0000001);
182            assert_approx_eq!(-0.9999999, -1.0);
183            assert_approx_eq!(-1.0e10, -1.0000001e10);
184        }
185
186        #[test]
187        fn approx_eq_custom_epsilon() {
188            assert_approx_eq!(0.0, 0.001, eps = 0.01);
189            assert_approx_eq!(0.0, -0.001, eps = 0.01);
190            assert_approx_eq!(1.0, 0.999, eps = 0.01);
191            assert_approx_eq!(100.0, 99.9, eps = 0.01);
192        }
193
194        #[test]
195        #[should_panic]
196        fn zero_not_approx_eq_to_one() {
197            assert_approx_eq!(0.0, 1.0);
198        }
199        #[test]
200        #[should_panic]
201        fn one_not_approx_eq_to_1_00001() {
202            assert_approx_eq!(1.0, 1.00001);
203        }
204        #[test]
205        #[should_panic]
206        fn inf_not_approx_eq_to_inf() {
207            assert_approx_eq!(f32::INFINITY, f32::INFINITY);
208        }
209        #[test]
210        #[should_panic]
211        fn nan_not_approx_eq_to_nan() {
212            assert_approx_eq!(f32::NAN, f32::NAN);
213        }
214    }
215}