use std::fmt;
pub fn ulp_distance(a: f64, b: f64) -> Option<u64> {
if a.is_nan() || b.is_nan() {
None
} else {
Some(ordered_key(a).abs_diff(ordered_key(b)))
}
}
fn ordered_key(value: f64) -> i64 {
let bits = value.to_bits().cast_signed();
if bits < 0 { i64::MIN - bits } else { bits }
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Tolerance {
Bitwise,
Ulps(u64),
Mixed {
absolute: f64,
relative: f64,
},
}
impl Tolerance {
pub fn check(self, actual: f64, expected: f64) -> Result<(), String> {
let passed = match self {
Self::Bitwise => actual.to_bits() == expected.to_bits(),
Self::Ulps(limit) => ulp_distance(actual, expected).is_some_and(|d| d <= limit),
Self::Mixed { absolute, relative } => {
assert!(
absolute.is_finite()
&& absolute >= 0.0
&& relative.is_finite()
&& relative >= 0.0,
"invalid tolerance {self:?}"
);
let scale = actual.abs().max(expected.abs());
actual.is_finite()
&& expected.is_finite()
&& (actual - expected).abs() <= absolute.max(relative * scale)
}
};
if passed {
Ok(())
} else {
Err(Mismatch {
actual,
expected,
tolerance: self,
}
.to_string())
}
}
}
#[track_caller]
pub fn assert_close(actual: f64, expected: f64, tolerance: Tolerance) {
if let Err(report) = tolerance.check(actual, expected) {
panic!("{report}");
}
}
struct Mismatch {
actual: f64,
expected: f64,
tolerance: Tolerance,
}
impl fmt::Display for Mismatch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let distance = match ulp_distance(self.actual, self.expected) {
Some(d) => d.to_string(),
None => "undefined (NaN)".to_owned(),
};
write!(
f,
"values differ beyond {:?}: actual {:?} ({:#018x}), expected {:?} ({:#018x}), \
{distance} ULPs apart, on {}-{}",
self.tolerance,
self.actual,
self.actual.to_bits(),
self.expected,
self.expected.to_bits(),
std::env::consts::ARCH,
std::env::consts::OS,
)
}
}