preface

With the rapid development of the Internet, online consumption has become the mainstream and integrated with the public life. As a program monkey, in the case of ensuring consumer privacy security, the accurate calculation of the amount of money has become a very important part. The rules of addition, subtraction, multiplication and division of numbers are easy for us, but do you know the rules of addition, subtraction, multiplication and division of computers?

In JavaScript, 0.1 + 0.2 does not equal 0.3. I don’t think only front-end programmers know this, or even back-end programmers know this, but do you understand these inequalities 👇

12 * 0.6! = 7 (7.199999999999999) 0.3 + 0.6! = 0.9 (0.8999999999999999) 0.4 * 0.7! = 0.28 (0.279999999999997) 0.9 * 0.2! = 0.18 (0.18000000000000002)... .Copy the code

In fact, there are many such examples, here xiaobian will not list one. As an excellent developer, how can only know, and do not know why, today with xiaobian together to get to the bottom of it!!

Knowledge reserves

In order to make it more convenient for everyone to understand the content of the following article, let’s first ‘wake up’ some math knowledge!

  • Science and technology law
  • Conversion between decimal and binary
  • Binary and power operations

Conversion between decimal and binary

Here is a simple review, unseal the sleeping memories of many years.

Decimal to binary

Take decimal 10.3 for example. Since the integer part and the decimal part are converted to binary in a different way, let’s break it down and start with the integer part 10. The integer part adopts the method of “mod 2 and reverse order”. Specific approach is: use 2 to divide decimal integer, can get a quotient and remainder; Then divide the quotient by 2, and another quotient and remainder will be obtained, and so on until the quotient is less than 1. Then, the remainder obtained first as the lowest significant bit of the binary number, and the remainder obtained later as the highest significant bit of the binary number are arranged in order.

10/2 = 5... 0 ===== mod is 0 5/2 = 2... 1 ===== mod is 1 2/2 = 1... 0 ===== mod is 0 1/2 = 0... 1 ===== mod is 1Copy the code

It’s in reverse order, so 10 to binary is 1010. Now let’s convert 0.3. The decimal part adopts the method of “round by 2 and arrange in order”. The specific approach is: multiply the decimal fraction with 2, you can get the product, take out the integral part of the product, and then multiply the remaining decimal part with 2, and get a product, and then take out the integral part of the product, and so on, until the decimal part of the product is 0, at this time 0 or 1 is the last bit of binary.

0.3 * 2 = 0.6 = = = = = integer is 0 0.6 * 2 = 1.2 = = = = = integer is 1 0.2 * 2 = 0.4 = = = = = integer is 0 0.4 * 2 = 0.8 = = = = = integer is 0 0.8 * 2 = 1.6 = = = = = integer is 1 0.6 * 2 = 1.2 = = = = = integer is 1 0.2 * 2 = 0.4 = = = = = integer is 0 0.4 * 2 = 0.8 = = = = = integer is 0 0.8 * 2 = = = = = = 1.6 is integer 1......Copy the code

Have you noticed that decimal 0.3 converted to binary is 0.0100110011001 in an infinite loop? , and finally merge the integer parts after transformation, that is, get 1010.0100110011001…… .

Of course, according to the computer’s storage rules, is unable to store infinite repeating decimals, so how to deal with it? The following will be explained yo!!

Binary to decimal

The conversion of binary to decimal also needs to be separated into integer and decimal parts. The integer part is multiplied from right to left by the corresponding power of 2 and increasing, the decimal part is multiplied from left to right by the corresponding power of 2 and decreasing, and then add the two parts. Take binary 1011.01 as an example:

/ / integer part 1011 = = = = 1 + 1 * 2 * 2 ^ 0 0 * 2 ^ ^ 1 + 2 * 1 * 2 ^ 3 = 11 / / decimal part 0.01 = = = = 0 + 1 * 2 * 2 ^ 1 ^ 2 = 0 + 0.25 = 0.25Copy the code

Finally, when the two parts are added together, binary 1011.01 is converted to decimal as 11 + 0.25 = 11.25

Scientific enumeration

A number is expressed as the form of a 10 n power multiplication (1 | | or less a < 10, n counting method for integer). For example 🌰 :

The decimal system

782300 = 7.823 * 10^5

0.00012 = 1.2 * 10^−4

10000 = 1 * 10^4

Copy the code

The same logic applies to binary conversion

111001 = 1.11001 * 2^5

0.001101 = 1.101 * 2^-3

10000 = 1 * 2^4
Copy the code

That is: 5.0 in decimal, 101.0 in binary, or 1.01 * 2^2 in scientific notation.

Binary and power operations

Make sure 0.1 + 0.2! The reason why = 0.3, there’s a lot of binary arithmetic involved, do you remember the following calculations?

  • Convert binary 1111111111 to decimal

    2^0 + 2^1 + 2^2 +… Plus 2 to the ninth, that’s a disaster. 1111111111 = 10000000000-1. In binary, this equation is true. Converting binary 10000000000 to decimal is relatively easy.

    1111111111 = 10000000000-1 = 2^ 10-1 = 1024-1 = 1023

  • Binary 2^-2 is converted to a decimal

    2^-2 = 1/2^2 = 1/100 = 0.01

  • Special operation

    2 ^ 0 = 1

Representation of numbers in JavaScript

Through the above description, I think the deep memory of the friends in the brain has been awakened, understanding the following content should be very smooth. All right, let’s get down to business!

In JavaScript, a decimal Number is represented by the Number type. In a computer, a decimal Number is converted to binary. Remember the binary conversion in the previous section, decimal 0.3 is an infinite decimal Number. How do you store binary numbers in a computer? Computers follow IEEE 754 standards to store a number as a 64-bit binary number.

The IEEE-754 binary Floating-point arithmetic standard is the most widely used floating-point arithmetic standard since the 1980s. It specifies four ways to represent floating-point values: Single precision (32 bits), double precision (64 bits), extended single precision (43 bits above, rarely used) and extended double precision (79 bits above, usually achieved as 80 bits). The Javascript programming language uses a double representation.

According to ieEE-754, any binary floating-point number V can be expressed as follows:

V = (-1)^S * M * 2^E
Copy the code
  • (-1)^S represents the sign bit. When S=0, V is positive. When S is equal to 1, V is negative.
  • M is a significant number, greater than or equal to 1, less than 2.
  • 2 to the E is the exponent bit.

In plain English, this representation translates a floating point number into the form of scientific notation (explained in section 2). For example, 5.0 in decimal, 101.0 in binary, 1.01 * 2^2 in scientific notation, S=0, M=1.01, E=2 in contrast to the representation of V above. If it is -5.0, the result is S=1, M=1.01, E=2.

In JavaScript programming language, following the representation form of V, the double precision 64-bit is divided into three parts (1 + 11 + 52) to represent S, E and M respectively.

  • Bit 62: symbol bit, 0 for a positive number and 1 for a negative number (s)
  • 52nd to 62nd: Storage index part (E)
  • Bits 0 to 51: Store decimal parts (f)

It is important to note that in computer storage, the 52-bit mantis f differs from the IEEE-754 V representation M: the mantis F stores only decimal places. This is because in binary, there are only 1 and 0, and the representation of M is 1.xxxx. By default, the first digit before the decimal point is 1, so it can be omitted to save 1 significant digit. In computer storage, only the fractional part XXXXX is retained. For example, when saving 1.01, only save 01, wait until read, add the first 1.

While we’re at it, I’d like to add that the maximum safe number for JavaScript is

MAX_SAFE_INTEGER == math.pow (2,53) -1 // trueCopy the code

The maximum safe number is what the exponent bit is when the mantissa f is all ones and the sign bit is zero. The exponent bit is 11 bits, and the maximum value is 2^ 11-1 = 2047, but the maximum safe exponent bit is not 2047.

If you think about it, the mansa f is 52 digits at most, and for binary infinite repeating decimals, you can’t determine whether the number after 52 digits is a 0 or a 1, only 52 digits are certain. According to the law of exponentials, if the exponent is greater than 52, you have to complete it with 0, which is not accurate. So the exponent of the maximum safe number is 52.

Max (1) ^ 0 = 52 = (10.000 * 1.1111111111111111111111111111111111111111111111111111 * 2 ^... 000-0.000... 001) * 2^52 = 2^ 53-1Copy the code

It works in theory, but let’s verify it in practice:

Anyone wondering why math.pow (2,53) isn’t the maximum safe value? Math.pow(2,53) == math.pow (2,53) + 1, so math.pow (2,53) and math.pow (2,53) + 1 convert to binary, In the computer storage is 9007199254740992, in the calculation and can not determine the accurate value.

Index a

I just mentioned the maximum value of the exponential bit, is the maximum value of the exponential bit really this value? The answer is no.

The index bit e contains 11 digits. The value ranges from 0000000000,11111111111, that is, [0,2^11-1]. It is expressed in decimal notation as [0,2047]. The problem with this storage is that when a decimal number is less than 0, the exponent should be negative.

According to ieeee-754 specification, the median value is offset to represent the negative index. The median value of [0,2047] is bias = 1023, that is to say, the range of the index is [-1023,1024], and the offset index is E = e-bias.

Now that we know the range of exponents and mantisses, the range of numbers that can be expressed in Javascript is [2^-1023,2^1024], beyond which Javascript will return an incorrect value (Infinity).

2^-1023 is expressed in scientific decimal notation as: 5 × 10^-324

2^1024 is expressed in scientific decimal notation as 1.7976931348623157 x 10^308

These two boundary values can be obtained by accessing the MAX_VALUE and MIN_VALUE attributes of the Number object, respectively:

Number.MAX_VALUE; / / 1.7976931348623157 e+308 Number. MIN_VALUE; // 5e-324Copy the code

As can be seen from the above figure, the offset index E has two limiting cases:

  • If E is 0, the exponent E is 1-1023 = -1022, which is 0.00 in decimal notation…… The decimal of XXX, which is very close to 0, is 0.
  • If E is all one and f is all zero, it means infinity. If the mantissa f is not all zeros, NaN is indicated.

Say so many, let’s take an example, decimal 5.0 in the computer how to express, deepen the understanding of small partners!

Decimal 5.0 === Binary 101.0 === Scientific notation 1.01 * 2^2

1.01 * 2^2 corresponds to the storage structure of the computer, that is:

  • The sign bit s = 0
  • The mantissa is f = 01
  • The exponent bit e = 2 + 1023 = 1025 (decimal), that is, 100 0000 0001

In this example, the offset exponent is 2, and the offset exponent is E = e-bias, i.e. E = 2, bias = 1024

Precision loss

Ok, back to the original question 0.1+0.2! = 0.3. The computer converts 0.1 and 0.2 to binary, adds them up, and finally converts the sum to decimal. It happens that 0.1 and 0.2 are all infinite loops when converted to binary.

0.1 => 0.0001 1001 1001 1001… (Infinite loop)

0.2 => 0.0011 0011 0011 0011… (Infinite loop)

The mantissa part of the storage structure can only represent 52 bits at most. In order to represent more than one number, we can only imitate decimal rounding, but binary only has 0 and 1, so it becomes 0 rounding. So 0.1 and 0.2 are stored in the computer in the form of:

0.1 => 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 101 0.2 => 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 001Copy the code

In scientific notation:

0.1 = > (1) - (1.1001100110011001100110011001100110011001100110011010) * 2 ^ - ^ 0 * 0.2 = 4 > ^ 0 * (1 -) (1.1001100110011001100110011001100110011001100110011010) * ^ 2-3Copy the code

When the order is not equal, it is necessary to “pair the order” first, change the smaller exponent into the larger exponent, and move the decimal part to the right accordingly:

0.1 = > (1) - (0.1100110011001100110011001100110011001100110011001101) * 2 ^ - ^ 0 * 0.2 = > 3 ^ 0 * (1 -) (1.1001100110011001100110011001100110011001100110011010) * ^ 2-3Copy the code

Final sum result:

1 * * 2 ^ (0.1100110011001100110011001100110011001100110011001101) - (3 + 1 * 1.1001100110011001100110011001100110011001100110011010) * * ^ 2-3 = 1 (10.0111001100110011001100110011001100110010011001100111) * 2 ^ - 1 * 3 standardization ( 1.0011001100110011001100110011001100110011001100110100) * 2 ^ - 2Copy the code

To convert binary to decimal:

/ / binary (1.0011001100110011001100110011001100110011001100110100) * 2 ^ 2 / / decimal (1 + 2 ^ 3 + 2 + 2 ^ ^ - 4-7 + 2 ^ - ^ 11 + 8 + 2 2^-12 + 2^-15 +... ^ - 50 + 2) * 2 ^ 2 = 0.3000000000000000444089209850062616169452667236328125Copy the code

Because of the accuracy, the end result:

0.30000000000000004
Copy the code

0.1 + 0.2 is a typical case of accuracy loss. It can be seen from the above calculation process that 0.1 and 0.2 have a precision loss in the process of binary conversion, and a second precision loss occurs after addition, so the result obtained is not accurate. And so it goes:

0.1 + 0.2 === 0.3  // false
Copy the code

To solve

Now that you know the cause of the problem, you have to find a way to solve it!

1. Convert decimals into whole numbers

Function to the add (num1, num2) {/ / converted to string, the length of the fractional part const Decimal1 = (num1. The toString (). The split ('. ') [1] | | "). The length; const Decimal2 = (num2.toString().split('.')[1] || '').length; Const baseNum = math.pow (10, math.max (Decimal1, Decimal2)); // Set baseNum = math.pow (10, math.max (Decimal1, Decimal2)); return (num1 * baseNum + num2 * baseNum) / baseNum; } / / calculation of 0.1 (0.1 + 0.2 + 0.2 * 10 * 10) / 10 = = = 0.3 / / trueCopy the code

This approach is the easiest to think of, the easiest to implement, and the most widely used at work. However, this approach is not very friendly for large numbers, and there will still be the problem of loss of accuracy.

2. Use third-party libraries

The problem of accuracy loss is not just in JavaScript, but also in Java, so there are a lot of good Math libraries that deal with accuracy loss. Here’s a library I use a lot.

Math.js

Math.js is an extensive math library for JavaScript and Node.js. It features a flexible expression parser with support for symbolic computation, comes with a large set of built-in functions and constants, and offers an integrated solution to work with different data types like numbers, big numbers, complex numbers, fractions, units, and matrices. Powerful and easy to use.

The meaning of Youdao translation is roughly as follows:

Math.js is an extensive math library for JavaScript and Node.js. It provides a flexible expression parser, supports symbolic computation, provides a large number of built-in functions and constants, and provides an integrated solution to handle different data types such as numbers, large numbers, complex numbers, fractions, units, and matrices. Powerful and easy to use.

Liverpoolfc.tv: mathjs.org/

Sometimes, however, people don’t want to introduce a library for problems that can be solved by a single function. That might as well take a look at the source of this library, so that their function is more robust, not out of the bug. Actually, this kind of library is very much, everybody can leave a message to tell me yo!

PS: Front-end technology changes with each passing day, but the change is never its ancestor, master the lowest level of knowledge, will not be afraid of update!