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.
== is exact, and that is the problem
Section titled “== is exact, and that is the problem”fn main() { let sum = 0.1 + 0.2; println!("{}", sum == 0.3); println!("{:.20}", sum); println!("{:.20}", 0.3);}false0.300000000000000044410.29999999999999998890The 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.
Compare with an epsilon
Section titled “Compare with an epsilon”“Equal” for floats usually means “closer than any difference my calculation could plausibly produce”, and that is a comparison you write yourself:
fn nearly_equal(a: f64, b: f64) -> bool { if a == b { return true; } (a - b).abs() <= 1e-9 * a.abs().max(b.abs())}
fn main() { let mut total = 0.0; for _ in 0..10 { total += 0.1; } println!("{}", total == 1.0); println!("{:.20}", total); println!("{}", nearly_equal(total, 1.0)); println!("{}", nearly_equal(0.1 + 0.2, 0.3)); println!("{:e}", f64::EPSILON);}false0.99999999999999988898truetrue2.220446049250313e-16Adding 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”fn nearly_equal(a: f64, b: f64) -> bool { if a == b { return true; } (a - b).abs() <= 1e-9 * a.abs().max(b.abs())}
fn main() { println!("{}", (1e-12_f64 - 2e-12).abs() < 1e-9);
let mut big = 0.0; for _ in 0..1_000_000 { big += 0.1; } println!("{:.6}", big); println!("{:e}", (big - 100_000.0_f64).abs()); println!("{}", (big - 100_000.0_f64).abs() < 1e-9); println!("{}", nearly_equal(big, 100_000.0));}true100000.0000011.3328826753422618e-6falsetrueBoth 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.
NaN breaks every comparison
Section titled “NaN breaks every comparison”fn main() { let nan = f64::NAN; println!("{} {} {}", nan == nan, nan < 1.0, nan > 1.0); println!("{}", nan.is_nan()); println!("{}", (-1.0_f64).sqrt().is_nan()); println!("{}", (0.0_f64 / 0.0).is_nan());}false false falsetruetruetrueNaN 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):
fn main() { let mut xs = vec![2.0, f64::NAN, 1.0]; xs.sort_by(f64::total_cmp); println!("{xs:?}");}[1.0, 2.0, NaN]- How to round a float to a number of decimals — why
0.3was never really0.3; the previous article in this chapter. - What is the difference between
f32andf64? — the type all of this comparing runs on. - Working with numbers — the rest of the chapter.