On the Loss of Computing Accuracy in JavaScript

Keywords: Javascript Programming Java Firefox

Abstract:
Because computers use binary to store and process numbers, they can not accurately represent floating-point numbers, and there is no corresponding encapsulation class in JavaScript to process floating-point operations. Direct calculation will lead to loss of operational accuracy.  
In order to avoid the difference of accuracy, the general method of processing the difference of accuracy in most programming languages is to upgrade the number needed to be calculated (multiplied by 10 n-power) to an integer that can be accurately recognized by the computer, and then degrade it (divided by 10 n-power) after the calculation.
Key words:
Four rounds of calculation accuracy will result in loss of operation accuracy.
1. doubts
We know that almost every programming language provides classes suitable for currency computing. For example, C # provides decimal, Java provides BigDecimal, JavaScript provides Number...  
Because decimal and BigDecimal have been used well before and have not produced any accuracy problems, there has been no doubt about the Number type of JavaScript, thinking that it can be calculated directly using Number type. But direct use is problematic.
Let's first look at the rounded code as follows:
  1. alert(Number(0.009).toFixed(2));  
  2. alert(Number(162.295).toFixed(2));  

According to the normal results, 0.01 and 162.30 should be ejected respectively. But the actual test results are different in different browsers:
0.00 and 162.30 are obtained under ie6, 7 and 8, and the first number is not intercepted correctly.
In firefox, 0.01 and 162.29 are obtained, and the second number is not intercepted correctly.
Under opera, 0.01 and 162.29 are obtained, and the second number is not intercepted correctly.
Let's look at the code for four operations:
  1. alert(1/3);//Ejection: 0.33333333333333  
  2. alert(0.1 + 0.2);//Ejection: 0.30000000000004  
  3. alert(-0.09 - 0.01);//Ejection: - 0.099999999999999999  
  4. alert(0.012345 * 0.000001);//Ejection: 1.23449999999999e-8  
  5. alert(0.000001 / 0.0001);//Ejection: 0.009999999999999998  

According to the normal results, except for the first line (because it can not be exhausted), other should get accurate results, but from the pop-up results we find that it is not the right result we want. Is it because it has not been converted to Number type? Let's convert to Number and then calculate:
  1. alert(Number(1)/Number(3));//Pop-up: 0.333333333333  
  2. alert(Number(0.1) + Number(0.2));//Ejection: 0.30000000000004  
  3. alert(Number(-0.09) – Number(0.01));//Ejection: -0.09999999999999999  
  4. alert(Number(0.012345) * Number(0.000001));//Ejection: 1.23449999999999e-8  
  5. alert(Number(0.000001) / Number(0.0001));//Ejection: 0.009999999999999998  

As a result, it seems that javascript recognizes numbers as number by default. To verify this, we use the typeof pop-up type to see:
  1. alert(typeof(1));//Pop-up: number  
  2. alert(typeof(1/3));//Pop-up: number  
  3. alert(typeof(-0.09999999));//Pop-up: number  

2. reasons
Why does this loss of accuracy occur? Is it a bug in the javascript language?  
We recall the principle of computer that we learned in college. The computer performs binary arithmetic. When decimal numbers can not be converted to binary numbers accurately, this precision error is unavoidable.  
Reviewing the relevant data of javascript, we know that the numbers in JavaScript are expressed in floating point numbers, and stipulate that the double precision floating point numbers in the standard of IE 754 should be used.
IEEE 754 specifies two basic floating-point formats: single-precision and double-precision.  
IEEE single-precision format has 24-bit valid digital accuracy (including symbols) and takes up 32 bits in total.  
The IEEE dual-precision format has 53-bit significant digital accuracy (including symbols) and occupies 64 bits in total.  
This structure is a scientific representation, which is expressed by symbols (positive or negative), exponents and tails. The base number is determined to be 2. That is to say, a floating point number is expressed as the exponential power of the tail multiplied by 2 plus symbols. Let's look at the specifications below.
  Symbol bit Index Position Fractional part Exponential offset
Single-precision floating-point 1 place (31) 8 bits (30-23) 23 bits (22-00) 127
Double 1 place (63) 11 bits (62-52) 52 bits (51-00) 1023

We use single-precision floating-point numbers to illustrate:
The index is 8 bits and the expressive range is 0 to 255.
The corresponding actual index is - 127 to + 128.
In particular, the data - 127 and + 128 are reserved for multiple purposes in IEEE.
The number represented by -127 is 0.
The combination of 128 and other digits represents a variety of meanings, the most typical of which is the NAN state.  
Knowing all this, let's simulate the calculation of the computer's process conversion, and find a simple 0.1 + 0.2 to deduce it:
  1. Decimal 0.1
  2. => Binary 0.00011001100110011... (Cycle 0011)
  3. => The tail number is 1.1001100110011001100. 1100 (total 52 bits, except 1 on the left of decimal point), exponent is - 4 (binary shift code is 00000010), symbol bit is 0.
  4. => Computer storage: 0 00000000100 10011001100110011... 11001)
  5. => Because the tail number is up to 52 bits, the actual storage value is 0.0001100110011001100110011001100110011001100110011001100111001.
  6. And decimal 0.2
  7. => Binary 0.0011001100110011... (Cycle 0011)
  8. => The tail number is 1.1001100110011001100. 1100 (total 52 bits, except 1 on the left of decimal point), exponent - 3 (binary shift code 00000011), symbol bit 0.
  9. => Storage: 0 00000000011 10011001100110011... 11001)
  10. The actual storage value is 0.0011001100110011001100110011001100110011001100111011001100110011001100111011001100110011101100110011001110011 because the tail number is up to 52 bits.
  11. So the two add up to the same thing: \\\\\\
  12.  0.00011001100110011001100110011001100110011001100110011001    
  13. +  0.00110011001100110011001100110011001100110011001100110011  
  14.  =  0.01001100110011001100110011001100110011001100110011001100    
  15. Converted to 10-digit, the result is: 0.30000000000004.

From the above deduction process, we know that this kind of error is unavoidable. The reason why there is no difference in accuracy between c_decimal and Java BigDecimal is that the difference in accuracy is shielded by the corresponding processing within them. javascript is a weak scripting language, and it does not deal with the calculation accuracy in itself, which requires us to do something else. The method has been dealt with.
3. Solutions
3.1 Upgrade and downgrade
As we have already known above, the reason for the difference in accuracy in javascript is that computers can not accurately represent floating-point numbers, even they can not accurately themselves, so they can not get accurate results. So how to make the computer know exactly the number to be calculated?
We know that decimal integers and binaries can be accurately converted to each other, so we upgrade floating-point numbers (multiplied by 10 n-power) to integers that can be accurately recognized by the computer to calculate, and then downgrade (divided by 10 n-power) after the calculation, can not we get accurate results? Okay, that's it!
We know that Math.pow(10,scale) can get the scale power of 10, so multiply the floating point number directly by Math.pow(10,scale)? That's what I thought at first, but then I found that the actual results of some numerical operations were not consistent with our guesses. Let's look at this simple operation:
  1. alert(512.06*100);  

Normally, 51206 should be returned, but the actual result is 51205.999999999. Strange? It's not surprising, because floating-point numbers can't participate in multiplication precisely, even if it's very special (just multiplied by 10 scales to upgrade). So we can't upgrade directly by multiplying the scale power by 10. Let's move the decimal point by ourselves.
How to move decimal points is sure that everyone has their own tricks. Here are some methods I wrote:
  1. /** 
  2.  * Left Completed String 
  3.  *  
  4.  * @param nSize 
  5.  *            Length to be repaired 
  6.  * @param ch 
  7.  *            Characters to be filled 
  8.  * @return 
  9.  */  
  10. String.prototype.padLeft = function(nSize, ch)  
  11. {  
  12.     var len = 0;  
  13.     var s = this ? this : "";  
  14.     ch = ch ? ch : '0';//Default complement 0  
  15.   
  16.     len = s.length;  
  17.     while (len < nSize)  
  18.     {  
  19.         s = ch + s;  
  20.         len++;  
  21.     }  
  22.     return s;  
  23. }  
  24.   
  25. /** 
  26.  * Right Completion String 
  27.  *  
  28.  * @param nSize 
  29.  *            Length to be repaired 
  30.  * @param ch 
  31.  *            Characters to be filled 
  32.  * @return 
  33.  */  
  34. String.prototype.padRight = function(nSize, ch)  
  35. {  
  36.     var len = 0;  
  37.     var s = this ? this : "";  
  38.     ch = ch ? ch : '0';//Default complement 0  
  39.   
  40.     len = s.length;  
  41.     while (len < nSize)  
  42.     {  
  43.         s = s + ch;  
  44.         len++;  
  45.     }  
  46.     return s;  
  47. }  
  48. /** 
  49.  * Left-shifted decimal point position (used in mathematical calculations, equivalent to dividing by Math.pow(10,scale)) 
  50.  *  
  51.  * @param scale 
  52.  *            Scales to be shifted 
  53.  * @return 
  54.  */  
  55. String.prototype.movePointLeft = function(scale)  
  56. {  
  57.     var s, s1, s2, ch, ps, sign;  
  58.     ch = '.';  
  59.     sign = '';  
  60.     s = this ? this : "";  
  61.   
  62.     if (scale <= 0) return s;  
  63.     ps = s.split('.');  
  64.     s1 = ps[0] ? ps[0] : "";  
  65.     s2 = ps[1] ? ps[1] : "";  
  66.     if (s1.slice(0, 1) == '-')  
  67.     {  
  68.         s1 = s1.slice(1);  
  69.         sign = '-';  
  70.     }  
  71.     if (s1.length <= scale)  
  72.     {  
  73.         ch = "0.";  
  74.         s1 = s1.padLeft(scale);  
  75.     }  
  76.     return sign + s1.slice(0, -scale) + ch + s1.slice(-scale) + s2;  
  77. }  
  78. /** 
  79.  * Move the decimal point to the right (for mathematical calculation, multiplied by Math.pow(10,scale)) 
  80.  *  
  81.  * @param scale 
  82.  *            Scales to be shifted 
  83.  * @return 
  84.  */  
  85. String.prototype.movePointRight = function(scale)  
  86. {  
  87.     var s, s1, s2, ch, ps;  
  88.     ch = '.';  
  89.     s = this ? this : "";  
  90.   
  91.     if (scale <= 0) return s;  
  92.     ps = s.split('.');  
  93.     s1 = ps[0] ? ps[0] : "";  
  94.     s2 = ps[1] ? ps[1] : "";  
  95.     if (s2.length <= scale)  
  96.     {  
  97.         ch = '';  
  98.         s2 = s2.padRight(scale);  
  99.     }  
  100.     return s1 + s2.slice(0, scale) + ch + s2.slice(scale, s2.length);  
  101. }  
  102. /** 
  103.  * Move the decimal point position (for mathematical calculation, equivalent to (multiply/divide) Math.pow(10,scale) 
  104.  *  
  105.  * @param scale 
  106.  *            Scales to be shifted (positive numbers move to the right; negative numbers move to the left; 0 returns the original value) 
  107.  * @return 
  108.  */  
  109. String.prototype.movePoint = function(scale)  
  110. {  
  111.     if (scale >= 0)  
  112.         return this.movePointRight(scale);  
  113.     else  
  114.         return this.movePointLeft(-scale);  
  115. }  

In this way, we can upgrade and downgrade to strings and call the custom method movePoint of String object. Multiply it by 10 scales, we pass positive integer scale, divide it by 10 scales, and we pass negative integer-scale.
Let's take a look at the code we upgraded 512.06 before. The call code using custom methods becomes like this:
  1. alert(512.06.toString().movePoint(2)); //Ejection: 51206  

So move the decimal point directly without fear that it will not listen to a long string of numbers (*^^*). Of course, the result of the movePoint method is a string, and it's also convenient to convert to Number type (no more nonsense).
3.2 Rounding
Well, with the foundation of upgrading and downgrading, let's look at rounding method. Because different browsers have different support for Number's toFixed method, we need to override the browser's default implementation in our own way.  
There is a simple way for us to determine for ourselves whether the last digit of the data to be intercepted is greater than or equal to 5, and then rounds or rounds. We know that the Math.ceil method takes the smallest integer greater than or equal to the specified number, and the Math.floor method takes the largest integer smaller than or equal to the specified number. So we can use these two methods to do rounding processing. First, we upgrade the rounded number to the rounded number scale (multiplied by the scale of 10), then we downgrade the rounded number scale after the ceil or floor rounding.( Divide by 10 scales.
The code is as follows:
  1. Number.prototype.toFixed = function(scale)  
  2. {  
  3.     var s, s1, s2, start;  
  4.   
  5.     s1 = this + "";  
  6.     start = s1.indexOf(".");  
  7.     s = s1.movePoint(scale);  
  8.   
  9.     if (start >= 0)  
  10.     {  
  11.         s2 = Number(s1.substr(start + scale + 1, 1));  
  12.         if (s2 >= 5 && this >= 0 || s2 < 5 && this < 0)  
  13.         {  
  14.             s = Math.ceil(s);  
  15.         }  
  16.         else  
  17.         {  
  18.             s = Math.floor(s);  
  19.         }  
  20.     }  
  21.   
  22.     return s.toString().movePoint(-scale);  
  23. }  

After overriding the toFixed method of Number type, let's do the following.
  1. alert(Number(0.009).toFixed(2));//Pop up 0.01  
  2. alert(Number(162.295).toFixed(2));//Pop up 162.30  

Correct results can be obtained by verification under ie6, 7, 8, firefox and Opera respectively.  
Another way is to use regular expressions to round up the data found on the Internet. The code is as follows:
  1. Number.prototype.toFixed = function(scale)  
  2. {  
  3.     var s = this + "";  
  4.     if (!scale) scale = 0;  
  5.     if (s.indexOf(".") == -1) s += ".";  
  6.     s += new Array(scale + 1).join("0");  
  7.     if (new RegExp("^(-|\\+)?(\\d+(\\.\\d{0," + (scale + 1) + "})?)\\d*$").test(s))  
  8.     {  
  9.         var s = "0" + RegExp.$2, pm = RegExp.$1, a = RegExp.$3.length, b = true;  
  10.         if (a == scale + 2)  
  11.         {  
  12.             a = s.match(/\d/g);  
  13.             if (parseInt(a[a.length - 1]) > 4)  
  14.             {  
  15.                 for (var i = a.length - 2; i >= 0; i--)  
  16.                 {  
  17.                     a[i] = parseInt(a[i]) + 1;  
  18.                     if (a[i] == 10)  
  19.                     {  
  20.                         a[i] = 0;  
  21.                         b = i != 1;  
  22.                     }  
  23.                     else  
  24.                         break;  
  25.                 }  
  26.             }  
  27.             s = a.join("").replace(new RegExp("(\\d+)(\\d{" + scale + "})\\d$"), "$1.$2");  
  28.         }  
  29.         if (b) s = s.substr(1);  
  30.         return (pm + s).replace(/\.$/, "");  
  31.     }  
  32.     return this + "";  
  33. }  

It has been proved that both methods can rounding accurately, so which method is better? To get the truth out of practice, let's write a simple way to verify the performance of the two ways:
  1. function testRound()  
  2. {  
  3.     var dt, dtBegin, dtEnd, i;  
  4.     dtBegin = new Date();  
  5.     for (i=0; i<100000; i++)  
  6.     {  
  7.         dt = new Date();  
  8.         Number("0." + dt.getMilliseconds()).toFixed(2);  
  9.     }  
  10.     dtEnd = new Date();  
  11.     alert(dtEnd.getTime()-dtBegin.getTime());  
  12. }  

To avoid caching problems with rounding the same number, we rounded the current number of milliseconds. It has been proved that in the case of 100,000 operations on the same machine, the average time consumed by the movePoint method is 2,500 milliseconds, and the average time consumed by the regular expression method is 4,000 milliseconds.
3.3 Add, subtract, multiply and divide
Rounding a given number can be achieved by floor/ceil or regular expression. Then can the four-rule operation be upgraded to integers that can be accurately recognized by the computer and then degraded? The answer is yes. Let's take a look at additions first.
  1. Number.prototype.add = function(arg)  
  2. {  
  3.     var n, n1, n2, s, s1, s2, ps;  
  4.   
  5.     s1 = this.toString();  
  6.     ps = s1.split('.');  
  7.     n1 = ps[1] ? ps[1].length : 0;  
  8.   
  9.     s2 = arg.toString();  
  10.     ps = s2.split('.');  
  11.     n2 = ps[1] ? ps[1].length : 0;  
  12.   
  13.     n = n1 > n2 ? n1 : n2;  
  14.     s = Number(s1.movePoint(n)) + Number(s2.movePoint(n));  
  15.     s = s.toString().movePoint(-n);  
  16.     return Number(s);  
  17. }  

At this time, the addition before execution is executed.
Alert (Number (0.1). add (0.2);//eject 0.3
Then we can calculate the exact results.  
Similarly, subtraction can be written:
  1. Number.prototype.sub = function(arg)  
  2. {  
  3.     var n, n1, n2, s, s1, s2, ps;  
  4.   
  5.     s1 = this.toString();  
  6.     ps = s1.split('.');  
  7.     n1 = ps[1] ? ps[1].length : 0;  
  8.   
  9.     s2 = arg.toString();  
  10.     ps = s2.split('.');  
  11.     n2 = ps[1] ? ps[1].length : 0;  
  12.   
  13.     n = n1 > n2 ? n1 : n2;  
  14.     s = Number(s1.movePoint(n)) - Number(s2.movePoint(n));  
  15.     s = s.toString().movePoint(-n);  
  16.     return Number(s);  
  17. }  

Similarly, multiplication can be written:
  1. Number.prototype.mul = function(arg)  
  2. {  
  3.     var n, n1, n2, s, s1, s2, ps;  
  4.   
  5.     s1 = this.toString();  
  6.     ps = s1.split('.');  
  7.     n1 = ps[1] ? ps[1].length : 0;  
  8.   
  9.     s2 = arg.toString();  
  10.     ps = s2.split('.');  
  11.     n2 = ps[1] ? ps[1].length : 0;  
  12.   
  13.     n = n1 + n2;  
  14.     s = Number(s1.replace('.''')) * Number(s2.replace('.'''));  
  15.     s = s.toString().movePoint(-n);  
  16.     return Number(s);  
  17. }  

Similarly, divisions can be written:
  1. Number.prototype.div = function(arg)  
  2. {  
  3.     var n, n1, n2, s, s1, s2, ps;  
  4.   
  5.     s1 = this.toString();  
  6.     ps = s1.split('.');  
  7.     n1 = ps[1] ? ps[1].length : 0;  
  8.   
  9.     s2 = arg.toString();  
  10.     ps = s2.split('.');  
  11.     n2 = ps[1] ? ps[1].length : 0;  
  12.   
  13.     n = n1 - n2;  
  14.     s = Number(s1.replace('.''')) / Number(s2.replace('.'''));  
  15.     s = s.toString().movePoint(-n);  
  16.     return Number(s);  
  17. }  

Important hint: Because division can not be precise to several decimal places, when the calculation is completed, it should be rounded appropriately according to the need to avoid the difference in accuracy.  
4. conclusion
Because computers use binary to store and process numbers and cannot accurately represent floating-point numbers, this difference in accuracy occurs in almost all programming languages (e.g. C/C++/C#,Java). To be exact, any programming language that uses the floating-point format of IEEE 754 to store floating-point types (float 32,double 64) has this problem! C and Java avoid this precision difference because they provide encapsulation classes decimal and BigDecimal for corresponding processing.
In order to avoid the difference in accuracy, the digital upgrade (multiplied by 10 n-power) is transformed into integers that can be accurately recognized by the computer, and then degraded (divided by 10 n-power) after the calculation is completed. This is a common method for most programming languages to deal with the difference in accuracy.

Posted by freedomsam on Wed, 27 Mar 2019 18:45:29 -0700