We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Ground Rules: Values, Types, Control Flow step 8 of 24
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 —sortrequiresOrd. You usesort_by(f64::total_cmp)instead. -
HashMap<f64, _>does not compile — keys requireEqandHash. -
[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.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.