Try the following calculation to see what we mean.
Open up Google Chrome (or any other modern web browser), press, depending on your operating system, CTRL or CMD + ALT + i to access the developer tools and in the Console tab type out the sum:
0.1 + 0.2
Then hit the Enter key for the result.
Seeing the following value?
Welcome to the world of floating point numbers...
If, for example, we type into the browser console:
0.7 + 0.1
We don't expect to find a result of:
So why do we see this happening?
Basically, when you convert 0.1 in binary you end up with a repeating pattern after the decimal place (in the same way that you find in base 10 when dividing 3 into 10 or 33 into 100 for example). The returned result is not exact and, as a consequence, floating point methods will not be 100% accurate when used with such values.
Are there any solutions?
Not really as the underlying math implementation, as described above, is imperfect.
The closest we can get to accuracy is to post-process the result.
parseFloat(Number(0.30000000000000004).toFixed(2)); // Returns 0.3
What we've done in the above example is take the imperfect result from our first example, convert this into a string representation of the number, fix the result to exactly 2 digits after the decimal place and then convert this back into a floating point value. After doing so we finally arrive at the correct answer.
Breaking down the above example at each stage:
- Then the toFixed() method accepts a numeric value between 0 and 20 (we chose 2 so as to make the value more readable/user-friendly) and rounds the number if necessary
- Finally, as the toFixed() method returns a string value, we convert this into a float through use of the parseFloat function
parseFloat(0.30000000000000004.toPrecision(2)); // Returns 0.3
In the above approach we start to convert the numeric value to a string with the toPrecision() method; fixing the number of digits after the decimal place within the string representation of the number to 2 decimal places. This is then converted to a floating point number using the parseFloat method.
As with the previous method we also arrive at the correct answer with this approach.
Why a third method?
Well, there's a problem with the previous methods when using a precision of 2 decimal places: rounding.
For example, with method 2:
parseFloat(931.175.toPrecision(2)); // Returns 930
Seeing a value of 930 when we expect 931.18 demonstrates that this method is clearly not accurate enough to be a reliable solution for precision math.
It gets only slightly better with Method 1:
parseFloat(Number(931.175).toFixed(2)); // Returns 931.17
We should expect to see 931.18 but get 931.17 instead.
Maybe we could try the following method:
Math.round(parseFloat((931.175 * Math.pow(10, 2)).toFixed(2))) / Math.pow(10, 2); // Returns 931.18
Bingo! And just to be doubly sure let's go back 1 digit:
Math.round(parseFloat((931.174 * Math.pow(10, 2)).toFixed(2))) / Math.pow(10, 2); // Returns 931.17
The difference with this approach (aside from more operations - and being a tad more complex) is that we use multiplier operations and the Math.round method to get a higher degree of accuracy.
So if we break it down:
- We multiply the value we are operating on by 100 (10 to the power of 2 or 10 * 10)
- Then we fix the number to 2 digits after the decimal place using the toFixed() method
- Next step is to convert to a float the string representation of the number using the parseFloat() method
- We round this number using the Math.round() method to give a higher degree of accuracy
- Then divide this value by 100 (to convert the number's decimal separator back to its original place)
All of the above is fine and well but what about performance? Whenever we are using custom functions to process large volumes of data we have to take this into consideration. How quickly do our functions execute? Are there any bottlenecks? What could be improved? Can they perform under heavy loads?
Using the following methods:
We can perform some basic speed tests to give us an approximation as to how effectively our scripts are performing.
Using Safari, Google Chrome and Firebug we ran the following in the browser console:
console.group("Function #1"); console.time("Function #1"); parseFloat(931.175.toPrecision(2)); console.timeEnd("Function #1"); console.groupEnd("Function #1"); console.group("Function #2"); console.time("Function #2"); parseFloat(Number(931.175).toFixed(2)); console.timeEnd("Function #2"); console.groupEnd("Function #2"); console.group("Function #3"); console.time("Function #3"); Math.round(parseFloat((931.175 * Math.pow(10, 2)).toFixed(2))) / Math.pow(10, 2); console.timeEnd("Function #3"); console.groupEnd("Function #3");
Interestingly the first function we tested, using toPrecision(), took the longest to execute making this quite an expensive process for the browser to perform. If we had a large volume of data to process this function would be quite inefficient and likely cause performance issues due to the long execution time.
Our second function, which we expected to have the largest performance footprint due to a triple step data type conversion (Number to String to Float), was surprisingly the quickest. In terms of speed this would definitely be the fastest method to use on large volumes of data but, as we saw earlier, due to significant rounding errors we would not be able to rely on this function for accuracy.
Although the execution times were significantly reduced in Safari they still mirrored, to some extent, those of Google Chrome. Interestingly the gap between the execution time for Function #1 and Function #3 was almost negligible in Safari whereas in Chrome they were hugely significant.
Finally our test in Firebug yielded wildly different results compared to those of Google Chrome and Safari. The execution times were all significantly reduced with our third function actually being the quickest out of all those tested. However the differences between all methods were relatively small, particularly between Function #2 and Function #3.
And that is where we wrap things up!
We hope you enjoyed the above entry and would love to receive any comments you may have concerning the topics we explored today.