Skip to content

← Ground Rules: Values, Types, Control Flow step 8 of 24

Medium Primitives

Floating point: f32, f64, NaN, and why == is a trap

For each pair of floats, say whether they are equal to within a tolerance.

pub fn approx_equal(pairs: &[(f64, f64)], tol: f64) -> Vec<bool>

A pair counts as equal when the absolute difference is at most tol. A pair where either value is NaN is never equal — not even NaN against itself.

0.1 + 0.2 against 0.3 with tol = 1e-9 is true. (NaN, NaN) is false. (1.0, 1.0) with tol = 0.0 is true.

The starter fails the gate, on purpose.

0.1 + 0.2 != 0.3, and that is not a bug

f64 stores numbers as sign, exponent and a 52-bit fraction — in base two. One tenth is not representable in base two any more than one third is in base ten. So 0.1 is really 0.1000000000000000055511151231257827…, 0.2 is similarly off, and the sum lands on 0.30000000000000004, which is a different f64 from 0.3.

The error is around 5.6 × 10⁻¹⁷ — utterly harmless, and fatal to ==. Every float comparison in real code is therefore a comparison against a tolerance, and picking the tolerance is a judgement about the domain, not a constant you can copy.

The lint, and how it got here

Clippy’s float_cmp fires on a == b for floats and says “consider comparing them within some margin of error”. But it lives in the pedantic group, which is off by default — meaning that on a plain clippy run the single most common float mistake produces no output at all.

So the starter opts in, on its very first line:

#![warn(clippy::float_cmp)]

That is an inner attribute (#! rather than #), which applies to the whole file and therefore has to be the first thing in it. Because the gate runs -D warnings, a lint you have merely warn-ed is promoted to an error. Leave the line where it is; it is doing the teaching.

Two side notes that will save you later. Plain rustc silently ignores clippy:: lint names, so a file carrying this attribute still compiles fine without clippy installed. And float_cmp deliberately does not fire when one side is a literal 0.0, because comparing against exact zero is sometimes genuinely what you mean.

NaN breaks more than you think

NaN — “not a number” — is what you get from 0.0 / 0.0, from f64::INFINITY - f64::INFINITY, from the square root of a negative. Its defining property is that every comparison involving it is false:

f64::NAN == f64::NAN   // false
f64::NAN <  1.0        // false
f64::NAN >= 1.0        // false

This is not a Rust quirk; it is IEEE 754, and every language has it. What Rust does differently is encode the consequence in its type system. Because NaN makes < non-total, f64 implements PartialOrd but not Ord, and PartialEq but not Eq. From that single fact follow a whole family of errors you will meet much later and would otherwise find arbitrary:

  • vec_of_floats.sort() does not compile — sort requires Ord. You use sort_by(f64::total_cmp) instead.
  • HashMap<f64, _> does not compile — keys require Eq and Hash.
  • [1.0, 2.0].iter().max() does not compile, for the same reason.

Remembering why is much cheaper than remembering the list.

The nice part of this problem: you do not need a special case for NaN. Work out what (NaN - x).abs() <= tol evaluates to, and you will find the requirement falls out of IEEE 754 by itself.

About the test data

JSON has no way to write NaN, so the harness accepts the strings "NaN", "inf" and "-inf" and turns them into the corresponding f64 before calling you. Inside your function they are ordinary floats.