Recently, when I was working on a project, the calculation of commodity prices was often involved, and the accuracy of calculation often appeared. At the beginning, I solved the problem with toFixed without thinking about it. Later, slowly, more and more problems, even toFixed appeared (sad), later through the search of the network of various blogs and forums, sorted out a summary.

Problem discovery

Summed up, there are two kinds of problems

The precision problem after floating-point operation

Occasionally precision problems occur when calculating the addition, subtraction, multiplication and division of commodity prices. Some common examples are as follows:

/ / add = = = = = = = = = = = = = = = = = = = = =
0.1 + 0.2 = 0.30000000000000004
0.7 + 0.1 = 0.7999999999999999
0.2 + 0.4 = 0.6000000000000001

/ / subtraction = = = = = = = = = = = = = = = = = = = = =
1.5 - 1.2 = 0.30000000000000004
0.3 - 0.2 = 0.09999999999999998
 
/ / the multiplication = = = = = = = = = = = = = = = = = = = = =
19.9 * 100 = 1989.9999999999998
0.8 * 3 = 2.4000000000000004
35.41 * 100 = 3540.9999999999995

/ / division = = = = = = = = = = = = = = = = = = = = =
0.3 / 0.1 = 2.9999999999999996
0.69 / 10 = 0.06899999999999999
Copy the code

ToFixed weird problem

I initially used toFixed(2) to solve the accuracy problem after floating point operations, because the definition is clearly written in W3school and rookie tutorials: toFixed() can round the Number to a specified Number of decimal places.

But in Chrome, the results are less than satisfactory:

1.35.toFixed(1) / / 1.4 is correct
1.335.toFixed(2) / / 1.33 error
1.3335.toFixed(3) / / 1.333 error
1.33335.toFixed(4) / / 1.3334 is correct
1.333335.toFixed(5)  / / 1.33333 error
1.3333335.toFixed(6) / / 1.333333 error
Copy the code

Using IETester under IE, the results are correct.

Why did it happen?

Let’s see why 0.1+0.2 equals 0.30000000000000004 instead of 0.3. First of all, to understand why this problem arises, let’s go back to the principles of computer composition of the complex zAO that we learned in college. Although it has all been returned to the university teachers, but it doesn’t matter, we still have Baidu.

Storage of floating point numbers

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).

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:

  • Sign bit S: The first bit is the sign bit (sign). 0 represents a positive number and 1 represents a negative number

  • Exponent bit E: The middle 11 bits store the exponent, which is used to represent the exponent

  • Mantissa: The last 52 digits are mantissa. The excess digits are automatically zeros

Floating-point operation

So what happens when JavaScript evaluates 0.1+0.2?

First, decimal 0.1 and 0.2 are converted to binary, but since floating point numbers are infinite in binary:

0.1 -> 0.0001 1001 1001 1001...(1100Loop)0.2 -> 0.0011 0011 0011 0011...(0011Loop)Copy the code

IEEE 754 supports a maximum of 53 binary bits for the decimal part of a 64-bit double precision floating-point number, so when the two are added together, the binary is:

0.0100110011001100110011001100110011001100110011001100 
Copy the code

A binary number truncated by the limitation of a floating-point decimal place, converted to decimal, is 0.30000000000000004. So there are errors when you do the arithmetic.

The solution

In view of the above two problems, online search a wave of solutions, are basically similar, respectively.

Solve toFixed

For the compatibility problem of toFixed, we can rewrite toFix to solve it. The code is as follows:

// toFixed compatible method
Number.prototype.toFixed = function(len){
    if(len>20 || len<0) {throw new RangeError('toFixed() digits argument must be between 0 and 20');
    }
    //.123 becomes 0.123
    var number = Number(this);
    if (isNaN(number) || number >= Math.pow(10.21)) {
        return number.toString();
    }
    if (typeof (len) == 'undefined' || len == 0) {
        return (Math.round(number)).toString();
    }
    var result = number.toString(),
        numberArr = result.split('. ');

    if(numberArr.length<2) {// Integer case
        return padNum(result);
    }
    var intNum = numberArr[0].// The integer part
        deciNum = numberArr[1].// The decimal part
        lastNum = deciNum.substr(len, 1);// The last number
    
    if(deciNum.length == len){
        // The length to intercept is equal to the current length
        return result;
    }
    if(deciNum.length < len){
        // The length needed to intercept is greater than the current length.
        return padNum(result)
    }
    // The length that needs to be truncated is less than the current length
    result = intNum + '. ' + deciNum.substr(0, len);
    if(parseInt(lastNum, 10) > =5) {// If the last digit is greater than 5, carry it
        var times = Math.pow(10, len); // The need for magnification
        var changedInt = Number(result.replace('. '.' '));// Cut to an integer
        changedInt++;// Integer carry
        changedInt /= times;// The integer is converted to a decimal
        result = padNum(changedInt+' ');
    }
    return result;
    // Add 0 to the end of the number
    function padNum(num){
        var dotPos = num.indexOf('. ');
        if(dotPos === - 1) {// Integer case
            num += '. ';
            for(var i = 0; i<len; i++){ num +='0';
            }
            return num;
        } else {
            // The decimal case
            var need = len - (num.length - dotPos - 1);
            for(var j = 0; j<need; j++){ num +='0';
            }
            returnnum; }}}Copy the code

We decide whether we need to carry by checking whether the last digit is greater than or equal to 5. If we need to carry, we multiply the decimal number by a multiple to make it an integer, and then divide it by a multiple to make it a decimal number, so we don’t have to judge by a digit.

Solve floating-point arithmetic precision

Now that we’ve discovered this problem with floating-point numbers, we can’t just use two floating-point numbers, so what do we do?

We can upgrade the number we need to compute (multiplied by 10 to the NTH power) to an integer that the computer can recognize accurately, and then degrade it (divided by 10 to the NTH power) after the computation is complete, which is the way most languages deal with precision problems. Such as:

0.1 + 0.2= =0.3 //false
(0.1*10 + 0.2*10) /10= =0.3 //true
Copy the code

But is this a perfect solution? Careful readers may have spotted the problem in the above example:

35.41 * 100 = 3540.9999999999995
Copy the code

It seems that digital upgrades are not entirely reliable (sad).

IndexOf (‘.’) (toString, indexOf(‘.’));

 /*** Method *** add/Subtract /divide * Floatobj.add (0.1, 0.2) >> 0.3 * floatobj.multiply (19.9, 100) >> 1990 * */
var floatObj = function() {

    /* * check whether obj is an integer */
    function isInteger(obj) {
        return Math.floor(obj) === obj
    }

    /* * Converts a floating point number to an integer, returning integer and multiple. For example, 3.14 >> 314, the multiple is 100 * @param floatNum {number} decimal * @return {object} * {times:100, num: 314} */
    function toInteger(floatNum) {
        var ret = {times: 1.num: 0}
        if (isInteger(floatNum)) {
            ret.num = floatNum
            return ret
        }
        var strfi  = floatNum + ' '
        var dotPos = strfi.indexOf('. ')
        var len    = strfi.substr(dotPos+1).length
        var times  = Math.pow(10, len)
        var intNum = Number(floatNum.toString().replace('. '.' '))
        ret.times  = times
        ret.num    = intNum
        return ret
    }

    /* * Core method to achieve addition, subtraction, multiplication and division operations, to ensure no loss of precision * * * @param a {number} 1 * @param b {number} 2 * @param digits {number} Such as 2, which would hold to two decimal places * @ param op {string} operation type, there are subtracting (add/subtract/multiply/divide) * * /
    function operation(a, b, digits, op) {
        var o1 = toInteger(a)
        var o2 = toInteger(b)
        var n1 = o1.num
        var n2 = o2.num
        var t1 = o1.times
        var t2 = o2.times
        var max = t1 > t2 ? t1 : t2
        var result = null
        switch (op) {
            case 'add':
                if (t1 === t2) { // The two decimal places are the same
                    result = n1 + n2
                } else if (t1 > t2) { // O1 is greater than O2
                    result = n1 + n2 * (t1 / t2)
                } else { // O1 is less than O2
                    result = n1 * (t2 / t1) + n2
                }
                return result / max
            case 'subtract':
                if (t1 === t2) {
                    result = n1 - n2
                } else if (t1 > t2) {
                    result = n1 - n2 * (t1 / t2)
                } else {
                    result = n1 * (t2 / t1) - n2
                }
                return result / max
            case 'multiply':
                result = (n1 * n2) / (t1 * t2)
                return result
            case 'divide':
                result = (n1 / n2) * (t2 / t1)
                return result
        }
    }

    // Add, subtract, multiply, divide
    function add(a, b, digits) {
        return operation(a, b, digits, 'add')}function subtract(a, b, digits) {
        return operation(a, b, digits, 'subtract')}function multiply(a, b, digits) {
        return operation(a, b, digits, 'multiply')}function divide(a, b, digits) {
        return operation(a, b, digits, 'divide')}// exports
    return {
        add: add,
        subtract: subtract,
        multiply: multiply,
        divide: divide
    }
}();
Copy the code

If floatObj is too cumbersome to call, we can add the corresponding operator to Number. Prototype.

If you think it’s good, check out my Nuggets page. Please visit Xie xiaofei’s blog for more articles