pub const DEFAULT_RELATIVE_TOLERANCE: f64 = 1e-9;
pub const DEFAULT_ABSOLUTE_TOLERANCE: f64 = 1e-15;
#[must_use]
pub fn approx_equal(a: f64, b: f64) -> bool {
approx_equal_with(a, b, DEFAULT_RELATIVE_TOLERANCE, DEFAULT_ABSOLUTE_TOLERANCE)
}
#[must_use]
pub fn approx_equal_with(a: f64, b: f64, relative: f64, absolute: f64) -> bool {
if a.is_nan() || b.is_nan() {
return false;
}
if a.is_infinite() || b.is_infinite() {
return a.is_infinite() && b.is_infinite() && a.is_sign_positive() == b.is_sign_positive();
}
let difference = (a - b).abs();
difference <= absolute || difference <= relative * a.abs().max(b.abs())
}
#[track_caller]
pub fn assert_approx_equal(actual: f64, expected: f64) {
assert!(
approx_equal(actual, expected),
"approx_equal failed\n actual: {actual:?}\n expected: {expected:?}\n \
absolute error: {:e}\n relative error: {:e}",
(actual - expected).abs(),
(actual - expected).abs() / actual.abs().max(expected.abs()),
);
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::{approx_equal, approx_equal_with};
#[rstest]
fn approx_equal_scales_with_magnitude() {
assert!(approx_equal(1e6 + 1e-4, 1e6));
assert!(approx_equal(1e-4 + 1e-14, 1e-4));
}
#[rstest]
fn approx_equal_still_separates_real_differences() {
assert!(!approx_equal(1.0, 1.000_1));
assert!(!approx_equal(1e6, 1.001e6));
}
#[rstest]
fn approx_equal_handles_zero_and_non_finite() {
assert!(approx_equal(0.0, -0.0));
assert!(approx_equal(f64::INFINITY, f64::INFINITY));
assert!(!approx_equal(f64::INFINITY, f64::NEG_INFINITY));
assert!(!approx_equal(f64::NAN, f64::NAN));
}
#[rstest]
fn approx_equal_with_honours_explicit_tolerances() {
assert!(approx_equal_with(100.0, 100.000_001, 1e-7, 0.0));
assert!(!approx_equal_with(100.0, 100.000_1, 1e-7, 0.0));
assert!(approx_equal_with(0.0, 1e-13, 0.0, 1e-12));
}
}