Float Equality
ID |
javascript.float_equality |
Severity |
low |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Reliability |
Language |
JavaScript |
Tags |
reliability, suspicious-code |
Description
Reports an equality test — ==, !=, === or !== — with a floating-point literal as one of its operands.
The rule fires only when a fractional literal is written out. JavaScript has one numeric type, so nothing in a === b says the values are fractional; an operand-typed trigger would need type inference the engine does not have for most JavaScript, and guessing would report every equality in the language. A literal spelled with a decimal point, or with a negative exponent, is the one form that says in the source itself that a fractional value is being compared.
The operand has to be the literal, optionally signed: x === -0.5 is reported, x === scale(0.5) is not — there the literal is an argument and the compared value is whatever the call returns. 1e3 is an integer written compactly and is not reported; 1e-3 is.
Rationale
// Bad
if (0.1 + 0.2 === 0.3) { // never true
applyDiscount();
}
if (total === 1.0) { // false when total is 0.3 + 0.7
finish();
}
-
Most decimal fractions have no exact binary representation, so a value that "should" be
0.3after arithmetic usually is not.0.1 + 0.2is0.30000000000000004, and0.3 + 0.7is0.9999999999999999. -
The comparison reads as an equality but is really a test for one particular accumulation of rounding error. It can pass on one input scale and fail on another with no change to the code.
-
Integer-valued float literals like
1.0are reported too. They are exactly representable themselves, but the accumulated value on the other side of the operator is what carries the error — and writing the decimal point is the author saying the quantity is fractional.
Remediation
Compare the absolute difference against a tolerance chosen for the quantity being measured.
// Good
const TOLERANCE = 1e-9;
if (Math.abs(0.1 + 0.2 - 0.3) < TOLERANCE) {
applyDiscount();
}
Number.EPSILON is the right tolerance only for values near 1: it is the gap between 1 and the next representable double, so it is far too small for large magnitudes and needlessly tight for small ones. For a relative comparison, scale it: Math.abs(a - b) ⇐ Number.EPSILON * Math.max(Math.abs(a), Math.abs(b)).
When the quantity is really discrete — money, counts, percentages in basis points — the better fix is to stop using floats for it. Hold the value as an integer of the smallest unit (cents rather than euros) and the equality becomes exact.