Understanding JavaScript’s Weird Decimal Calculations

Dom Carmel Tremblay
9 min readApr 8, 2019

--

As developers (or future developers), we need to be aware of the way JavaScript handles numbers. JavaScript does have its fair share of quirks and oddities and number manipulation does not seem to be any different. The classic example that everyone keeps referring too is the following calculation:

0.1 + 0.2 === 0.3 // this is false!because0.1 + 0.2 = 0.30000000000000004

What? Is this another quirk of JavaScript? Actually, it isn’t. We get the same weird result in any language using double-precision floating numbers. To understand why we need to embark on an exciting journey to learn computer calculation!

Photo by Ruediger Theiselmann on Unsplash

How computers represent data internally

Computers are using the binary system to represent numbers. This system uses only 2 digits called bits to represent numbers: 0 and 1. The binary system is well suited for computers for 2 main reasons:

  1. 0 and 1 are states that can easily be represented with electrical current. The absence of voltage means 0 and a presence of voltage means 1.
  2. Using more than 2 states could lead to errors because the voltage amplitude would be less drastic between states. Consequently, any variations in the voltage could have a significant impact and corrupt the data.

Before getting into the details of the binary system, let’s review the decimal system that we learned since we were kids. It will allow us to better understand how binary works.

The decimal number system

The decimal system is a positional system using a base of 10 and 10 digits (0,1,2,3,4,5,6,7,8,9) where the value of each digit depends on its position. For example, these are the place values of each digit in the number 4567. Starting from the right, each position is increasing by a power of 10.

To have the final value, we calculate the following:

(4 x 1000) + (5 x 100) + (6 x 10) + (7 x 1) = 4567

The binary number system

Like the decimal system, binary is also a positional system, but with a base 2 and only 2 binary digits or bits(0, 1). Again each value depends on its position. Let’s represent 75 in binary. Starting from the right, each position is increased by a power of 2.

75 in binary
(0 x 128) + (1 x 64) + (0 x 32) + (0 x 16) + (1 X 8) + (0 x 4) 
+ (1 x 2) + (1 x 1) = 75

Taken for granted that we use a full eight bits for a positive number, the maximum value we can represent is 255.

255 in binary
1 x 128) + (1 x 64) + (1 x 32) + (1 x 16) + (1 x 8) + (1 x 4) 
+ (1 x 2) + (1 x 1) = 255

The minimum being 0, we can represent 256 different values using 8 bits. However, the maximum and minimum values do change if we consider negative numbers.

The binary representation of negative numbers

The left-most bit is used by computers to identify a negative number. If it’s 1, it’s a negative number and if it’s 0, it’s a positive number. For example, to represent -75, the binary representation is the following.

-75 in binary

Because it’s a negative number, the left-most bit needs to be 1 and its value is -128. We must then add values to get to -75.

(1 x -128) + (0 x 64) + (1 x 32) + (1 X 16) + (0 x 8) + (1 x 4) 
+ (1 x 1) = -75

In that case, the maximum positive value that can be represented is 127 and the minimum value is -128. We have still 256 possible different values, but the range is different. We’ve seen how to represent positive and negative values. What about fractional values? Let’s see how it goes with fractions.

Representing fractions in binary

Before getting into a floating-point representation, let’s examine how fractions can be stored in 16 bits. The first 6 bits form the fractional part and the next 10 bits the whole number.

As we can see, the values for the fractional part are halved with each position. We can calculate the decimal value of the whole number with the following:

(0 x -512) + (0 x 256) + (1 x 128) + (1 x 64) + (1 x 32) + (1 x 16) + (1 x 8) + (1 x4) = 252

And the decimal value of the fractional part is the following:

(1 x 0.5) + (0 x 0.25) + (1 x 0.125) + (1 x 0.0625) + (0 x 0.03125) + (0 x 0.0156) = 0.6875

Adding the two parts together:

252 + 0.6875 = 252.6875

Having a fixed fractional part is convenient to understand how it works, but computers are using a floating-point representation.

Floating-point representation in binary

To understand floating-point representation in binary, we need to take a moment and have a look at the scientific notation where any number can be expressed with a significant (sometimes referred as the mantissa) and an exponent.

Some examples of numbers using the scientific notation

Computers do follow this notation to represent real numbers in the binary system. Let’s use 16 bits to represent a floating-point number. Starting from the right, the first 6 bits are used to represent the exponent and the following 10 bits represent the significant.

The left-most bit of both the significant and exponent is a signed bit. 1 for the significant means that it’s a negative number and 1 for the exponent means it’s a negative exponent.

To have the full picture, we need to place an imaginary binary point at the following location.

Let’s find out the equivalent decimal value. First, we need to calculate the exponent. We know the exponent is positive since its signed bit contains 0.

We can obtain the value of the exponent with the following:

(0 x -32) + (0 x 16) + (0 x 8) + (0 x 4) + (1 x 2) + (1 x 1) = 3

We then get the following expression:

To transform it into decimal notation, the binary point needs to be moved by 3 (exponent value) positions to the right. Then we can obtain the value with the following:

We can obtain the final decimal value with the following calculation:

(0 x -8) + (1 x 4) + (0 x 2) + (0 x 1) + (1 x 0.5) + (0 x 0.25) + (1 x 0.125) + (1 x 0.0625) + (1 x 0.03125) + (1 x 0.0156) = 4.7343

Now that we understand better how floating-point numbers are being represented in binary, let’s explore how JavaScript handles numbers.

Binary representation in JavaScript

The representation of numbers in JavaScript follows the format according to the IEEE-754 standard. Every number (of type Number) in JavaScript is stored as a double-precision floating-point number with 64 bits. 1 bit is used for the sign, 11 bits for the exponent, and 52 bits for the significand. Note that the positions of the exponent and the significand are inverted, but the principle remains the same.

Rounding errors resulting from floating-point numbers

The problem with floating-point numbers comes from the fact that not all numbers can be represented accurately with a computer. First, let’s take an example in decimals. If we divide 1/3, the decimals are repeating infinitely.

0.33333333333333333333333333...

This comes from the fact that the denominator (3) has not a prime that’s occurring in the base (10). In other words, fractions such as 1/3, 2/3, 1/7, 5/6 will have digits that repeat periodically. The same situation occurs with binary.

In binary, only numbers where the denominator is a power of 2 can be represented finitely. For example, 1/10 in binary will have periodic digits.

0.00011001100110011001100110011...

The following is the scientific notation of the binary number above:

This is the binary number we obtain with 64 bits (I am not going into the details of the conversion calculation here. To know more about it, you can read this excellent article from Max Koretskyi).

Since we have a limited amount of bits (52) to represent a number with an unlimited fraction, we need to round the number. In JavaScript, the binary number above will be rounded to obtain the following:

Now because the number needs to be rounded, we are losing some precision even with 52 bits. The resulting binary number is, in fact, an approximation of the actual value.

We repeat the process for the representation of 0.2 and the binary is also rounded. Then the two values are added together and the sum is also rounded. In the end, the result of the operation in decimal becomes:

0.3000000000000000444089209850062616169452667236328125

But we see only:

0.30000000000000004

That was quite a journey

Quite a journey indeed. To better understand rounding errors produced by floating-point arithmetic, we learned along the way how computers store numbers in binary and how to represent positive and negative values, and finally how to represent floating-point values in JavaScript. We learned that some numbers cannot be accurately represented by computers and binary numbers with periodic digits need to be rounded. That is what is causing the weird result when executing 0.1 + 0.2.

Question is how can we avoid rounding errors with the arithmetic of floating-point values? One solution is to work with integers whenever possible. For example, when using currency values and performing the following operation, we get:

10.10 + 10.20 = 20.299999999999997

Working in cents instead, thus integer values, we obtain:

1010 + 1020 = 2030 / 100 = 20.3 

Another approach and probably a better one is to use a math library such as math.js. I hope this clarifies how computers and more specifically JavaScript handle number representation in binary. If you wish to continue your journey to learn more, make sure to visit the following references:

--

--

Dom Carmel Tremblay

As a passionate software engineer and tech educator, I'm committed to clarifying the complexity of web development, data science, and artificial intelligence.