DEV Community

Daniel Scott
Daniel Scott

Posted on

How to compare numbers correctly in JavaScript

The advice in this post relates to JavaScript, since all numbers in JavaScript are (currently) IEEE-754 double-precision floating-point numbers. However, everything in here is equally applicable to any language that has a floating-point type.

In short: Don't use the language-provided equality test, and don't use language-provided "epsilon" constants as your "tolerance" for errors. Instead, choose your own tolerance.

Now, the long version (which I originally penned in response to some flawed advice I found online about how to compare numbers in JavaScript).

The problem, and a flawed approach to solving it

Take this ("bad") code, which addresses the classic floating point problem of (0.1 + 0.2) == 0.3 returning false:

let f1 = 0.1 + 0.2;
let f2 = 0.3;
console.log(Math.abs(f1 - f2) < Number.EPSILON); // 'True - Yippeee!!!'

Ok, so far so good. But it fails with other inputs:

let f1 = 1000000.1 + 0.2;
let f2 = 1000000.3;
console.log(Math.abs(f1 - f2) < Number.EPSILON); // '!!!!!! false !!!!!!!'

The basic pattern being used is sound: avoid a direct equality comparison, and check that your two numbers are within some tolerable difference. However, the tolerance used is badly chosen.

Why does Number.EPSILON fail the second example above?

It's actually very dangerous to use Number.Epsilon as a "tolerance" for number comparisons.

Other languages have a similar construct (the .Net languages all have it as double.Epsilon for example). If you check any sound documentation for such constants, they tend to come with a warning not to use the "floating point epsilon" for comparisons.

The "epsilon" provided by the language is simply the smallest possible "increment" you can represent with that particular floating point type. For IEEE double-precision numbers, that number (Number.EPSILON) is minuscule!

The problem with using it for comparisons is that floating point numbers are implemented like scientific notation, where you have a some small(ish) number of significant digits, and an exponent which moves the decimal point left or right (possibly a loooooooooooong way left or right).

Double-precision floating point numbers (as used in JavaScript) have about 15 significant (decimal) digits. What that means is if you want to hold a number like 1,000,000,000 (10 significant digits), then you can only hold a fraction up to about five or six decimal places. The double-precision floating point numbers 3,000,000,000.00001 and 3,000,000,000.000011 will be seen as equal. (note that because floats are stored as binary, it's not a case of there being exactly 15 significant decimal digits at all times - information is lost at some power of two, not a power of 10).

Number.EPSILON is waaaaay smaller than .00001 - so while the first example works with a "tolerance" of Number.EPSILON (because the numbers being compared are all smaller than 1.0), the second example breaks.

There is no one-size-fits all "epsilon" for comparisons

If you go hunting online, there's a fair bit of discussion on how to choose a suitable epsilon (or tolerance) for performing comparisons. After all the discussion, and some very clever code that has a good shot at figuring out a "dynamically calculated universal epsilon" (based on the largest number being compared) it always ends up boiling back down to this:

YOU need to choose the tolerance that makes sense for your application!

The reason dynamically calculated tolerances (based on the scale of the numbers being compared) aren't a universal solution is that when a collection of numbers being compared vary wildly in size it's easy to end up with a situation that breaks one of the most important rules of equality: "equality must be transitive". i.e.

if a == b, and b == c, then a == c must evaluate as TRUE as well!

Using a tolerance that changes with every single equality test in your program is a very good route to having a != c somewhere when you would reasonably expect a and c to be equal. You can also guarantee this will happen at annoyingly "random" times. Thar be the way to Bug Island me-hearties: enter if ye dare and may the almighty have mercy on yer soul ... arrrrrrrr**!!!

** actually ... "arrrghhhhhhhh!!!" is more appropriate

Choosing a tolerance for your application

So, how do you select a suitable tolerance for your program? I'm glad you asked! ...

Let's assume you're holding dimensions of a building in millimetres (where a 20 metre long building would be 20,000). Do you really care if that dimension is within .0000000001 of a millimetre of some other dimension when you're comparing? - probably not!

In this case a sensible epsilon (or tolerance) might be .01 or .001**. Plug that into the Math.abs(f1 - f2) < tolerance expression instead.

Definitely do NOT use Number.EPSILON for this application, since you might get a 200m long building somewhere (200,000mm) and that may fail to compare properly to another 200m long dimension using JavaScript's Number.EPSILON.

** things will tend to work out even cleaner if you use tolerances that can be represented precisely in binary. Some nice simple options are powers of two. e.g. 0.5 ( 2^-1 ), 0.25 ( 2^-2 ), 0.125 ( 2^-3 ), 0.0625 ( 2^-4 ) etc.

Avoid floating point numbers wherever you can

even in JavaScript where they're unavoidable

Incidentally, if you didn't care whether your measurements in the previous example were any closer than 1mm to each other, then you should probably just use an integer type and be done with it.

If you're working in JavaScript then you're [currently**] stuck with floating point numbers. The only real alternative JavaScript offers is to store your numbers as strings. This can actually be a sensible approach for large integers that only need to be tested for equality and don't need to have numeric operations performed on them (such as database primary keys). There are some more "floating-point gotchas" waiting when you get to integers big enough to contain more than about 15-16 digits! (specifically, anything larger than 9,007,199,254,740,991)

Likewise (still on the "building model" example above), if you only cared whether your measurements were within 0.1mm of each other, then you could use a "decimal" type (if your language supports it), or just store all your measurements internally as integers representing tenths of millimetres (e.g. 20 metre building = 200,000 "tenth-millimetres" internally)

Floating point numbers are great for what they were designed for (complex modelling of real-world measurements or coordinates), but they introduce weirdness into calculations involving money, or other things we expect to "be nice and even".

** As of mid-2019, there has been talk of introducing a "BigInt" type to JavaScript (offering an alternative to floating-point numbers), but it's not supported in many implementations yet and it hasn't worked its way through to a final ECMAScript specification yet either. Google's V8 implementation of JavaScript seems to be an early adopter along with Mozilla, so you should be able to use it in current versions of Chrome, Firefox, and other V8-derived platforms now.

Why are floating point numbers so weird?

If you're not already familiar with the old 0.1+0.2 != 0.3 mind-bender, then I've thrown together a quick primer on the way floating point numbers work, which will shed some light on the madness.

Why Floating Point Numbers are so Weird >>

An interactive plaything: Go ahead and break stuff

If you want to have a play around with floating point comparisons in Javascript and peek into how the numbers lose precision as they get bigger, then there's a jsfiddle I stuck together at: https://jsfiddle.net/r0begv7a/3/

Top comments (2)

Collapse
 
manlycoffee profile image
Sal Rahman

Thanks for the article. It opened me up to the opportunity to look around, and I've found an article that proposes a potential solution to the problem that you posed in yours. That is, the epsilon comparison can have an additional step of multiplying it with the largest of the two operands. This way, the tolerance can be adjusted according to the operands that are supplied to the equality check.

Take a look. randomascii.wordpress.com/2012/02/...

Collapse
 
tim999 profile image
Tim

"The reason dynamically calculated tolerances (based on the scale of the numbers being compared) aren't a universal solution is that when a collection of numbers being compared vary wildly in size it's easy to end up with a situation that breaks one of the most important rules of equality: "equality must be transitive""

Floating point values already break nearly every other rule, e.g., equality isn't even reflexive. Why should we care if our rule also happens to break transitivity? Any possibility of a universally consistent definition of equality has already gone out the window. Floating point INequalities aren't transitive, either.

Please, show me any numeric algorithm where it's important that equality is transitive, but not important that it's reflexive, or that inequalities are transitive.

Or if your answer is to avoid floating point entirely ("Don't use the language-provided equality test") and write all custom numeric routines, that may be a safe approach but I'd like to see a program which actually does this, and what's involved. It sounds like I'll be rewriting half of Mathematica.