My first acquaintance with weird number calculation behavior had happened back in 2012 when I realized that getting a sum of two fractions did not return an expected result. It was similar to
const sum = 0.1 + 0.2; // 0.30000000000000004
and returned an inaccurate result. This is how I met IEEE 754 specification for the first time, and I got that not every decimal can be precisely represented (there is a good StackOverflow answer).
This is a pretty common error when developers start counting money and operate with currencies that have a decimal part (frankly, most of them). But there is a solution.
Strive to work with integers.
May sound naive, but it may save hours of debugging in a big application. Just compare two solutions: the first one calculates decimals as is…
const purchase1 = 3.57; // dollars
const purchase2 = 99.99; // dollars
const result = purchase1 + purchase2; // 103.55999999999999
…and second one has exact precision for UI.
const purchase1 = 357; // cents
const purchase2 = 9999; // cents
const precision = 2;
const result = (purchase1 + purchase2) / 10 ** precision; // 103.56
Thankfully, Javascript’s number
is big enough to store such values as purchases safely (Number.MAX_SAFE_INTEGER
equals to 9007199254740991
).
As a second option, think of your rounding tactic to get precise and predictable results.
There are several more unusual values defined by the standard:
0
/-0
Infinity
/-Infinity
NaN
(Not a Number). Yes, this value is also a Number.In reality, it’s hard to get a negative zero. In most calculations such as 3 - 3
it’ll be a still regular 0
. However, we can still get a negative one by dealing with Infinity
:
const negativeZero = 0 / -Infinity; // -0
But even in this case, you likely won’t be affected by negative zero, as comparison +0 === -0
still returns true. However, ES2015 introduced Object.is()
, which helps us to determine values like -0
:
const isNegativeZero = Object.is(0, -0); // false
Talking about NaN
. Despite the fact that it is considered as a Number, in JS there are plenty of ways to determine this exception, like:
console.log(NaN !== NaN); // true
console.log(isNaN(NaN)); // true
console.log(Number.isNaN(NaN)); // true
console.log(Object.is(NaN, NaN)); // true
All of them are good except (#2). I don’t recommend using isNaN(NaN)
because results of it may be unexpected (coercion inside makes it not precise and unreliable).
console.log(isNaN("🥕")); // also true and makes sense, it's a carrot!
In reality, it is rather simple to get an Infinity
and ruin your calculations. The most simple case is division by zero. Don’t think your try/catch
statement will help you, because it is not a case for IEEE 754. You’ll get a surprise instead:
const fancyCalculation = 1 / 0; // Infinity!
const anotherCalculation = 0 / 0; // Guess what? NaN!
Thankfully, together with ECMAScript 2015 we’ve got a good tool to exclude corner cases after a calculation, and it’s called Number.isFinite()
(like isFinite
but no coercion so more precise results).
console.log(Number.isFinite(NaN)); // false
console.log(Number.isFinite(Infinity)); // false
You should be safe enough now!
IEEE 754 specification may be misleading, and its predefined behavior casts a shadow on Javascript. But who said development is simple? Just handle corner cases, and write tests. Having one type for numbers (in opposite to such language as Java) is still good enough for a language, which was developed in ten days.