I came across a bug recently for a simple table of inputs. A user is able to add a series of numbers, up to 2 decimal places, and then when a save button is clicked the total is validated to make sure that it is between 0 and 100. The issue that the user had is that from their perspective they had entered a series of decimals that added up to exactly 100 but the form was stopping them from saving, giving the error message that the total was over 100.
If you have been working with JS for a while then it is quite likely that you have come across this issue, or a similar one. The problem in this instance is that from a decimals perspective I am adding up numbers that equal exactly 100 but when compared to 100 JS is telling me that the number is greater than 100.
const total = 11.1 + 33.17 + 11.3 + 11.32 + 33.11 // 100.00000000000001 total <= 100 // false
The problem is that actually the number that JS has for its total is not exactly 100 as we would expect. I have reproduced this issue in a codesandbox so that the numbers can be played around with a bit more.
The fundamental issue is that the JS Number type uses a floating point format - IEEE 754 - binary64 - that is unable to precisely represent some decimal numbers. I’m not going to go into detail here - I started but it got too long for one post so you can read more about some of the maths involved in - JS - Why does 0.1 + 0.2 ≠ 0.3.
For this post all that you need to know is that when working with integers the maths will be fine - as long as you don’t go out of range:
Number.MAX_SAFE_INTEGER //9007199254740991 Number.MIN_SAFE_INTEGER //-9007199254740991
When working with decimals though - as soon as you start doing any sort of arithmetic there is always the potential for unexpected results.
What to do?
The most important thing is to simply be aware of this potential issue. The majority of the time you’ll probably find it will not be a problem for you.
By far the most common areas that I have seen this come up is when working with currencies or percentages (the particular example at the start of this post was a table of percentages), but there will be many other examples when performing arithmetic on decimals needs to be precise.
Work only with integers
My recommendation is to always make it so that we are working with integers only as opposed to decimals. As mentioned earlier, integer arithmetic will be precise as long as you don’t exceed the min or max values.
This approach does rely on us knowing the number of decimal places required in advance though.
So going back to the numbers used earlier:
11.1 + 33.17 + 11.3 + 11.32 + 33.11 // 100.00000000000001 (1110 + 3317 + 1130 + 1132 + 3311) / 100 // 100
As you can see, in this instance we know there can only be 2 decimal places so we multiply each number by 100 to give make sure we always have integers, and then divide by 100 at the end to give us the result we are after. How you want to go about performing these conversions will depend on the codebase you are working with - you could do it case by case or create some reusable helper functions.
You can see an integer only sandbox that takes this approach. The only difference between the original solution, where we get the floating point issue, and the the integer only solution is the total line:
total = inputs.reduce((total, current) => current + total, 0); total = inputs.reduce((total, current) => current * 100 + total, 0) / 100;
In many cases it will be this simple to fix the problem.
If you’re working with currencies then you could use a library like dinero.js or currency.js. These are both designed to solve floating point issues with currencies, although they go about it in very different ways.
I have created a currency.js implementation of the original codesandbox. In this example it takes in exactly the same numbers we started with but outputs our desired value of 100.00.
One thing to be concious of when working with currencies, is that you may need to vary the number of decimal places and this might impact sums that you need to carry out. If you look at the Active ISO 4217 currencies you can see that although 2 decimal places is by far the most common, there are some currencies with 0, 3 and 4 decimal places.
One approach that I have seen in codebases and also recommended in forums is to use toFixed. In a lot of cases this might be fine but it can give people a false sense of security and lead to incorrect results.
If you are keeping the number of decimal places consistent (inputs have 2 decimal places and the total has 2 decimal places) then you shouldn’t have any issues:
const total = 11.10 + 33.17 + 11.30 + 11.32 + 33.11 total.toFixed(2); // "100.00"
However, it is very easy to end up with unexpected results using toFixed. For example, we would expect 0.14 + 0.21 + 0.30 to equal 0.65 and, if we wanted one decimal place, to round to 0.7. If you do this in JS though, you end up with 0.6:
const total = 0.14 + 0.21 + 0.30 // 0.6499999999999999 total.toFixed(1); // "0.6"
This goes back to the inability of the JS floating point format to precisely store some numbers that are easy to precisely represent in decimal format. So effectively we are actually rounding 0.6499999999999999 to 1 decimal place and so get 0.6.
You see toFixed given as an answer to simple rounding questions in JS but you get this problem and the solution, again, is to make sure you are only working with integers and round by shifting your result by the number of decimal places you want to round to. You can see this below where we multiply the number by 100 as we want to round to 2 decimal places. This gives us the answer we might have expected from toFixed earlier:
const input = 0.365; input.toFixed(2); // "0.36" Math.round(input * 100) / 100; // 0.37
The other potential problem to mention with toFixed is that you end up working with Strings instead of Numbers, this can lead to concatenation as opposed to addition:
const input = 0.365; const fixed = input.toFixed(2); // "0.36" fixed + 0.21; // "0.360.21"
It’s easy to fix, using parseFloat, but it is another thing to miss and makes your code harder to read.
Finally, every time you convert between types there is a performance cost - probably unnoticeable in most cases but it is still there. It’s usually much faster to keep working with just Numbers if possible.
For these reasons I avoid toFixed and have not come across a situation that can’t be solved working purely with integers. I find this code easier to read and less error prone.