Assert_eq! for float numbers

When I wrote/write unit tests I wonder why no assert_near! macro, similar for
ASSERT_NEAR(val1, val2, abs_error) from googletest testing library for C++.

And today I found in f64.floor documentation such code example:

let f = 3.99_f64;
let g = 3.0_f64;

assert_eq!(f.floor(), 3.0);
assert_eq!(g.floor(), 3.0);

I wonder is assert_eq have some magic to work around for floating point accuracy problems which forces to avoid direct compare of floats.

But after inspecting assert_eq!, I can not see special case for floats (f32 and f64).

So, is any reason to use == for f64 instead of (f1 - f2).abs() < epsilon,
may be some magic in implementation of PartialEq for f64?

1 Like

There was a discussion here about increasing the number of utility macros, but it was decided that they probably don't belong in the standard library. So these kinds of macros will need to be written for your use case or by finding an appropriate crate on crates.io.

After a cursory look in crates.io I found this crate assert_approx_eq.

3 Likes

Now this is one of the (rare) cases where floating point equality actually makes sense.
An f64 can represent integers up to 9_007_199_254_740_992 exactly and I expect floor to return that exact value, not just something close. If it doesn't, I'd consider it a bug.

1 Like

Right, the main reason you don't want a standard assert_eq, (as far as I understand it) is that the way you test float equality differs depending on which calculations you're actually doing. For floor, == works because it is supposed to be returning only a single exact value. Other calculations might want to take error into account.

With that said, (f1 - f2).abs() < epsilon is almost never what you want. For one, it doesn't handle NaN/Infinity at all. And your epsilon needs to be recomputed if you're working with different number scales. If you use the wrong epsilon, you're not really asserting anything meaningful.

Here's the function I've been using, for reference:

pub fn nearly_equal(a: f32, b: f32) -> bool {
	let abs_a = a.abs();
	let abs_b = b.abs();
	let diff = (a - b).abs();

	if a == b { // Handle infinities.
		true
	} else if a == 0.0 || b == 0.0 || diff < f32::MIN_POSITIVE {
		// One of a or b is zero (or both are extremely close to it,) use absolute error.
		diff < (f32::EPSILON * f32::MIN_POSITIVE)
	} else { // Use relative error.
		(diff / f32::min(abs_a + abs_b, f32::MAX)) < f32::EPSILON
	}
}

I won't tell you this is correct. I have no clue; floating point is hard. But it is at least informative.

1 Like