基础前端

JS 小数精度引发的血案

2020-08-19  本文已影响0人  CondorHero

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

0.1 + 0.1 是否等于 0.3

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

一、浮点数的精度运算

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

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"
  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,这可能不是我们想要的结果,一定要注意这一点

上一篇下一篇

猜你喜欢

热点阅读