The backend asks why the frontend numerical precision is lost?

I believe that all front-end partners will inevitably use JavaScript to process numeric related operations in their daily work, such as numeric calculation strong>, Keep specified decimal places, The value returned by the interface is too large, etc. These operations may cause the original normal value to be displayed in JavaScript code> does behave abnormally (**That is, loss of precision), which is also criticized by many developers (~Should you Have you ever stepped on a trap?~), of course including many back-end developers (~I have been asked this question more than once**~).

This article mainly includes Accuracy loss scenarios, reasons for accuracy loss, solutions and other aspects. If there is anything incorrect in the article, please share your opinions in the comment area.

Some friends said that this problem of loss of accuracy is related to basic content, or even content that has been published in college textbooks for a long time, or that a back-end developer cannot not know, and it is very headline-grabbing. I can only say It is right in your position (~It would be wrong to leave this position aside~).

Because not everyone has learned computer-related knowledge in college, or they have entered the software development industry through career changes, training, etc., this group of people may not have that much understanding of these so-called basic knowledge. Once this is in place, if you look at the title of this article based on this standpoint, you won’t be so angry.

Calculation of floating point numbers

Numerical calculations are still widely used in front-end applications, but when floating point numbers are involved in calculations, precision may be lost, as follows:

Add (+)

  • Normal calculation:0.1 + 0.2 = 0.3

  • JavaScript Calculation: 0.1 + 0.2 = 0.30000000000000004

Minus (-)

  • Normal calculation: 1 - 0.9 = 0.1

  • JavaScript Calculation: 1 - 0.9 = 0.09999999999999998

Multiply(*)

  • Normal calculation:0.0532 * 100 = 5.32

  • JavaScript Calculation: 0.0532 * 100 = 5.319999999999999

Except (/)

  • Normal calculation: 0.3 / 6 = 0.05

  • JavaScript Calculation: 0.3 / 6 = 0.049999999999999996

Exceeds the maximum value

The so-called exceeding the maximum value (maximum and minimum value) refers to exceeding Number.MIN_SAFE_INTEGER (- 9007199254740991), that is, + ( 2^53 – 1) or Number.MAX_SAFE_INTEGER (+ 9007199254740991), that is, a value in the range of - (2^53 – 1), which is the most common in the project The following situations are:

  • The value returned by the backend exceeds the maximum value

    • Example 1: The list data returned by the backend usually has a corresponding ID to identify uniqueness, but the backend generates this ID is of Long type, then the value is likely to exceed the largest positive integer that can be represented in JavaScript. At this time, the accuracy will be lost, that is, the ID value actually obtained by the front end and the one returned by the back end will be inconsistent.

    • Example 2: The backend may need to calculate some values and then return the corresponding result value to the frontend. At this time, if the value exceeds the maximum, there will also be a loss of accuracy.

  • When the front end performs numerical calculations, the calculation result exceeds the maximum value

Keep specified decimal places

In addition to the above scenarios involving floating point calculations that exceed the maximum value, we usually also process the numerical value to retain specified decimal places, and some developers may directly use Number.prototype.toFixed To achieve, but this method does not guarantee the results we expect. For example, when retaining decimal places, there will be problems when rounding is required, as follows:

console.log(1.595.toFixed(2))
console.log(1.585.toFixed(2))
console.log(1.575.toFixed(2))
console.log(1.565.toFixed(2))
console.log(1.555.toFixed(2))
console.log(1.545.toFixed(2))
console.log(1.535.toFixed(2))
console.log(1.525.toFixed(2))
console.log(1.515.toFixed(2))
console.log(1.505.toFixed(2))

The computer can actually only store/recognize **binary** internally, so documents, pictures, numbers, etc. will be converted into Binary , and for numbers, although we see the representation result of Decimal, in fact, the underlying process will be Decimal and Binary mutual conversion, and this conversion process may cause precision loss, because converting decimal to binary may produce an infinite loop part, and actually Storage space is limited.

IEEE 754 standard

Number storage in Javascript uses the **Double precision floating point**[2] data type specified in **IEEE 754**[1], double precision floating point Points use 64 bits (8 bytes) to store a **floating point number**[3], which can represent 53 bits in binary Valid digits, i.e. (0-52 bits are 1) 111...111 = (53 bits are 1, 0-52 bits are 0) 1000...000 - 1, which is 2^53 - 1, and this is also Number.MAX_SAFE_INTEGER ( + 9007199254740991) corresponding value.

The composition of double-precision floating point numbers

Double precision floating point number (double) consists of the following parts:

  • sign Sign bit, 0 is positive, 1 is negative

    • 1 bit at bit 63

  • exponent The exponent part, representing the power of 2

    • Accounting for 11 bits, at bits 52-62

    • The exponent uses the offset code representation, that is, the true value e of the exponent is added with an offset quantity, and then get the expand code (that is, the calculation result) and express it as a binary number

    • whereOffset = (2^n-1) - 1, n is the number of digits in theexponent (i.e. n = 11), so the offset is Math.pow(2, 11-1) - 1 = 1023

    • Exponent code E = Exponent true value e + Offset code (2^n-1) - 1

  • mantissa The mantissa part indicates the precision of floating point numbers

    • Accounting for 52 bits, in bits 0-51

    • The mantissa is expressed in an implicit way, that is, the highest bit of the mantissa always implies a 1 and is hidden in to the left of the decimal point (i.e. 1 < mantissa < 2), so the number of significant digits in the mantissa is 53 digits, not 52 Bit

Stored procedure for decimal floating point numbers

With the above formula, let's demonstrate how a decimal floating-point number is stored in the computer in the form of a double-precision floating-point number. This is roughly divided into the following two steps:

  • Decimal to binary
    • Convert decimal to binary for integer part and decimal part respectively

  • Find the values of sign, exponent, and mantissa

Below we use the value 263.3 to demonstrate.

Convert decimal to binary

Convert the integer part 263 and fractional part 0.3 of 263.3 to the corresponding binary number respectively. Here you can use Convenient **`online conversion tool`**[4], you can also choose to calculate manually:

  • Integer part to Binary

    • Keep dividing by 2 until the remainder is 0 or a cycle appears, and then combine the remainders from bottom to top

    • Use Number.prototype.toString() to verify

  • Decimal part to Binary

    • Keep multiplying by 2 until the product is 1 or a cycle occurs, then from top to bottom change the integer digitsof each product /strong> Just combine

    The final result is 263.3(10) and the corresponding binary is 100000111.010011001...

Find the values of sign, exponent and mantissa

  • Asking for sign
    • Where sign is the sign bit, and 263.3(10) is a positive number, so sign = 0

  • Asking for exponent
    • According to the formula (-1)^S x (1. M) x 2^(E-1023), we can know that The mantissa must conform to the form of 1. M, so the decimal point in 100000111.010011001... needs to be moved 8 digits to the left to become < strong>1.00000111 010011001 ...

    • Among them, 8 is the true value of the exponent, but when actually stored, it stores the binary number of exponent code. According to the formula exponent code = exponent True value (8) + offset (1023), that is, exponent = 1031, so the exponent value is 1031 Binary:10000000111

  • Asking for mantissa
    • According to the previous step 1.00000111 010011001 ... it is easy to know the mantissa mantissa = 00000111 010011001 ...

Final storage form

Regarding the rounding method of this method, the most common theory currently is the Banker Algorithm. Indeed, in most cases it can indeed comply with the rules of the Banker Algorithm, but in some cases it does not It does not comply with its rules, so strictly speaking Number.prototype.toFixed does not count as using **Banker's Algorithm**. If you want to ask why, please see **`ECMAScript? 2024 Language Specification (tc39.es)`**[5], which will be mentioned below.

Banker’s Algorithm

The so-called banker's algorithm can be summarized in one sentence as:

Consider rounding to five, after five if there is a number, round up one, after five if there are countless, look at odd and even , 五前 is odd and should be discarded, 五后 is odd and should be further into one

  • Rounding means that the value < 5 after the reserved digit should be rounded,4 Just a representative value

  • Six-in means the value> 5 after the reserved bit should be one,6 Just a representative value

  • If the value after the reserved digit = 5, check whether there is a number after 5
    • If there are countless numbers after 5, judge by looking at the oddness of the value before 5
      • If the value before 5 is an even number, reject

      • If the value before 5 is odd, then advance

    • If there is a number after 5, then Add one

Verify it with an example:

(1.1341).toFixed(2) = '1.13'


(1.1361).toFixed(2) = '1.14'


(1.1351).toFixed(2) = '1.14'


(1.1350).toFixed(2) = '1.14'


(1.1050).toFixed(2) = '1.10'

It doesn’t look like a problem, right?

(1.1051).toFixed(2) = 1.11 (correct √)
(1.105).toPrecision(17) = '1.1050000000000000'


(1.105).toFixed(2) = 1.10 (Correct √)


(1.125).toFixed(2) = 1.13 (Incorrect ×)
1.125.toPrecision(17) = '1.1250000000000000'


(1.145).toFixed(2) = 1.15 (Incorrect ×)
1.145.toPrecision(17) = '1.1450000000000000'


(1.165).toFixed(2) = 1.17 (Incorrect ×)
1.165.toPrecision(17) = '1.1650000000000000'


(1.185).toFixed(2) = 1.19 (Incorrect ×)
1.185.toPrecision(17) = '1.1850000000000001'

toFixed standard defined by ECMAScript

If you don’t understand it at first glance, then let’s try to explain the content of this standard (~Personal understanding~)!

  1. Letx = target number, such as: **(1.145).toFixed(2)** wherex = 1.145

  2. Letf = parameter, such as: **(1.145).toFixed(2)** wheref = 2

  3. If **f = undefined**, that is, no parameters are passed, then f = 0

  4. If **f = Infinite**, that is, infinite value is passed in, then RangeError exception will be thrown

  5. If **f < 0 or f > 100**, the passed in value is not between 0 - 100 If the value is between, a RangeError exception will be thrown.

  6. If **x = Infinite**, that is, if you want to operate on the inexact value reserved bit, return its string form

    • For example, **Infinity.toFixed(2) = 'Infinity', NaN.toFixed(2) = 'NaN'**

  7. Letx = **The mathematical value that the computer can represent ?(x)**[6]

    • The conversion from number or BigInt x to mathematical value is expressed as **mathematical value of x**, or ?(x)

  8. Let **return value symbol s = ”**, which is the symbol definition initial value

  9. If **x < 0**, then **s = '-'** and x = -x

  10. If **x ≥ 10^21**, then return value m = x corresponding to scientific notation represented by >String

  11. If **x < 10^21**, then

a. Let **n = `an integer`**, where **`n / 10^f - x`** is as close as possible to **`0`**, if there are two such * *n**, choose **larger n**

b. If **n = `integer0`**, then **m = `"0"`**, otherwise, **m = a string consisting of numbers in the `decimal` representation of `n` Values (in order, without leading zeros) `**

c. If **exponent f ≠ `0`**, then **k = `m.length`**

* If **k ≤ f**, then
    * **z = `string`** consisting of `f + 1-k` occurrences of code unit `0x0030(DIGIT ZERO)`
    * **m = `z + m`**
    * **k = `f + 1`**
* Let **a = the first `k-f` code unit of `m`**
* Let **b = the other `f` coding units of `m`**
* Set **m = `a + "." + b`**
  1. Returns a string composed of s + m

Can’t understand? Then choose somewhere you understand to read

Without further ado, let’s use (1.125).toFixed(2) = 1.13 as an example!

  • Initialx = 1.125, f = 2, s = ”according to the above specification

  • According to specification 7 we know that x = 1.125.toPrecision(53) = 1.125

  • According to the formula provided in specification 11.a: **n / 10^f - x ≈ 0** Substitute into the calculation: **n ≈ 112.5**:

    • At this time, the integer closest to n has two values of 112 and **113**, take the largest 113 according to the standard

    • After following the specifications of 11.c, we get m = 1.13

  • Finally returns s + m= 1.13

Not yet, let’s try another chestnut of (-1.105).toFixed(2) = -1.10!

  • According to the above specification initialx = 1.105, f = 2, s = ‘-‘

  • According to specification 7 we know that x = (-1.105).toPrecision(53) = 1.10499…

  • According to the formula provided in specification 11.a: **n / 10^f - x ≈ 0** Substitute into the calculation: **n ≈ 110.4... **:

    • At this time, the closest integer to n is only one with the value **110* *(Because only when the decimal point is 5, there will be two cases of rounding up/down)

    • After following the specifications of 11.c, we get m = 1.10

  • The final return is s + m= -1.13

Although we know the reasons for precision loss and the logic of toFixed rounding, in fact, when doing calculations, we still want to proceed according to the actual values we see. Calculated or rounded instead of the underlying converted value.

Use third-party libraries

Check it out if necessary:

  • **math.js**[7]

  • **big.js**[8]

  • **bignumber.js**[9]

  • **decimal.js**[10]

Expansion of ideas

Floating point calculation

Floating point numbers may lose precision after underlying conversion in JavaScript, but integers within the safe range will not be lost, so we You can first convert Floating-point numbers into Integers for calculation, and then convert the calculation results into floating-point numbers.

Take 0.1 + 0.2 = 0.30000000000000004 as an example, as follows:

  • Original formula:0.1 + 0.2 = x

  • Expand 10 times: 0.1 * 10 + 0.2 * 10 = 10 * x

  • Variation: 10 * x = 3

  • Result: x = 0.3

[Note] This is not a non-optimal solution, because not all floating point numbers can be converted to integers, for example

The key is that (44.8976).toPrecision(53) has an error in the four digits after the decimal point of the result after taking the precision and the actual value, resulting in an incorrect value in the final product.

Exceeds the maximum value

For values beyond the safe range produced by the backend return or frontend calculation mentioned earlier, we can use BigInt To handle, this is a new primitive value type that provides a way to represent integers greater than 2^53 - 1.

[**Note**] The BigInt here cannot be used to handle values returned by the backend that exceed the safe range (such as id). Because the precision loss has already occurred before we need to convert these values to BigInt, it is meaningless to perform the conversion. At this time, the best way is to use the interface as String< /strong> returns the corresponding value

Keep specified decimal places

Since the rounding method of Number.prototype.toFixed() is not what we need, we can directly rewrite it to match, for example:

Number.prototype.toFixed=function (d) {
    var s=this + "";
    if(!d)d=0;
    if(s.indexOf(".")==-1)s + =".";
    s + =new Array(d + 1).join("0");
    if(new RegExp("^(-|\ + )?(\d + (\.\d{0," + (d + 1) + "})?)\d*$") .test(s)){
        var s="0" + RegExp.$2,pm=RegExp.$1,a=RegExp.$3.length,b=true;
        if(a==d + 2){
            a=s.match(/\d/g);
            if(parseInt(a[a.length-1])>4){
                for(var i=a.length-2;i>=0;i--){
                    a[i]=parseInt(a[i]) + 1;
                    if(a[i]==10){
                        a[i]=0;
                        b=i!=1;
                    }else break;
                }
            }
            s=a.join("").replace(new RegExp("(\d + )(\d{" + d + "})\d$"),"$1.$2");
 
        }if(b)s=s.substr(1);
        return (pm + s).replace(/\.$/,"");
    }return this + "";
 
}

Welcome to follow the public account of the same name “Panda's Cat“. Articles will be updated simultaneously, and you can also quickly join the front-end communication group!

The above is the entire content of this article. Since it involves some Networking related content, it may be difficult to understand, but it is not that difficult to understand after crossing this hurdle.

I hope this article is helpful! ! !

syntaxbug.com © 2021 All Rights Reserved.