# 6.4 Comparison operators and float equality
> The six comparison operators in full, why == betrays floating-point values, and the epsilon technique professionals use instead.
Source: https://learnrust.net/chapter-6/comparison-operators-and-float-equality/
Lesson [4.6](@/chapter-4/boolean-values.md) introduced the comparison operators "gently" and promised them a full lesson in chapter 6. Lesson [4.5](@/chapter-4/floating-point-types.md) 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](@/chapter-4/integer-types.md) applies here too: `i32` won't compare against `u32`) and produces a `bool`. They sit below all arithmetic in lesson [6.1](@/chapter-6/operator-precedence-and-associativity.md)'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](@/chapter-6/logical-operators.md).
Here they all are at once, on input the program reads with lesson [5.6](@/chapter-5/parsing-strings-into-numbers.md)'s recipe:
```rust
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 `bool`s, comparing a `bool` *again* is redundant:
```rust
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 thing
```
{% callout(kind="best", title="Best 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`.)
{% end %}
## 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:
```rust
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](@/chapter-5/formatting-strings.md)'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.
{% callout(kind="tip", title="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.
{% end %}
## 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:
```rust
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:
```rust
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:
```rust
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.
```rust
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.
{% callout(kind="warning", title="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.
{% end %}
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):
```rust
if passed == true {
println!("passed");
}
if expired == false {
println!("still valid");
}
```
Show solution
```rust
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?
```rust
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?
```rust
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.