preface

Because JavaScript uses the IEEE-754 standard for floating-point numbers, it can’t accurately represent many real numbers, so some exist. This paper is the other side of the problem to do a thorough exploration and explore the corresponding solutions.

1 problem

Before, the company’s business was cross-border e-commerce, and there would be businesses requiring front-end calculation of taxes and fees, such as the following

65.00 (commodity price) * 0.119 (taxes and fees) = 7.734999999999999Copy the code

In this calculation, the accuracy is controlled to keep all the data to 2 decimal places, so the function toFixed is used

(65.00 (commodity price) * 0.119 (taxes)). ToFixed (2) = 7.73 (not meeting expectations)Copy the code

When Ben first saw the answer, he naively thought his browser had broken down, so he ran the numbers on his phone and got 7.74. The question was, who was wrong? Therefore, Ben consulted a lot of relevant materials and hands-on experiments, and finally had the birth of this paper.

2 floating point number

Before we solve the problem, we need to understand what floating point numbers are.

2.1 What is a floating point number

In the development of computer system, many ways to express real numbers have been put forward. Typical examples are Fixed Point numbers as opposed to floating Point numbers. In this expression, the decimal point is fixed somewhere in the middle of all the numbers in the real number. The disadvantage of fixed-point number expression method is that its form is too rigid. Fixed decimal position determines the integer part and decimal part of fixed number, which is not suitable for expressing large or small numbers at the same time. Eventually, most modern computer systems adopted what is known as floating point representation. Significand this expression uses scientific notation to express the real numbers, namely, a Mantissa (Mantissa, sometimes called significant number — Significand; Mantissa is actually an informal term for significant numbers), a Base, an Exponent, and a sign for positive and negative to represent real numbers. For example, 123.45 can be expressed as 1.2345 × 102 in the decimal scientific counting method, where 1.2345 is the mantissa, 10 is the cardinal number and 2 is the exponent. Floating-point numbers use exponents to achieve the effect of floating decimal points, which can flexibly express a wider range of real numbers.

Floating-point numbers are stored in computers in a finite number of consecutive bytes. In the IEEE standard, floating point number is to divide all the binary bits of consecutive bytes of a specific length into a sign field of a specific width, an exponential field and a mantissa field. The stored values are used to represent the symbol, exponent and mantissa of a given binary floating point number, respectively. In this way, a given value can be expressed by mantissa and adjustable exponents (hence the name “floating point”).

2.2 IEEE floating point number

Floating-point numbers are stored in computers in a finite number of consecutive bytes. In the IEEE standard, floating point number is to divide all the binary bits of consecutive bytes of a specific length into a sign field of a specific width, an exponential field and a mantissa field. The stored values are used to represent the symbol, exponent and mantissa of a given binary floating point number, respectively. In this way, a given value can be expressed by mantissa and adjustable exponents (hence the name “floating point”). Many languages use this canonical floating-point notation, and javascript is no exception.

IEEE 754 specifies:

There are two basic floating point formats: single precision and double precision.

The IEEE single-precision format has a 24-bit significant digit precision and occupies a total of 32 bits. The IEEE double precision format has 53 significant digit precision and takes up a total of 64 bits.Copy the code

2.3 Range and accuracy

It can be seen from the above that the number represented in the computer is limited. Javascript, as a dynamic language, has only one number type. The nubmer type uses double precision floating point numbers in the IEEE754 standard. That is to say, the storage space of a number in JS is fixed. Now let’s see how much room there is for certainty.

So the question is, what happens when you run out of space for a number with 52 bits, so that binary has infinite numbers like decimal?

The rounding rule for floating point numbers adopted by IEEE754 is sometimes referred to as Round to Even

This is a bit like the familiar rounding of the decimal system, where less than half is rounded and more than half (including half) is advanced. For binary floating-point numbers, however, there is an additional rule, which is that when the value to be rounded is exactly half the value, instead of simply rounding, the last significant digit of two equally spaced savable values is taken to be zero.

3 Calculation accuracy

Before solving the problem, we also need to understand the calculation process here with a most classic example to illustrate the process of js data calculation

0.1 + 0.2
Copy the code

A lot of people have seen this expression, so what exactly happens behind this expression, look at this step by step

A Decimal to binary

In the first step, the browser will convert the decimal values we saw 0.1 and 0.2 to the binary values 0.1 and 0.2

For converting from decimal to binary, most people know that integers are mod by 2, arranged in reverse order until the quotient is 0. But decimals, the rules are not the same as integers, the rules are as follows

Multiply by 2 and round until the fractional part of the product is zeroCopy the code

With the rules in place, we now do a transformation of 0.1, 0.2

0.1 Converting to Binary

Binary 0.00011001100110011... (Loop 0011) Mantissa 1.1001100110011001100... 1100 (a total of 52 bits, except the 1 to the left of the decimal point), the index is -4 (binary shift code is 00000000010), the sign bit is 0 Computer storage is: 0 00000000100 1001100110011... Because the tail up to 52, 11001 so actually stored value of 0.0001100110011001100110011001100110011001100110011001101Copy the code

0.2 Conversion to Binary

Binary 0.0011001100110011... (Loop 0011) Mantissa 1.1001100110011001100... 1100 (a total of 52 bits, except the 1 to the left of the decimal point), the index is -3 (binary shift code is 00000000011), the sign bit is 0 storage is: 0 00000000011 1001100110011... Because the tail up to 52, 11001 so actually stored value of 0.001100110011001100110011001100110011001100110011001101Copy the code

The binary of 0.1 + the binary of 0.2 here’s the binary addition rule

Computer calculation of binary addition is divided into three parts, the first step is to convert two addends to binary numbers, the calculation of two addends without carrying the sum, the result. The second part takes the two addends and (&). The third part uses the result of and operation to carry out the left shift operation (<<) (at the same time to calculate the sum of two addends needed to carry), and the result is obtained. Repeat this operation with the result of the or - different operation and the result of the left shift operation as two new addends. Until the result of and is 0, the result of xor is the binary number corresponding to the sum of the two addends.Copy the code

Following the above rules, we have completed the first step of both 0.1 and 0.2, and now we need to proceed to the second and third steps. The final result is as follows

0.01001100110011001100110011001100110011001100110011001100 
Copy the code

Then the binary result is converted to a decimal representation, the following is the rule of converting a binary decimal to a decimal

The integer part is calculated from right to left by multiplying each binary number to the corresponding power of 2, while the decimal part is calculated from left to rightCopy the code

The final result is 0.30000000000000004

Well, a 0.1+0.2 calculation would be something like this.

4. Rounding problems

4.1 the problem

Now, we can answer the first question, why did the problem situation that we calculated at the beginning of the article occur?

1 65.00 (commodity price) * 0.119 (tax) = 7.734999999999999! = = 7.735Copy the code
2 (65.00 (commodity price) * 0.119 (taxes)). ToFixed (2) = 7.73! = 7.74Copy the code

Why doesn’t the first multiplication equal 7.735? Ben explained it in section 3. Now let’s talk about the second rounding accuracy problem.

When rounding a number, it is also necessary to convert the decimal number we understand into the binary number that the computer understands, and then perform the corresponding operation according to the rounding rules of the computer (as described in 2.3 Range and Precision).

When 65.00 (commodity price) * 0.119 (tax) is stored in binary, it is already an approximation, and then the result is rounded in binary and converted to decimal of course, there is an error.

The toFixed() method is called in a decimal world, while the actual operation is in a binary world. There are two different worlds with different rules.

4.2 Solutions

As mentioned earlier, decimal decimal can easily be represented inaccurately when converted to binary, but decimal integers can be represented accurately when converted to binary, because the rules for converting decimal integers to binary are as follows

Mod by 2, order in reverse order until the quotient is 0Copy the code

So savvy students will come up with the following methods

Put decimals in place integer (multiple) and reduce back to multiple (divide multiple)Copy the code

The following function can be written in the same way

function toFixed(num, s) {
  var times = Math.pow(10, s)
  var des = num * times
  des = Math.round(des) / times
  return des + ' '
}
Copy the code

Note that math.round () is used to convert the number to the nearest integer, which is manually incremented by 0.5 if parseInt() is used. Here are the main differences between the three methods.

Math.round Effect: Round, return the parameter +0.5, then round down. Math. Round (5.57) & emsp; &emsp; // Return 6 math.round (2.4) &emsp; &emsp; Math.round(-1.5)&emsp; // Return 2 math.round (-1.5)&emsp; &emsp; // Return -1 math.round (-5.8)&emsp; &emsp; / / return - 6Copy the code
ParseInt parseInt parseInt parseInt parseInt parseInt parseInt parseInt parseInt parseInt parseInt parseInt parseInt parseInt The parseInt (5.57) & emsp; &emsp; // Return 5 parseInt(2.4)&emsp; &emsp; // Return 2 parseInt(-1.5)&emsp; &emsp; // return -1 parseInt(-5.8)&emsp; &emsp; / / return - 5Copy the code

5 concludes

1. Seemingly infinite numbers are infinite in the binary representation of a computer, so there is “dropping” due to the memory bit limit, and precision loss occurs.

2. The solution to the loss of accuracy is to place decimals into integers (multiplied by multiples) and then shrink back to the original multiples (divided by multiples).