JS 小数精度引发的血案

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

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

补充于 Saturday, September 19, 2020 18:32:20 :
最近发现一个特别好用的 JS API Intl,它的其中一个构造函数是 NumberFormat,非常方便给数字🔢,添加一些东西,例如:给数字添加千分分隔符,添加百分号。

使用示例🌰:

new Intl.NumberFormat("zh-Hans-CN", { style: 'percent', maximumFractionDigits: 20 }).format(0.123456789);
//"12.3456789%"

// 简单的写个函数:
function decimal2Percentage (decimal, maximumFractionDigits = 20) {
    return new Intl.NumberFormat("zh-Hans-CN", { style: 'percent', maximumFractionDigits }).format(decimal);
};
  1. zh-Hans-CN 表示数字已哪种语言本地化
  2. { style: 'percent', maximumFractionDigits: 20 } 数字百分号表示,小数后面保留最大位数为 20maximumFractionDigits 的取值范围为:0~20,很合理,一般超过 20 位没啥意义了。如果不写 maximumFractionDigits 会从 minimumfractiondigits 和 0 中取最大的那一个minimumfractiondigits 的默认值为 0,所以不写 maximumFractionDigits 百分数默认只展示整数位。

参考链接🔗:Intl.NumberFormat

三、大数精度问题

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353