0.1+0.2=0.30000000000000004, 1-0.9= 0.099999999999998, A lot of people know that this is a floating point error problem, but they don’t know exactly. This article will help you understand the principle behind this and the solution, will also explain to you JS large number crisis and the four operations will encounter the pit.

Storage of floating point numbers

The first step is to figure out how JavaScript stores decimals. Unlike other languages such as Java and Python, all numbers in JavaScript, including integers and decimals, have only one type – Number. It is implemented in accordance with IEEE 754 standards and is expressed in 64-bit fixed length, which is the standard double double floating-point number (and related float 32-bit single precision). It’s described in detail in the principles of how a computer is made, so don’t worry if you don’t remember.

The advantage of this storage structure is that it can normalize the processing of integers and decimals and save storage space.

The 64-bit bits can be divided into three parts:

  • The sign bitS: The first bit is the sign bit of positive and negative numbers (sign), 0 represents a positive number and 1 represents a negative number
  • Index aE: Middle 11-bit storage index (exponent), used to represent power numbers
  • Mantissa bitsM: The last 52 digits are mantissa (mantissa), the excess part will automatically enter a house of zeros

The actual number can be calculated using the following formula:

Note that the above formula follows scientific notation, which is 0

The final formula becomes:

So 4.5 is finally expressed as (M=001, E=1025) :

Here is another example to explain the reason for floating point error, 0.1 converted to binary representation of 0.0001100110011001100(1100 cycle), 1.100110011001100X2 ^-4, so E=-4+1023=1019; M drops the first 1 and gets 100110011… . And finally:

In decimal form0.100000000000000005551115123126, hence the floating point error.

Why 0.1+0.2=0.30000000000000004? The calculation steps are as follows:

/ / 0.1 and 0.2 are converted into binary operations again after 0.00011001100110011001100110011001100110011001100110011010 + 0.0011001100110011001100110011001100110011001100110011010 = 0.0100110011001100110011001100110011001100110011001100111 / / In decimal it is exactly 0.30000000000000004Copy the code

Why does x=0.1 get 0.1?

Congratulations, you’ve reached the point where mountains are not mountains. Because the fixed length of Mantissa is 52 bits, plus the omitted one bit, the maximum number that can be represented is 2^53=9007199254740992, which corresponds to the mantissa of scientific count is 9.007199254740992, which is also the maximum precision that JS can represent. Its length is 16, so you can use toPrecision(16) to calculate the accuracy. If you exceed the accuracy, you will automatically round it up. So we have:

0.10000000000000000555. ToPrecision (16) / / return 0.1000000000000000, remove just after the zero at the end of 0.1 / / but you see ` 0.1 ` is not actually ` ` 0.1. 0.1. ToPrecision (21) = 0.100000000000000005551Copy the code

The crisis of large number

You probably already have a vague sense of what happens if the integer is greater than 9007199254740992? Since E is 1023, the largest integer that can be represented is 2 to the 1024 minus 1, which is the largest integer that can be represented. But you can’t calculate it that way, because 2 to the 1024 becomes Infinity

> < span style = "box-sizing: border-box; color: RGB (51, 51, 51)Copy the code

So what happens to numbers between two to the 53 and two to the 63?

  • (2 ^ 2 ^ 53, 54), which can only be an even number
  • (2 ^ ^ 2, 54, 55), and can only accurately represent 4 multiples
  • . Skip more multiples of 2 in turn

The following diagram is a good representation of the JavaScript correspondence between floating point and Real numbers. The ones we use, minus 2^53, 2^53, are very small in the middle, and they get thinner and thinner and less precise as you go.

In the early order system of Taobao, the order number was treated as a number. Later, with the order number exploding, it has exceeded 9007199254740992. The final solution is to change the order number into a string.

To solve the problem of large numbers you can refer to the third party library bignumber.js, the principle is to treat all numbers as strings, re-implement the calculation logic, the disadvantage is that the performance is much worse than the native. Therefore, it is necessary to support large numbers. Now TC39 has a Stage 3 proposal, Proposal BigINT, and the problem of large numbers is expected to be solved completely. Babel 7.0 can be used before browsers support it, which automatically converts to big-INTEGER internally. Note that this keeps accuracy but reduces efficiency.

toPrecision vs toFixed

When processing data, the two functions can easily be confused. What they have in common is the conversion of numbers into strings for display. Do not use it in the middle of a calculation, only for the final result.

The differences need to be noted:

ToPrecision is the processing precision, the precision from left to right from the first non-zero number counting. ToFixed is the number of digits after the decimal point, counting from the decimal point. Both can round up extra numbers, and some people use toFixed to round up extra numbers, but be aware that this is buggy.

For example: 1.005.toFixed(2) returns 1.00 instead of 1.01.

Cause: 1.005 actually corresponds to the number 1.00499999999999989, in the round all were removed!

Solution: Use the professional rounding function math.round () to handle. Math.round(1.005 * 100) / 100 does not work because 1.005 * 100 = 100.499999999999. Math.round should also be used after both multiplication and division accuracy errors are resolved. This can be done using the number-Precision-# round method described later.

The solution

Back to my main concern: how to solve floating-point errors. First of all, it’s theoretically impossible to store an infinite number of decimals in a finite amount of space, but we can manipulate it to get what we want.

Data presentation class

When you have data like 1.4000000000000001 to display, it is recommended to use toPrecision and parseFloat to convert it to a number, as follows:

The parseFloat (1.4000000000000001 toPrecision (12)) = = = 1.4 / / TrueCopy the code

The encapsulation method is:

function strip(num, precision = 12) {
  return +parseFloat(num.toPrecision(precision));
}
Copy the code

Why choose 12 as the default precision? This is a rule of thumb choice, generally choosing 12 will solve most 0001 and 0009 problems, and is sufficient in most cases, you can increase if you need more precision.

Data operation class

For operational operations such as +-*/, you cannot use toPrecision. The correct way is to convert decimals into whole numbers and then operate. Take addition:

/ accurate addition * * * * / function add (num1, num2) {const num1Digits = (num1. The toString (). The split ('. ') [1] | | "). The length; const num2Digits = (num2.toString().split('.')[1] || '').length; const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits)); return (num1 * baseNum + num2 * baseNum) / baseNum; }Copy the code

The above method can be applied to most scenarios. Special processing is needed for scientific notation such as “2.3e+1” (when the number precision is greater than 21, the number will be forced to be displayed in scientific notation).

Reading this far means you’re very patient, so I’ll give you a bonus. When you encounter floating point errors, you can directly use github.com/dt-fe/numbe…

Perfect support for floating point number addition, subtraction, multiplication, division, rounding operations. Very small at 1K, much smaller than most similar libraries (math.js, BigDecimal.js), with 100% test coverage and readable code, you can use it in your application!

Reference Documents:

Github.com/camsong/blo…

Juejin. Cn/post / 684490…