1use 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}