博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
记一次 JavaScript 浮点型数字误差引发的问题
阅读量:4566 次
发布时间:2019-06-08

本文共 3324 字,大约阅读时间需要 11 分钟。

需求

车间的工人在生产出来产品后,需要完成初步的自检,并通过手机上报。在实际生产中,用户(工人)不方便进行数值的输入,因而表单中的一些项设计成 picker 模式以供选取数值。数值的取值范围,根据允许的误差范围生成。示例如下:

示例一// 误差0.01mm ~ 0.06mm// picker 展示的数值0.01, 0.02, 0.03, 0.04, 0.05, 0.06示例二// 误差15mm ~ 18mm// picker 展示的数值15, 16, 17, 18示例三// 误差1.05mm ~ 1.1mm// picker 展示的数值1.05, 1.06, 1.07, 1.08, 1.09, 1.1

由以上例子可以得知,取值范围的计算是根据误差范围的最小值的最小位数作为基数,从最小值(包含)逐步累加至最大值(包含)。

实现

首先,根据最小值获取小数位的个数。

function getDecimalPlace(value) {    // 先将 Number 转换为 String    value = value + '';    // 查找小数点的位置,加 1 是为了方便计算小数点的位数。    var floatIndex = value.indexOf('.') + 1;    // 返回的结果是小数位的个数    return floatIndex ? value.length - floatIndex : 0;}

用几个实际的数值,测试一下这个方法。

getDecimalPlace(1); //0getDecimalPlace('1.0'); //0getDecimalPlace('1.5'); //1getDecimalPlace('1.23'); //2

然后,根据小数位的个数计算累加的基数。

var min = 0.01;var max = 0.06;var decimal = getDecimalPlace(min);// 基数var radixValue = Math.pow(10, -decimal);

最后,根据误差范围和基数,循环生成取值范围。

var value = min;var range = [];for (; value <= max; value += radixValue) {    range.push(value);}console.log(range);//结果:[0.01,0.02,0.03,0.04,0.05]

从结果来看,好像哪里不对。没错,最大值 0.06 没有出现在取值范围中。

问题

JavaScript 采用了 IEEE-754 浮点数表示法。这是一种二进制表示法,二进制浮点数表示法并不能精确表示类似 0.1 这样简单的数字。

通过一个简单的例子来验证上面这段话。

var num1 = 0.2 - 0.1;var num2 = 0.3 - 0.2;console.log(num1 === num2); //falseconsole.log(num1 === 0.1); //trueconsole.log(num2 === 0.1); //false

由此可知,前面计算取值范围的方法中,遇到了类似的问题。

var max = 0.06;var value = 0.05;console.log(value + 0.01 === max); //false

因为从 0.05 + 0.01 的结果并不等于 0.06,所以循环只执行了 5 次(而非预期的 6 次)就结束了。

在尝试修复此问题之前,先把前面的代码封装一下。

function getRange(min, max) {    var decimal = getDecimalPlace(min);    var radixValue = Math.pow(10, -decimal);    var value = min;    var range = [];    for (; value <= max; value += radixValue) {        range.push(value);    }        return range;}

解决问题

最简单粗暴的办法,就是调整循环条件,在循环结束后再将最大值添加至数组。

function getRange(min, max) {    var decimal = getDecimalPlace(min);    var radixValue = Math.pow(10, -decimal);    var value = min;    var range = [];    for (; value < max; value += radixValue) {        range.push(value);    }    range.push(max);        return range;}

再次使用之前的数据测试:

getRange(0.01, 0.06);//结果:[0.01,0.02,0.03,0.04,0.05,0.06]

运行结果与预期一致,问题解决。

新的问题

然而,后续的测试中又出现了意外。

getRange(1.55, 1.65);// 结果:[1.55,1.56,1.57,1.58,1.59,1.6,1.61,1.62,1.6300000000000001,1.6400000000000001,1.65]

1.6300000000000001 这样的数值,显然不是我们期望得到的。出现此现象,与之前的问题原因一致。

方案一

将参与计算的数值,先转换为整型,再进行计算。

function getRange(min, max) {    var decimal = getDecimalPlace(min);    var radixValue = Math.pow(10, -decimal);    var multi = Math.pow(10, decimal)    var value = min * multi;    var range = [];    for (; value < max * multi; value += radixValue * multi) {        range.push(value / multi);    }    range.push(max);    return range;}

注意事项:

  • 向数组中添加数值时,需要再除以倍数,得到最终的数值。

方案二

使用 toFixed() 方法,对浮点型进行格式化。

function getRange(min, max) {    var decimal = getDecimalPlace(min);    var radixValue = Math.pow(10, -decimal);    var value = min;    var range = [];    for (; value < max || +value.toFixed(decimal) === max; value += radixValue) {        range.push(+value.toFixed(decimal));    }    return range;}

注意事项:

  • toFixed() 方法返回的值是 String 类型,因此需要再转换为 Number 类型。
  • 做了一点优化,调整循环条件后,移除了循环外 push() 最大值的语句。

最后

JavaScript 中浮点型精度的误差,是非常基础但是却又经常不被重视的问题。文中分享的方案,足以覆盖项目中的所有情况,但如果用在其它地方或项目中,在一些极端情况下可能会有问题。

参考资料

转载于:https://www.cnblogs.com/xiaoyucoding/p/11587663.html

你可能感兴趣的文章
jquery和dom元素相互转换
查看>>
放大的X--HDOJ-201307292012
查看>>
题目831-签到-nyoj-20140818
查看>>
百词斩-斩家秘籍
查看>>
php反射
查看>>
hdu 1018 Big Number 数学结论
查看>>
【MUI】百度地图定位功能
查看>>
bzoj3687 简单题
查看>>
STL容器简介
查看>>
HashMap遍历的两种方式,推荐使用entrySet()
查看>>
如何在Android开发中测试应用在真机上实验
查看>>
传奇代码研究 极品机率...
查看>>
mysql存储过程和定时调用
查看>>
(转)park1.0.0生态圈一览
查看>>
需要学习双拼了
查看>>
字符串和数组使用时该注意的一些地方
查看>>
我们的故事墙--一切为了可视化
查看>>
进程与线程的区别?-转
查看>>
php array_walk
查看>>
yolo train:CUDA Error: an illegal memory access was encountered darknet: cuda.c:36:check_error
查看>>