Let’s face it: JavaScript math has some serious issues.
Say your app is responsible for tracking transactions between users and vendors. One of those vendors is a floral distribution company, sending large units to stores all over America containing thousands of flowers. Let’s calculate how much the buyer should pay. Copy and paste the following code into your browser console or run it here:
const flower = { price: 0.94 };
const calculateFlowersTotal = n => n * flower.price;
let bundles = [
calculateFlowersTotal(5),
calculateFlowersTotal(20),
calculateFlowersTotal(100),
calculateFlowersTotal(500)
];
let order = [7, 3, 0, 5];
let orderTotal = order.reduce((acc, quantity, i) =>acc += quantity * bundles[i], 0);
console.log(‘Order subtotal: ‘, orderTotal + ‘\n’);
let orderTotalWithTax = orderTotal * 1.0725; //should return 2616.14925
console.log(‘Should return true: ‘, orderTotalWithTax === 2616.14925);
console.log(‘\nWhat gives? Let's have a look..\n’);
console.log(‘calculated total with tax: ‘, orderTotalWithTax);
As you can see, the total is not what we expect. The subtotal seems to have been accurately calculated, but there was an unexpected result in adding the tax. Can you spot the trend in the operations above which allowed for safe calculations, and how that broke down? Although integers and decimals are both classified as Numbers, and are technically both floats, they have slightly different behavior which may result in inaccurate calculations. Particularly when involving inaccurately represented decimal values when two float numbers are assessed with any multiplication, division, or exponential operation.
“In JavaScript all numbers are IEEE 754 floating point numbers. Due to the binary nature of their encoding, some decimal numbers cannot be represented with perfect accuracy.” Josh Clanton
The bit value of 0.1 is one of these cases, but the interpreter was safely assuming that the user does not want to see the inaccuracies by removing the trailing section. Let’s looks at some examples:
0.1*0.1; //evaluates to 0.010000000000000002
(0.1*0.1)/0.1 //evaluates to 0.10000000000000002
It seems as if in multiplying and dividing we are eliminating the removal assumption previously held that the trailing values should not be displayed. The full value is used in calculation, including the extra value necessary to represent them, leading to the interpreter believing that the full value is more likely the real value, and that removing the trailing value would decrease the ability of calculations to make calculations with any degree of accuracy.
Here are some suggestions on how to best approach accurately calculating transactions. They involve converting decimals to integers, performing calculations, then converting back. However, these approaches can still break down when when an unexpected float is introduced, as seen in the flower sales tax example.
Currently, the only foolproof solution for these inaccuracies would be to use libraries that provide precision functions. A discussion three major implementations, big.js, bignumber.js and decimal.js, can be found here.
We can only hope that one day the ECMA team will take the solutions provided from these authors and implement them into the language’s native operators.
Jacob