美文网首页基础前端
JS 小数精度引发的血案

JS 小数精度引发的血案

作者: CondorHero | 来源:发表于2020-08-19 01:59 被阅读0次

    前言: 在找工作面试的时候,相信你偶尔会遇到一道经典的面试题,即:

    0.1 + 0.1 是否等于 0.3
    

    都不用思考,你就能马上说出答案,肯定不等于啊,如果记忆里好点你还能记住运算结果,0.1 + 0.2 = 0.30000000000000004。造成这种结果的原因是,小数在转成二进制的时候,采用的办法是小数乘积正取正,这个方法没有任何问题,问题是有的小数是无法整乘,即使用 小数乘积正取正 方法,把十进制的小数换算成二进制,结果会出现无限不循环数。而且 JS 采用双精度存值,只保留 64 位,这就导致了约数的出现,有约数那肯定就不准确了,由此出现小数精度的问题。

    一、浮点数的精度运算

    我看网上给出大约三种解决方案:

    • 使用 toFixed,parseFoloat 原生 JS 方法,我们不能使用这个方法,因为本来计算就不准确,又使用 toFixed 给约数下,不准确加上更不准确 😩。看例子:
    parseFloat(0.9);    //0.9
    parseFloat(9999999999999999)    //10000000000000000
    parseInt("9999999999999999");    //10000000000000000
    parseFloat(9.999999999999999);   //10
    
    toFixed不会四舍五入:
    var num = 1.835;
    num.toFixed(2); //"1.83"
    
    toFixed 取值不准确:
    var num = 0.999999999999998898;
    num.toFixed(10); //"1.0000000000"
    
    • 将浮点数转为整数运算,再对结果做除法,例如 (0.1 * 10 + 0.2 * 10) / 10 === 0.3,但是 8800.03 * 100 === 880003.0000000001 转换结果又不对,所以小数运算还是有问题的。

    • 比较推荐的是这三个库, bignumber.jsdecimal.js,以及 big.js 来解决精确度的问题。三者的区别为:

    1. big.js:极简主义;易于使用; 小数点后指定的精度;精度仅适用于除法;4 种舍入模式。适用于取精度简单的运算

    2. bignumber.js:以 2-64 为基数; 配置选项;NaN; 无限; 小数点后指定的精度;精度仅适用于除法;随机数;基本前缀;9种舍入模式;模模式;模幂。多种精度任你选择,更加适用于金融类

    3. decimal.js:二进制,八进制和十六进制;配置选项;NaN; 无限; 非整数幂,exp,ln,log;三角函数 以有效数字指定的精度;始终应用精度;随机数;序列化和反序列化;基本前缀;9种舍入模式;模模式;二进制指数表示法。适合做程序员计算器。

    摘自:big.js,bignumber.js 和 decimal.js 有什么区别?

    按功能范围分 decimal.js > bignumber.js > big.js

    比较好奇 big.js 怎么用 JS 实现计算的,看了源码,一堆x,t, b 等变量,结果源码没看懂。

    二、浮点数的百分比表示

    这就是我遇到的血案,后端传回来的毛利率为小数,前端自己处理成百分比的形式,但是因为某些小数在乘 100 的时候出现精度的问题,感觉无解似的。这时候我的解决思路就是把数字按数组来处理,并写了个函数,函数可以让输入数自动扩大一百倍,然后吐出来的数只需要手动加个百分号就行了。

    源码如下:

    const decimal2Percentage = (decimal) => {
        // 判断是整数还是小数
        const isInteger = String(decimal).split(".")[1];
        if (isInteger) {
            // 小数逻辑
    
            // 获取小数的整数部分
            const firstNumber = String(decimal).split(".")[0];
            // 获取小数的小数部分
            decimal = String(decimal).split(".")[1];
            // 小数位数少于两位补零0.1 ==> 10
            decimal = decimal.length < 2 ? decimal.padEnd(2, 0) : decimal;
    
            const percentage = decimal.split("");
    
            // 小数点后移两位,达到乘100的效果
            decimal.length > 2 && percentage.splice(2, 0, ".");
            if (firstNumber > 0) {
                // 小数的整数大于零需要保留
                return `${firstNumber}${percentage.join("")}`;
            } else {
                // 裁掉多余的零
                let numberIndex = percentage.findIndex(number => number !== "0");
                percentage.splice(0, numberIndex);
                percentage[0] === "." && percentage.unshift("0");
                return `${percentage.join("")}`;
            };
        } else {
            // 整数逻辑,直接添加两个零,零除外
            const newDecimal = String(decimal) !== "0" ? String(decimal).split(".").concat([ 0, 0 ]) : [ "0" ];
            return newDecimal.join("");
        };
    }
    console.log(decimal2Percentage(0.0001)); //0.01
    console.log(decimal2Percentage(0.1)); //10
    console.log(decimal2Percentage(0)); //0
    console.log(decimal2Percentage(90)); //9000
    console.log(decimal2Percentage(12)); //1200
    console.log(decimal2Percentage(0.102023231)); //10.2023231
    

    三、大数精度问题

    [Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER] 表示 JS 数的表示范围,当超出这个范围怎么办,聪明的你肯定想到了,没错使用字符串来表示, antd 的 inputNumber 输入框就是先输入数字,数字不能表示时切换字符串来表示。这个属于位数精度的问题。

    https://www.npmjs.com/package/fraction.js

    四、你没注意到的 Math.round 方法

    还有另外一个与 JavaScript 计算相关的问题,即 Math.round(x),它虽然不会产生精度问题,但是它有一点小陷阱容易忽略。下面是它的舍入的策略:

    如果小数部分大于 0.5,则舍入到下一个绝对值更大的整数。
    如果小数部分小于 0.5,则舍入到下一个绝对值更小的整数。
    如果小数部分等于 0.5,则舍入到下一个正无穷方向上的整数。
    所以,对 Math.round(-1.5),其结果为 -1,这可能不是我们想要的结果,一定要注意这一点

    相关文章

      网友评论

        本文标题:JS 小数精度引发的血案

        本文链接:https://www.haomeiwen.com/subject/mknadktx.html