6.4Comparison operators and float equality
Lesson 4.6 introduced the comparison operators "gently" and promised them a full lesson in chapter 6. Lesson 4.5 went further and promised a tool: the professional technique for comparing floats, owed to you since the words "until then, the discipline is just: don't." Both debts come due in this lesson.
The six, formally
| Operator | true when... |
|---|---|
x == y | x equals y |
x != y | x does not equal y |
x < y | x is less than y |
x > y | x is greater than y |
x <= y | x is less than or equal to y |
x >= y | x is greater than or equal to y |
Each takes two operands of the same type (the no-mixing rule of lesson 4.2 applies here too: i32 won't compare against u32) and produces a bool. They sit below all arithmetic in lesson 6.1's table, so x + 1 == y * 2 compares the finished sums, and they refuse to chain, so "is x between a and b" needs two comparisons joined by the && arriving in lesson 6.5.
Here they all are at once, on input the program reads with lesson 5.6's recipe:
use std::io;
fn read_number(prompt: &str) -> i32 {
println!("{prompt}");
let mut input = String::new();
io::stdin()
.read_line(&mut input)
.expect("failed to read input");
input.trim().parse().expect("that wasn't a whole number")
}
fn main() {
let a = read_number("First number?");
let b = read_number("Second number?");
println!("{a} == {b} is {}", a == b);
println!("{a} != {b} is {}", a != b);
println!("{a} < {b} is {}", a < b);
println!("{a} > {b} is {}", a > b);
println!("{a} <= {b} is {}", a <= b);
println!("{a} >= {b} is {}", a >= b);
}First number?
4
Second number?
5
4 == 5 is false
4 != 5 is true
4 < 5 is true
4 > 5 is false
4 <= 5 is true
4 >= 5 is false
For integers, characters, and booleans, that's the whole story: the operators mean what the table says, always, with no asterisks. The asterisks all belong to floats, and they get the rest of the lesson.
First, one matter of style. Since comparisons produce bools, comparing a bool again is redundant:
if is_ready == true { // works, but
if is_ready { // this is the same thing
if is_ready == false { // works, but
if !is_ready { // this is the same thingBest practice
Never compare against true or false. The variable already is the answer; == true just asks it to repeat itself. (Clippy, from lesson 0.11, flags this as bool_comparison.)
Floats: ordering mostly works, equality mostly doesn't
The four ordering operators (<, >, <=, >=) are dependable on floats whenever the two values are meaningfully far apart. 9.0 < 10.0 is true, every time. They only get dicey when the operands are nearly identical, where lesson 4.5's rounding errors are larger than the true difference; whether that risk matters depends on the application (a game checking whether an explosion reached you can be wrong by a hair, a financial ledger can't).
Equality has no such "mostly". The tiniest rounding error anywhere upstream makes == answer false, and rounding errors are not exotic; lesson 4.5 showed 0.1 + 0.2 == 0.3 failing. Here's the same disease in a form that looks even more innocent. Both of these subtractions should give 0.01:
fn main() {
let d1: f64 = 100.0 - 99.99;
let d2: f64 = 10.0 - 9.99;
println!("d1 = {d1:.20}");
println!("d2 = {d2:.20}");
println!("d1 == d2 is {}", d1 == d2);
}d1 = 0.01000000000000511591
d2 = 0.00999999999999978684
d1 == d2 is false
(The {:.20} is lesson 5.5's precision control, used here as a microscope.) Neither value is 0.01, they're wrong in different directions, and == does exactly its job on the bits it was given. The operator isn't broken. The expectation is.
Tip
One narrow case where float == is fine: comparing against a literal that was never computed. If a variable was initialized from the literal 9.8 and nothing has done arithmetic to it, gravity == 9.8 holds, because both sides are the same bit pattern. The moment a value has been through any calculation, all bets are off.
The tool: compare with a tolerance
If "exactly equal" is the wrong question for computed floats, the right question is: are they within some small distance of each other? That distance is conventionally called epsilon. First attempt, with everything you already know:
fn approx_equal_abs(a: f64, b: f64, abs_epsilon: f64) -> bool {
(a - b).abs() <= abs_epsilon
}
fn main() {
let d1: f64 = 100.0 - 99.99;
let d2: f64 = 10.0 - 9.99;
println!("{}", approx_equal_abs(d1, d2, 0.000001));
}true
Better already. But an absolute epsilon has a scaling problem: 0.000001 is a generous allowance for numbers near 0.01 and an absurdly strict one for numbers near a trillion, where floating-point spacing itself is coarser than that. Any fixed tolerance you pick is too loose at one magnitude and too tight at another.
The standard repair (it goes back to Donald Knuth's The Art of Computer Programming) is to scale the tolerance to the numbers being compared, making it a relative epsilon, a percentage rather than a distance:
fn approx_equal_rel(a: f64, b: f64, rel_epsilon: f64) -> bool {
(a - b).abs() <= f64::max(a.abs(), b.abs()) * rel_epsilon
}
Read the right side as "rel_epsilon percent of the larger operand's size". With rel_epsilon at 1e-8, two numbers near a trillion may differ by a few thousand and still count as equal, while two numbers near 0.01 must agree to ten decimal places. The tolerance travels with the magnitude.
One failure mode remains, and it's a sneaky one: comparisons against zero, or very near it. Watch the same function pass and then fail on what is logically the same question:
fn approx_equal_rel(a: f64, b: f64, rel_epsilon: f64) -> bool {
(a - b).abs() <= f64::max(a.abs(), b.abs()) * rel_epsilon
}
fn main() {
let a: f64 = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1;
println!("{}", approx_equal_rel(a, 1.0, 1e-8));
println!("{}", approx_equal_rel(a - 1.0, 0.0, 1e-8));
}true
false
a is within a whisker of 1.0, and the first call says so. The second call asks whether a - 1.0 is within a whisker of 0.0, the same fact rearranged, and gets false: when both operands are nearly zero, "a percentage of the larger one" is itself nearly zero, and the tolerance collapses to nothing.
The professional version therefore checks both ways: an absolute test to handle the near-zero neighborhood, then the relative test for everything else.
fn approx_equal_rel(a: f64, b: f64, rel_epsilon: f64) -> bool {
(a - b).abs() <= f64::max(a.abs(), b.abs()) * rel_epsilon
}
fn approx_equal(a: f64, b: f64, abs_epsilon: f64, rel_epsilon: f64) -> bool {
if (a - b).abs() <= abs_epsilon {
true
} else {
approx_equal_rel(a, b, rel_epsilon)
}
}
fn main() {
let a: f64 = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1;
println!("{}", approx_equal_rel(a, 1.0, 1e-8));
println!("{}", approx_equal_rel(a - 1.0, 0.0, 1e-8));
println!("{}", approx_equal(a, 1.0, 1e-12, 1e-8));
println!("{}", approx_equal(a - 1.0, 0.0, 1e-12, 1e-8));
}true
false
true
true
Reasonable defaults: 1e-12 absolute, 1e-8 relative. They're judgment calls, not constants of nature; a physics engine and an accounting system will tune them differently. This is lesson 4.5's promised tool. It costs two small functions, and in exchange == never gets another chance to lie to you about floats.
Warning
The standard library defines f64::EPSILON, and the internet is full of advice to use it as the tolerance. Don't. It's machine epsilon: the gap between 1.0 and the next representable number, about 2.22e-16. As a tolerance it's roughly "zero rounding error allowed", which fails for any value that's been through a few operations. It's a building block for numericists, not a ready-made answer.
A last float footnote, carried over from lesson 4.5: NaN answers false to every comparison, including NaN == NaN and NaN < anything. If a value might be NaN, test it with .is_nan(), never with ==.
Quiz time
Question #1
Tighten this fragment using this lesson's style rule (two changes):
if passed == true {
println!("passed");
}
if expired == false {
println!("still valid");
}Show solution
if passed {
println!("passed");
}
if !expired {
println!("still valid");
}
A bool needs no second opinion.
Question #2
What does this print, and why is one line surprising?
fn main() {
println!("{}", 0.1 + 0.2 == 0.3);
println!("{}", 0.25 + 0.25 == 0.5);
}Show solution
false
true
The first is lesson 4.5's classic: none of 0.1, 0.2, 0.3 is exactly representable in binary, and the errors don't cancel. The second is true because 0.25 and 0.5 are powers of two, which binary floats store exactly; the addition is error-free. Float == isn't randomly wrong, it's precisely wrong, but since "is my number secretly a sum of powers of two" is no way to live, use approx_equal for computed values anyway.
Question #3
A teammate "fixes" float comparison like this and asks for review. What do you tell them?
fn approx_equal_bad(a: f64, b: f64) -> bool {
(a - b).abs() <= f64::EPSILON
}Show solution
Two problems. f64::EPSILON is machine epsilon, the rounding granularity at 1.0, so the test allows essentially no accumulated error, and for values much larger than 1.0 it's stricter than the float format itself can honor (neighboring representable numbers near 1e12 are further apart than EPSILON, so even identical-looking computations can fail). And as a fixed absolute tolerance it doesn't scale: too strict for big values, arguably too loose near zero is the other failure direction. Point them at the combined absolute-plus-relative approx_equal from this lesson.
Next lesson: &&, ||, and the art of asking two questions at once, including the range check that lesson 4.6's quiz has been waiting on.