use core::iter::zip;
pub trait ApproxEq<Other: ?Sized = Self, Epsilon = Self> {
fn approx_eq(&self, other: &Other) -> bool {
self.approx_eq_eps(other, &Self::relative_epsilon())
}
fn approx_eq_eps(&self, other: &Other, rel_eps: &Epsilon) -> bool;
fn relative_epsilon() -> Epsilon;
}
impl ApproxEq for f32 {
fn approx_eq_eps(&self, other: &Self, rel_eps: &Self) -> bool {
let diff = (self - other).abs();
diff <= *rel_eps * self.abs().max(1.0)
}
fn relative_epsilon() -> Self {
if cfg!(any(feature = "std", feature = "libm")) {
1e-6
} else {
5e-3
}
}
}
impl<E, T: Sized + ApproxEq<T, E>> ApproxEq<Self, E> for [T] {
fn approx_eq_eps(&self, other: &Self, rel_eps: &E) -> bool {
self.len() == other.len()
&& zip(self, other).all(|(s, o)| s.approx_eq_eps(o, rel_eps))
}
fn relative_epsilon() -> E {
T::relative_epsilon()
}
}
impl<E, T: Sized + ApproxEq<T, E>, const N: usize> ApproxEq<Self, E>
for [T; N]
{
fn approx_eq_eps(&self, other: &Self, rel_eps: &E) -> bool {
self.as_slice().approx_eq_eps(other, rel_eps)
}
fn relative_epsilon() -> E {
T::relative_epsilon()
}
}
impl<E, T: ApproxEq<T, E>> ApproxEq<Self, E> for Option<T> {
fn approx_eq_eps(&self, other: &Self, rel_eps: &E) -> bool {
match (self, other) {
(Some(s), Some(o)) => s.approx_eq_eps(o, rel_eps),
(Some(_), None) | (None, Some(_)) => false,
(None, None) => true,
}
}
fn relative_epsilon() -> E {
T::relative_epsilon()
}
}
#[macro_export]
macro_rules! assert_approx_eq {
($a:expr, $b:expr) => {
match (&$a, &$b) {
(a, b) => $crate::assert_approx_eq!(
*a, *b,
"assertion failed: `{a:?} ≅ {b:?}`"
)
}
};
($a:expr, $b:expr, eps = $eps:literal) => {
match (&$a, &$b) {
(a, b) => $crate::assert_approx_eq!(
*a, *b, eps = $eps,
"assertion failed: `{a:?} ≅ {b:?}`"
)
}
};
($a:expr, $b:expr, $fmt:literal $(, $args:expr)*) => {{
use $crate::math::ApproxEq;
match (&$a, &$b) {
(a, b) => assert!(ApproxEq::approx_eq(a, b), $fmt $(, $args)*)
}
}};
($a:expr, $b:expr, eps = $eps:literal, $fmt:literal $(, $args:expr)*) => {{
use $crate::math::ApproxEq;
match (&$a, &$b) {
(a, b) => assert!(
ApproxEq::approx_eq_eps(a, b, &$eps),
$fmt $(, $args)*
)
}
}};
}
#[cfg(test)]
mod tests {
mod f32 {
#[test]
fn approx_eq_zero() {
assert_approx_eq!(0.0, 0.0);
assert_approx_eq!(-0.0, 0.0);
assert_approx_eq!(0.0, -0.0);
}
#[test]
fn approx_eq_positive() {
assert_approx_eq!(0.0, 0.0000001);
assert_approx_eq!(0.0000001, 0.0);
assert_approx_eq!(0.9999999, 1.0);
assert_approx_eq!(1.0, 1.0000001);
assert_approx_eq!(1.0e10, 1.0000001e10);
}
#[test]
fn approx_eq_negative() {
assert_approx_eq!(0.0, -0.0000001);
assert_approx_eq!(-0.0000001, 0.0);
assert_approx_eq!(-1.0, -1.0000001);
assert_approx_eq!(-0.9999999, -1.0);
assert_approx_eq!(-1.0e10, -1.0000001e10);
}
#[test]
fn approx_eq_custom_epsilon() {
assert_approx_eq!(0.0, 0.001, eps = 0.01);
assert_approx_eq!(0.0, -0.001, eps = 0.01);
assert_approx_eq!(1.0, 0.999, eps = 0.01);
assert_approx_eq!(100.0, 99.9, eps = 0.01);
}
#[test]
#[should_panic]
fn zero_not_approx_eq_to_one() {
assert_approx_eq!(0.0, 1.0);
}
#[test]
#[should_panic]
fn one_not_approx_eq_to_1_01() {
if cfg!(any(feature = "std", feature = "libm")) {
assert_approx_eq!(1.0, 1.00001);
} else {
assert_approx_eq!(1.0, 1.01);
}
}
#[test]
#[should_panic]
fn inf_not_approx_eq_to_inf() {
assert_approx_eq!(f32::INFINITY, f32::INFINITY);
}
#[test]
#[should_panic]
fn nan_not_approx_eq_to_nan() {
assert_approx_eq!(f32::NAN, f32::NAN);
}
}
}