1. The introduction

It is well known that JavaScript only has the numeric type Number, and Number adopts the 64-bit double precision floating point encoding in IEEE754 specification. The classic 0.1 + 0.2 === 0.30000000000000004 problem arises.

Let’s deduce the calculation of 0.1 + 0.2 with the attitude of knowing what and why.

2. Base conversion

First we need to know how to convert a decimal number to binary, as follows:

Multiply the number after the decimal point by 2 and take the integer part of the result (either 1 or 0), then multiply the decimal part by 2 again and take the integer part of the result… And so on, until the decimal part is zero or the number of digits is enough. Then put the integer parts in order

According to the above method, we take the binary number of 0.1, and find that the binary number converted by 0.1 is:

0.000110011001100110011 (0011 infinite loop)……

So the loss of precision isn’t a problem with the language, it’s an inherent flaw in floating-point storage. Floating-point numbers cannot accurately represent all of the values in their range. They can only accurately represent the values that can be represented by the scientific notation M *2^e. For example, if the scientific notation of 0.5 is 2^(-1), they can be accurately stored. 0.1 and 0.2 cannot be accurately stored.

So how do you store this infinite loop of binary numbers? You can’t just arbitrarily cut them off. This time the role of IEEE754 specification is reflected.

3. The IEEE754 standard

IEEE754 gives a definition of floating point number representation. The format is as follows:

(-1)^S * M * 2^E

The meaning of each symbol is as follows: S, the sign bit, determines the positive and negative, 0 is positive, 1 is negative. M is the significant number, greater than 1 and less than 2. E is the exponent.

Then 0.1 is represented by IEEE754 specification:

(1) ^ 0 * 1.100110011 (0011)… * ^ 2-4

For floating-point storage in computers, IEEE754 specification provides single-precision floating-point encoding and double-precision floating-point encoding.

IEEE754 specifies that for 32-bit single-precision floating-point numbers, the highest 1 bit is the sign bit S, the next 8 bits are the exponent E, and the remaining 23 bits are the significant digit M.

For 64-bit double-precision floating-point numbers, the highest 1 bit is the sign bit S, the next 11 bits are the exponent E, and the remaining 52 bits are the significant digit M.

digits Order number Significant digit/mantissa
Single-precision floating point number 32 8 23
A double – precision floating – point number 64 11 52

We take the single-precision floating point number as an example to analyze the actual storage mode of 0.15625.

When translated to binary, 0.15625 is 0.00101, which in scientific notation is 1.01 * 2^(-3), so the sign bit is 0, indicating that the number is positive. Note that the next 8 bits do not store the exponent -3 directly, but rather the order, which is defined as follows:

Order = exponent + offset

For single precision data, the specified offset is 127, and for double precision, the specified offset is 1023. So the order of 0.15625 is 124, represented in 8-bit binary numbers as 01111100.

Note also that when storing significant digits, we will not store the 1 before the decimal point (because the binary significant digit must start with a 1), so we store 01, less than 23 digits, and fill out the rest with zeros.

Of course, there is a problem that needs to be described here, for the significant number of infinite loop 0.1 how to truncate the number, IEEE754 default rounding mode is:

Round to nearest, ties to even

That is, round to the nearest value that can be represented, and take an even value if there are two numbers that are equally close.

4. Return to 0.1 +0.2===0.30000000000000004

JavaScript stores all values of the Number type in 64-bit double precision floating-point numbers. According to the IEEE754 specification, the binary Number of 0.1 retains only 52 significant digits. The 1.100110011001100110011001100110011001100110011001101 * 2 ^ (4). We in order to segment the sign bit, digital and efficient digital bits, is 0.1 when the actual storage bit pattern is 0-01111111011-1001100110011001100110011001100110011001100110011010.

By the same token, the binary number is 1.100110011001100110011001100110011001100110011001101 0.2 * 2 ^ (3), 0.2 when the actual storage so bit pattern is 0-01111111100-1001100110011001100110011001100110011001100110011010.

Expand 0.1 and 0.2 according to actual conditions and add zeros at the end, the result is as follows:

0.00011001100110011001100110011001100110011001100110011010 + 0.00110011001100110011001100110011001100110011001100110100 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- = 0.01001100110011001100110011001100110011001100110011001110Copy the code

Keep only 52 valid number, (0.1 + 0.2) the result of the binary number is 1.001100110011001100110011001100110011001100110011010 * 2 ^ (2), omit the tail end of 0, The 1.00110011001100110011001100110011001100110011001101 * 2 ^ (2), Thus (0.1 + 0.2) at the time of actual storage mode is 0-01111111101-0011001100110011001100110011001100110011001100110100.

The result of (0.1 + 0.2) has a decimal number of 0.30000000000000004, so the derivation is complete.

We can verify in Chrome that our derivation is consistent with the browser.

The newbie tool also provides a rich base conversion function that allows us to verify the accuracy of the results.

(0.1). The toString ('2') / /"0.0001100110011001100110011001100110011001100110011001101"(0.2). The toString ('2') / /"0.001100110011001100110011001100110011001100110011001101"(0.1 + 0.2). The toString ('2') / /"0.0100110011001100110011001100110011001100110011001101"(0.3). The toString ('2') / /"0.010011001100110011001100110011001100110011001100110011"
Copy the code

5. Solve the problem of accuracy loss

5.1 class library

There are many math libraries on NPM that support JavaScript and Node.js, such as math.js, Decimal. js, d.js, and so on

5.2 Native Methods

The toFixed() method rounds the Number to a specified decimal Number. But it does not mean that the method is reliable. Chrome tests the following:

ToFixed (1) // 1.4 correct 1.335.toFixed(2) // 1.33 error 1.3335. ToFixed (3) // 1.333 Error 1.33335.toFixed(4) // 1.3334 correct 1.333335. ToFixed (5) // 1.33333 error 1.3333335Copy the code

We can rewrite toFix. To determine whether the last digit is greater than or equal to 5, first multiply the decimal number by a multiple to a whole number, then divide it by a multiple to a decimal number, so as not to judge by a digit. See article.

5.3 ES6

ES6 adds a tiny constant to the Number object, number.epsilon

Number.epsilon // 2.220446049250313E-16 number.epsilon. toFixed(20) //"0.00000000000000022204"
Copy the code

The purpose of introducing such a small quantity is to set a margin of error for floating-point calculations, and if the error can be less than Number.EPSILON, the result can be considered reliable.

Error checking function (from “INTRODUCTION to ES6 Standards” – Ruan Yifeng)

function withinErrorMargin (left, right) {
    return Math.abs(left - right) < Number.EPSILON
}
withinErrorMargin(0.1+0.2, 0.3)
Copy the code