lox_math/
is_close.rs

1/*
2 * Copyright (c) 2024. Helge Eichhorn and the LOX contributors
3 *
4 * This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, you can obtain one at https://mozilla.org/MPL/2.0/.
7 */
8
9use glam::DVec3;
10
11pub trait IsClose {
12    const DEFAULT_RELATIVE: f64;
13    const DEFAULT_ABSOLUTE: f64;
14
15    fn is_close_with_tolerances(&self, rhs: &Self, rel_tol: f64, abs_tol: f64) -> bool;
16
17    fn is_close_abs(&self, rhs: &Self, abs_tol: f64) -> bool {
18        self.is_close_with_tolerances(rhs, Self::DEFAULT_RELATIVE, abs_tol)
19    }
20
21    fn is_close_rel(&self, rhs: &Self, rel_tol: f64) -> bool {
22        self.is_close_with_tolerances(rhs, rel_tol, Self::DEFAULT_ABSOLUTE)
23    }
24
25    fn is_close(&self, rhs: &Self) -> bool {
26        self.is_close_with_tolerances(rhs, Self::DEFAULT_RELATIVE, Self::DEFAULT_ABSOLUTE)
27    }
28}
29
30impl IsClose for f64 {
31    const DEFAULT_RELATIVE: f64 = 1e-8;
32
33    const DEFAULT_ABSOLUTE: f64 = 0.0;
34
35    fn is_close_with_tolerances(&self, rhs: &Self, rel_tol: f64, abs_tol: f64) -> bool {
36        (self - rhs).abs() <= f64::max(rel_tol * f64::max(self.abs(), rhs.abs()), abs_tol)
37    }
38}
39
40impl IsClose for DVec3 {
41    const DEFAULT_RELATIVE: f64 = 1e-8;
42    const DEFAULT_ABSOLUTE: f64 = 0.0;
43
44    fn is_close_with_tolerances(&self, rhs: &Self, rel_tol: f64, abs_tol: f64) -> bool {
45        self.x.is_close_with_tolerances(&rhs.x, rel_tol, abs_tol)
46            && self.y.is_close_with_tolerances(&rhs.y, rel_tol, abs_tol)
47            && self.z.is_close_with_tolerances(&rhs.z, rel_tol, abs_tol)
48    }
49}
50
51#[macro_export]
52macro_rules! assert_close {
53    ($lhs:expr, $rhs:expr) => {
54        assert!($lhs.is_close(&$rhs), "{:?} ≉ {:?}", $lhs, $rhs);
55    };
56    ($lhs:expr, $rhs:expr, $abs_tol:expr) => {
57        assert!(
58            $lhs.is_close_abs(&$rhs, $abs_tol),
59            "{:?} ≉ {:?}",
60            $lhs,
61            $rhs
62        );
63    };
64    ($lhs:expr, $rhs:expr, $abs_tol:expr, $rel_tol:expr) => {
65        assert!(
66            $lhs.is_close_with_tolerances(&$rhs, $rel_tol, $abs_tol),
67            "{:?} ≉ {:?}",
68            $lhs,
69            $rhs
70        );
71    };
72}
73
74#[cfg(test)]
75mod tests {
76    use rstest::rstest;
77
78    use super::*;
79
80    #[rstest]
81    #[case(1.0, 1.0 + f64::EPSILON, true)]
82    #[case(0.0, 0.0 + f64::EPSILON, false)]
83    fn test_is_close_f64(#[case] a: f64, #[case] b: f64, #[case] expected: bool) {
84        assert_eq!(a.is_close(&b), expected);
85    }
86
87    #[rstest]
88    #[case(1.0, 1.0 + f64::EPSILON, 0.0, true)]
89    #[case(0.0, 0.0 + f64::EPSILON, 2.0 * f64::EPSILON, true)]
90    fn test_is_close_f64_abs(
91        #[case] a: f64,
92        #[case] b: f64,
93        #[case] abs_tol: f64,
94        #[case] expected: bool,
95    ) {
96        assert_eq!(a.is_close_abs(&b, abs_tol), expected);
97    }
98
99    #[rstest]
100    #[case(1.0, 1.0 + f64::EPSILON, 0.0, false)]
101    #[case(0.0, 0.0 + f64::EPSILON, 2.0 * f64::EPSILON, false)]
102    fn test_is_close_f64_rel(
103        #[case] a: f64,
104        #[case] b: f64,
105        #[case] rel_tol: f64,
106        #[case] expected: bool,
107    ) {
108        assert_eq!(a.is_close_rel(&b, rel_tol), expected);
109    }
110
111    #[test]
112    fn test_assert_close() {
113        assert_close!(1.0, 1.0 + f64::EPSILON);
114        assert_close!(0.0, 0.0 + f64::EPSILON, 2.0 * f64::EPSILON);
115        assert_close!(0.0, 0.0 + f64::EPSILON, 2.0 * f64::EPSILON, 0.0);
116    }
117}