Skip to content
Select theme

How to compare floats without surprises

Short answer: == compares the value actually stored, not the decimal you typed — and a computed float almost never lands exactly on the number you meant. So for “close enough”, compare the difference against an epsilon that scales with the size of the numbers. And never compare anything against f64::NAN: every comparison involving NaN is false, including x == x — test with is_nan() instead.

From a fresh project — cargo new compare-demo, then replace src/main.rs with each block below.

false
0.30000000000000004441
0.29999999999999998890

The comparison is not broken — == compares the two stored values exactly, and they genuinely are different values. Binary floats cannot represent most decimal fractions, so neither side holds “zero point three”: 0.1 + 0.2 accumulates into 0.30000000000000004441, while the literal 0.3 is stored as 0.29999999999999998890. Both print as 0.3 under ordinary formatting, which is what makes the trap look impossible — How to round a float to a number of decimals covers why the stored value and the printed one diverge. Comparisons do not see the printed form; they see the stored one.

“Equal” for floats usually means “closer than any difference my calculation could plausibly produce”, and that is a comparison you write yourself:

false
0.99999999999999988898
true
true
2.220446049250313e-16

Adding 0.1 ten times misses 1.0 by one part in 10¹⁶ — == says false, the helper says true. The guard clause matters: without it, nearly_equal(f64::INFINITY, f64::INFINITY) computes inf - inf, which is NaN, and the final comparison returns false for two values that are equal. The 1e-9 is a choice, not a constant from the standard library — pick it from the problem, as the largest difference you are willing to call a rounding artefact.

f64::EPSILON is in the standard library, but it is not “the epsilon for comparing floats”. It is the gap between 1.0 and the next f64 above it — about 2.2×10⁻¹⁶, fine as a building block for a scaled tolerance, useless as a bare absolute one. No single absolute tolerance works at every scale, which is the next section’s problem.

An absolute epsilon only works at one scale

Section titled “An absolute epsilon only works at one scale”
true
100000.000001
1.3328826753422618e-6
false
true

Both ends fail with the same absolute 1e-9. At the small end it declares 1e-12 and 2e-12 “equal” even though they differ by 100%. At the large end, summing 0.1 a million times drifts about 1.3×10⁻⁶ away from 100_000.0 — barely a rounding artefact at that magnitude, a relative error around 10⁻¹¹ — but the absolute epsilon calls it unequal. The fix is already in the helper: measure the difference relative to the size of the numbers, 1e-9 * a.abs().max(b.abs()). One tolerance, scaled, covers both ends. If the values can be vanishingly small, decide separately what “equal” means down there — relative comparison gets strict as the magnitude shrinks.

false false false
true
true
true

NaN comes from operations with no meaningful answer — dividing zero by zero, taking the square root of a negative number — and the IEEE 754 rule is brutal: NaN is unordered with everything. Every ==, <, <=, >, >= involving a NaN is false, including nan == nan. That kills the obvious patterns: if x == f64::NAN never runs its branch, and x != x is the accidental NaN test that works but reads like a typo. is_nan() says what it does.

There is one deeper consequence. Because NaN has no place in an ordering, floats implement PartialOrd and not Ord — which is why a Vec<f64> has no plain .sort(). If you need to sort, total_cmp provides a total order that handles NaN deterministically (a positive NaN sorts above everything, +infinity included):

[1.0, 2.0, NaN]