Understanding JavaScript’s Weird Decimal Calculations
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!
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:
- 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.
- 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.
(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.
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.
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:
- The binary number system,
Computer Science (video series). - Here is what you need to know about JavaScript’s Number type, Max Koretskyi.
- How numbers are encoded in JavaScript, 2ality.
- What Every JavaScript Developer Should Know About Floating Points, Brian Rinaldi.
- Binary Fractions, ElectronicsTutorial.