Random thoughts & observations

From the mundane to the profound and everything in between here’s what’s rocking our world

Handling floating point numbers in JavaScript

Posted: August 2, 2014
Written by: Saints At Play
Category: Javascript

If you've been working with JavaScript for some time you're probably familiar with data type conversions and number formatting. Standard run-of-the-mill tasks for sure but things can get quite interesting when you start to play with floating point numbers...

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?

0.30000000000000004

Welcome to the world of floating point numbers...

JavaScript and floats

If, for example, we type into the browser console:

0.7 + 0.1

We don't expect to find a result of:

0.7999999999999999

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.

Method 1

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:

  1. The Number object explicitly converts the supplied value into a number for processing (variables in JavaScript are mutable so we sometimes need to explicitly declare their data types where necessary)
  2. 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
  3. Finally, as the toFixed() method returns a string value, we convert this into a float through use of the parseFloat function

Method 2

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. 

Method 3

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

Result!

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:

  1. We multiply the value we are operating on by 100 (10 to the power of 2 or 10 * 10)
  2. Then we fix the number to 2 digits after the decimal place using the toFixed() method
  3. Next step is to convert to a float the string representation of the number using the parseFloat() method
  4. We round this number using the Math.round() method to give a higher degree of accuracy
  5. Then divide this value by 100 (to convert the number's decimal separator back to its original place)

All of which gives us a much more accurate approach to working with floating point conversions in JavaScript.

Performance

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?

A quick and easy way to gauge JavaScript performance is to use the Console object in the Console tab of the Developer Tools window.

Using the following methods:

  1. console.group()
  2. console.time()
  3. console.timeEnd()

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");

The results, bearing in mind that they will always be dictated by, and be at the mercy of, each browser's own internal JavaScript rendering engine (and therefore not globally applicable across all platforms), were quite interesting.

Google Chrome

Google Chrome console results for JavaScript speed test

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.

Our final function was, given the number of native JavaScript functions being used, a lot faster than we expected and, combined with its higher rate of accuracy in rounding floating point numbers, makes this a suitable candidate for processing large volumes of data.

Safari

Safari console results for JavaScript speed test

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.

Firebug

Firebug console results for JavaScript speed test

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.

Parting shots

So, after exploring some methods of parsing floating point numbers in JavaScript and handling rounding errors, we've used the browser console to perform some basic performance testing of our code. Through doing so we've gained some insights into which functions perform best and how those execution times will vary depending on each browser's internal JavaScript rendering engine.

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.

« Return to Posts

Comments

There are no comments

Posting comments after three months has been disabled.