Skip to content

Commit a067cd2

Browse files
committed
Replace incorrect suggested fix for float_cmp
Using `f32::EPSILON` or `f64::EPSILON` as the floating-point equality comparison error margin is incorrect, yet `float_cmp` has until now recommended this be done. This change fixes the given guidance (both in docs and compiler hints) to not reference these unsuitable constants. Instead, the guidance now clarifies that the scenarios in which an absolute error margin is usable, provides a reference implementation of using a user-defined absolute error margin (as an absolute error margin can only be used-defined and may be different for different comparisons) and references the floating point guide for a reference implementation of relative error based equaltiy comparison for when absolute error margin cannot be used. changelog: Fix guidance of [`float_cmp`] and [`float_cmp_const`] to not incorrectly recommend `f64::EPSILON` as the error margin. Fixes #6816
1 parent 1de41b1 commit a067cd2

File tree

6 files changed

+91
-88
lines changed

6 files changed

+91
-88
lines changed

clippy_lints/src/operators/float_cmp.rs

-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ pub(crate) fn check<'tcx>(
5757
Applicability::HasPlaceholders, // snippet
5858
);
5959
}
60-
diag.note("`f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`");
6160
});
6261
}
6362
}

clippy_lints/src/operators/mod.rs

+79-35
Original file line numberDiff line numberDiff line change
@@ -574,69 +574,113 @@ declare_clippy_lint! {
574574
/// implement equality for a type involving floats).
575575
///
576576
/// ### Why is this bad?
577-
/// Floating point calculations are usually imprecise, so
578-
/// asking if two values are *exactly* equal is asking for trouble. For a good
579-
/// guide on what to do, see [the floating point
580-
/// guide](http://www.floating-point-gui.de/errors/comparison).
577+
/// Floating point calculations are usually imprecise, so asking if two values are *exactly*
578+
/// equal is asking for trouble because arriving at the same logical result via different
579+
/// routes (e.g. calculation versus constant) may yield different values.
581580
///
582581
/// ### Example
582+
///
583583
/// ```no_run
584-
/// let x = 1.2331f64;
585-
/// let y = 1.2332f64;
584+
/// let a: f64 = 1000.1;
585+
/// let b: f64 = 0.2;
586+
/// let x = a + b;
587+
/// let y = 1000.3; // Expected value.
588+
///
589+
/// // Actual value: 1000.3000000000001
590+
/// println!("{x}");
586591
///
587-
/// if y == 1.23f64 { }
588-
/// if y != x {} // where both are floats
592+
/// let are_equal = x == y;
593+
/// println!("{are_equal}"); // false
589594
/// ```
590595
///
591-
/// Use instead:
596+
/// The correct way to compare floating point numbers is to define an allowed error margin. This
597+
/// may be challenging if there is no "natural" error margin to permit. Broadly speaking, there
598+
/// are two cases:
599+
///
600+
/// 1. If your values are in a known range and you can define a threshold for "close enough to
601+
/// be equal", it may be appropriate to define an absolute error margin. For example, if your
602+
/// data is "length of vehicle in centimeters", you may consider 0.1 cm to be "close enough".
603+
/// 1. If your code is more general and you do not know the range of values, you should use a
604+
/// relative error margin, accepting e.g. 0.1% of error regardless of specific values.
605+
///
606+
/// For the scenario where you can define a meaningful absolute error margin, consider using:
607+
///
592608
/// ```no_run
593-
/// # let x = 1.2331f64;
594-
/// # let y = 1.2332f64;
595-
/// let error_margin = f64::EPSILON; // Use an epsilon for comparison
596-
/// // Or, if Rust <= 1.42, use `std::f64::EPSILON` constant instead.
597-
/// // let error_margin = std::f64::EPSILON;
598-
/// if (y - 1.23f64).abs() < error_margin { }
599-
/// if (y - x).abs() > error_margin { }
609+
/// const ALLOWED_ERROR_VEHICLE_LENGTH_CM: f64 = 0.1;
610+
/// let within_tolerance = (x - y).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM;
611+
/// println!("{within_tolerance}"); // true
600612
/// ```
613+
///
614+
/// NB! Do not use `f64::EPSILON` - while the error margin is often called "epsilon", this is
615+
/// a different use of the term that is not suitable for floating point equality comparison.
616+
/// Indeed, for the example above using `f64::EPSILON` as the allowed error would return `false`.
617+
///
618+
/// For the scenario where no meaningful absolute error can be defined, refer to
619+
/// [the floating point guide](https://www.floating-point-gui.de/errors/comparison)
620+
/// for a reference implementation of relative error based comparison of floating point values.
621+
/// `MIN_NORMAL` in the reference implementation is equivalent to `MIN_POSITIVE` in Rust.
601622
#[clippy::version = "pre 1.29.0"]
602623
pub FLOAT_CMP,
603624
pedantic,
604-
"using `==` or `!=` on float values instead of comparing difference with an epsilon"
625+
"using `==` or `!=` on float values instead of comparing difference with an allowed error"
605626
}
606627

607628
declare_clippy_lint! {
608629
/// ### What it does
609-
/// Checks for (in-)equality comparisons on floating-point
610-
/// value and constant, except in functions called `*eq*` (which probably
630+
/// Checks for (in-)equality comparisons on constant floating-point
631+
/// values (apart from zero), except in functions called `*eq*` (which probably
611632
/// implement equality for a type involving floats).
612633
///
613-
/// ### Why restrict this?
614-
/// Floating point calculations are usually imprecise, so
615-
/// asking if two values are *exactly* equal is asking for trouble. For a good
616-
/// guide on what to do, see [the floating point
617-
/// guide](http://www.floating-point-gui.de/errors/comparison).
634+
/// ### Why is this bad?
635+
/// Floating point calculations are usually imprecise, so asking if two values are *exactly*
636+
/// equal is asking for trouble because arriving at the same logical result via different
637+
/// routes (e.g. calculation versus constant) may yield different values.
618638
///
619639
/// ### Example
640+
///
620641
/// ```no_run
621-
/// let x: f64 = 1.0;
622-
/// const ONE: f64 = 1.00;
642+
/// let a: f64 = 1000.1;
643+
/// let b: f64 = 0.2;
644+
/// let x = a + b;
645+
/// const Y: f64 = 1000.3; // Expected value.
623646
///
624-
/// if x == ONE { } // where both are floats
647+
/// // Actual value: 1000.3000000000001
648+
/// println!("{x}");
649+
///
650+
/// let are_equal = x == y;
651+
/// println!("{are_equal}"); // false
625652
/// ```
626653
///
627-
/// Use instead:
654+
/// The correct way to compare floating point numbers is to define an allowed error margin. This
655+
/// may be challenging if there is no "natural" error margin to permit. Broadly speaking, there
656+
/// are two cases:
657+
///
658+
/// 1. If your values are in a known range and you can define a threshold for "close enough to
659+
/// be equal", it may be appropriate to define an absolute error margin. For example, if your
660+
/// data is "length of vehicle in centimeters", you may consider 0.1 cm to be "close enough".
661+
/// 1. If your code is more general and you do not know the range of values, you should use a
662+
/// relative error margin, accepting e.g. 0.1% of error regardless of specific values.
663+
///
664+
/// For the scenario where you can define a meaningful absolute error margin, consider using:
665+
///
628666
/// ```no_run
629-
/// # let x: f64 = 1.0;
630-
/// # const ONE: f64 = 1.00;
631-
/// let error_margin = f64::EPSILON; // Use an epsilon for comparison
632-
/// // Or, if Rust <= 1.42, use `std::f64::EPSILON` constant instead.
633-
/// // let error_margin = std::f64::EPSILON;
634-
/// if (x - ONE).abs() < error_margin { }
667+
/// const ALLOWED_ERROR_VEHICLE_LENGTH_CM: f64 = 0.1;
668+
/// let within_tolerance = (x - Y).abs() < ALLOWED_ERROR_VEHICLE_LENGTH_CM;
669+
/// println!("{within_tolerance}"); // true
635670
/// ```
671+
///
672+
/// NB! Do not use `f64::EPSILON` - while the error margin is often called "epsilon", this is
673+
/// a different use of the term that is not suitable for floating point equality comparison.
674+
/// Indeed, for the example above using `f64::EPSILON` as the allowed error would return `false`.
675+
///
676+
/// For the scenario where no meaningful absolute error can be defined, refer to
677+
/// [the floating point guide](https://www.floating-point-gui.de/errors/comparison)
678+
/// for a reference implementation of relative error based comparison of floating point values.
679+
/// `MIN_NORMAL` in the reference implementation is equivalent to `MIN_POSITIVE` in Rust.
636680
#[clippy::version = "pre 1.29.0"]
637681
pub FLOAT_CMP_CONST,
638682
restriction,
639-
"using `==` or `!=` on float constants instead of comparing difference with an epsilon"
683+
"using `==` or `!=` on float constants instead of comparing difference with an allowed error"
640684
}
641685

642686
declare_clippy_lint! {

tests/ui/float_cmp.rs

-6
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,16 @@ fn main() {
7171
twice(ONE) != ONE;
7272
ONE as f64 != 2.0;
7373
//~^ ERROR: strict comparison of `f32` or `f64`
74-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
7574
ONE as f64 != 0.0; // no error, comparison with zero is ok
7675

7776
let x: f64 = 1.0;
7877

7978
x == 1.0;
8079
//~^ ERROR: strict comparison of `f32` or `f64`
81-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
8280
x != 0f64; // no error, comparison with zero is ok
8381

8482
twice(x) != twice(ONE as f64);
8583
//~^ ERROR: strict comparison of `f32` or `f64`
86-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
8784

8885
x < 0.0; // no errors, lower or greater comparisons need no fuzzyness
8986
x > 0.0;
@@ -105,17 +102,14 @@ fn main() {
105102
ZERO_ARRAY[i] == NON_ZERO_ARRAY[j]; // ok, because lhs is zero regardless of i
106103
NON_ZERO_ARRAY[i] == NON_ZERO_ARRAY[j];
107104
//~^ ERROR: strict comparison of `f32` or `f64`
108-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
109105

110106
let a1: [f32; 1] = [0.0];
111107
let a2: [f32; 1] = [1.1];
112108

113109
a1 == a2;
114110
//~^ ERROR: strict comparison of `f32` or `f64` arrays
115-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
116111
a1[0] == a2[0];
117112
//~^ ERROR: strict comparison of `f32` or `f64`
118-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
119113

120114
// no errors - comparing signums is ok
121115
let x32 = 3.21f32;

tests/ui/float_cmp.stderr

+5-16
Original file line numberDiff line numberDiff line change
@@ -4,49 +4,38 @@ error: strict comparison of `f32` or `f64`
44
LL | ONE as f64 != 2.0;
55
| ^^^^^^^^^^^^^^^^^ help: consider comparing them within some margin of error: `(ONE as f64 - 2.0).abs() > error_margin`
66
|
7-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
87
= note: `-D clippy::float-cmp` implied by `-D warnings`
98
= help: to override `-D warnings` add `#[allow(clippy::float_cmp)]`
109

1110
error: strict comparison of `f32` or `f64`
12-
--> tests/ui/float_cmp.rs:79:5
11+
--> tests/ui/float_cmp.rs:78:5
1312
|
1413
LL | x == 1.0;
1514
| ^^^^^^^^ help: consider comparing them within some margin of error: `(x - 1.0).abs() < error_margin`
16-
|
17-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
1815

1916
error: strict comparison of `f32` or `f64`
20-
--> tests/ui/float_cmp.rs:84:5
17+
--> tests/ui/float_cmp.rs:82:5
2118
|
2219
LL | twice(x) != twice(ONE as f64);
2320
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider comparing them within some margin of error: `(twice(x) - twice(ONE as f64)).abs() > error_margin`
24-
|
25-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
2621

2722
error: strict comparison of `f32` or `f64`
28-
--> tests/ui/float_cmp.rs:106:5
23+
--> tests/ui/float_cmp.rs:103:5
2924
|
3025
LL | NON_ZERO_ARRAY[i] == NON_ZERO_ARRAY[j];
3126
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: consider comparing them within some margin of error: `(NON_ZERO_ARRAY[i] - NON_ZERO_ARRAY[j]).abs() < error_margin`
32-
|
33-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
3427

3528
error: strict comparison of `f32` or `f64` arrays
36-
--> tests/ui/float_cmp.rs:113:5
29+
--> tests/ui/float_cmp.rs:109:5
3730
|
3831
LL | a1 == a2;
3932
| ^^^^^^^^
40-
|
41-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
4233

4334
error: strict comparison of `f32` or `f64`
44-
--> tests/ui/float_cmp.rs:116:5
35+
--> tests/ui/float_cmp.rs:111:5
4536
|
4637
LL | a1[0] == a2[0];
4738
| ^^^^^^^^^^^^^^ help: consider comparing them within some margin of error: `(a1[0] - a2[0]).abs() < error_margin`
48-
|
49-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
5039

5140
error: aborting due to 6 previous errors
5241

tests/ui/float_cmp_const.rs

-8
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,21 @@ fn main() {
1515
// has errors
1616
1f32 == ONE;
1717
//~^ ERROR: strict comparison of `f32` or `f64` constant
18-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
1918
TWO == ONE;
2019
//~^ ERROR: strict comparison of `f32` or `f64` constant
21-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
2220
TWO != ONE;
2321
//~^ ERROR: strict comparison of `f32` or `f64` constant
24-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
2522
ONE + ONE == TWO;
2623
//~^ ERROR: strict comparison of `f32` or `f64` constant
27-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
2824
let x = 1;
2925
x as f32 == ONE;
3026
//~^ ERROR: strict comparison of `f32` or `f64` constant
31-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
3227

3328
let v = 0.9;
3429
v == ONE;
3530
//~^ ERROR: strict comparison of `f32` or `f64` constant
36-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
3731
v != ONE;
3832
//~^ ERROR: strict comparison of `f32` or `f64` constant
39-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
4033

4134
// no errors, lower than or greater than comparisons
4235
v < ONE;
@@ -70,5 +63,4 @@ fn main() {
7063
// has errors
7164
NON_ZERO_ARRAY == NON_ZERO_ARRAY2;
7265
//~^ ERROR: strict comparison of `f32` or `f64` constant arrays
73-
//~| NOTE: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
7466
}

tests/ui/float_cmp_const.stderr

+7-22
Original file line numberDiff line numberDiff line change
@@ -4,65 +4,50 @@ error: strict comparison of `f32` or `f64` constant
44
LL | 1f32 == ONE;
55
| ^^^^^^^^^^^ help: consider comparing them within some margin of error: `(1f32 - ONE).abs() < error_margin`
66
|
7-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
87
= note: `-D clippy::float-cmp-const` implied by `-D warnings`
98
= help: to override `-D warnings` add `#[allow(clippy::float_cmp_const)]`
109

1110
error: strict comparison of `f32` or `f64` constant
12-
--> tests/ui/float_cmp_const.rs:19:5
11+
--> tests/ui/float_cmp_const.rs:18:5
1312
|
1413
LL | TWO == ONE;
1514
| ^^^^^^^^^^ help: consider comparing them within some margin of error: `(TWO - ONE).abs() < error_margin`
16-
|
17-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
1815

1916
error: strict comparison of `f32` or `f64` constant
20-
--> tests/ui/float_cmp_const.rs:22:5
17+
--> tests/ui/float_cmp_const.rs:20:5
2118
|
2219
LL | TWO != ONE;
2320
| ^^^^^^^^^^ help: consider comparing them within some margin of error: `(TWO - ONE).abs() > error_margin`
24-
|
25-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
2621

2722
error: strict comparison of `f32` or `f64` constant
28-
--> tests/ui/float_cmp_const.rs:25:5
23+
--> tests/ui/float_cmp_const.rs:22:5
2924
|
3025
LL | ONE + ONE == TWO;
3126
| ^^^^^^^^^^^^^^^^ help: consider comparing them within some margin of error: `(ONE + ONE - TWO).abs() < error_margin`
32-
|
33-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
3427

3528
error: strict comparison of `f32` or `f64` constant
36-
--> tests/ui/float_cmp_const.rs:29:5
29+
--> tests/ui/float_cmp_const.rs:25:5
3730
|
3831
LL | x as f32 == ONE;
3932
| ^^^^^^^^^^^^^^^ help: consider comparing them within some margin of error: `(x as f32 - ONE).abs() < error_margin`
40-
|
41-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
4233

4334
error: strict comparison of `f32` or `f64` constant
44-
--> tests/ui/float_cmp_const.rs:34:5
35+
--> tests/ui/float_cmp_const.rs:29:5
4536
|
4637
LL | v == ONE;
4738
| ^^^^^^^^ help: consider comparing them within some margin of error: `(v - ONE).abs() < error_margin`
48-
|
49-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
5039

5140
error: strict comparison of `f32` or `f64` constant
52-
--> tests/ui/float_cmp_const.rs:37:5
41+
--> tests/ui/float_cmp_const.rs:31:5
5342
|
5443
LL | v != ONE;
5544
| ^^^^^^^^ help: consider comparing them within some margin of error: `(v - ONE).abs() > error_margin`
56-
|
57-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
5845

5946
error: strict comparison of `f32` or `f64` constant arrays
60-
--> tests/ui/float_cmp_const.rs:71:5
47+
--> tests/ui/float_cmp_const.rs:64:5
6148
|
6249
LL | NON_ZERO_ARRAY == NON_ZERO_ARRAY2;
6350
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
64-
|
65-
= note: `f32::EPSILON` and `f64::EPSILON` are available for the `error_margin`
6651

6752
error: aborting due to 8 previous errors
6853

0 commit comments

Comments
 (0)