diff --git a/egui/src/math.rs b/egui/src/math.rs index df008231..4eda85a0 100644 --- a/egui/src/math.rs +++ b/egui/src/math.rs @@ -86,7 +86,8 @@ pub fn round_to_precision(value: f32, decimal_places: usize) -> f32 { pub fn format_with_minimum_precision(value: f32, precision: usize) -> String { let text = format!("{:.*}", precision, value); - if (text.parse::().unwrap() - value).abs() <= std::f32::EPSILON { + let epsilon = 16.0 * f32::EPSILON; // margin large enough to handle most peoples round-tripping needs + if almost_equal(text.parse::().unwrap(), value, epsilon) { // Enough precision to show the value accurately - good! text } else { @@ -96,3 +97,52 @@ pub fn format_with_minimum_precision(value: f32, precision: usize) -> String { value.to_string() } } + +/// Should return true when arguments are the same within some rounding error. +/// For instance `almost_equal(x, x.to_degrees().to_radians(), f32::EPSILON)` should hold true for all x. +/// The `epsilon` can be `f32::EPSILON` to handle simple transforms (like degrees -> radians) +/// but should be higher to handle more complex transformations. +pub fn almost_equal(a: f32, b: f32, epsilon: f32) -> bool { + #![allow(clippy::float_cmp)] + + if a == b { + true // handle infinites + } else { + let abs_max = a.abs().max(b.abs()); + abs_max <= epsilon || ((a - b).abs() / abs_max) <= epsilon + } +} + +#[test] +fn test_almost_equal() { + for &x in &[ + 0.0_f32, + f32::MIN_POSITIVE, + 1e-20, + 1e-10, + f32::EPSILON, + 0.1, + 0.99, + 1.0, + 1.001, + 1e10, + f32::MAX / 100.0, + // f32::MAX, // overflows in rad<->deg test + f32::INFINITY, + ] { + for &x in &[-x, x] { + for roundtrip in &[ + |x: f32| x.to_degrees().to_radians(), + |x: f32| x.to_radians().to_degrees(), + ] { + let epsilon = f32::EPSILON; + assert!( + almost_equal(x, roundtrip(x), epsilon), + "{} vs {}", + x, + roundtrip(x) + ); + } + } + } +}