需求

车间的工人在生产出来产品后,需要完成初步的自检,并通过手机上报。在实际生产中,用户(工人)不方便进行数值的输入,因而表单中的一些项设计成 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); //0
getDecimalPlace('1.0'); //0
getDecimalPlace('1.5'); //1
getDecimalPlace('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); //false
console.log(num1 === 0.1); //true
console.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 中浮点型精度的误差,是非常基础但是却又经常不被重视的问题。文中分享的方案,足以覆盖项目中的所有情况,但如果用在其它地方或项目中,在一些极端情况下可能会有问题。

参考资料

记一次 JavaScript 浮点型数字误差引发的问题的更多相关文章

  1. [WinForm]TextBox只能输入数字或者正浮点型数字

    关键代码: /// <summary> /// 只能输入数字[KeyPress事件] /// </summary> /// <param name="textB ...

  2. JavaScript将数字转换为大写金额

    用JavaScript将数字转换为大写金额,好了 0.0 To code! var digitUppercase = function(n) { var fraction = ['角', '分']; ...

  3. QDoubleSpinBox浮点型数字调节框

    样式: import sys from PyQt5.QtWidgets import QApplication, QWidget, QDoubleSpinBox class Demo(QWidget) ...

  4. JavaScript 格式化数字、金额、千分位、保留几位小数、舍入舍去…

    JavaScript 格式化数字.金额.千分位.保留几位小数.舍入舍去… 类库推荐 1. Numeral.js 一个用于格式化和操作数字的JavaScript库.数字可以被格式化为货币,百分比,时间, ...

  5. 前端 javascript 数据类型 数字

    1.数字(Number) JavaScript中不区分整数值和浮点数值,JavaScript中所有数字均用浮点数值表示. 转换: parseInt(..)    将某值转换成数字,不成功则NaN pa ...

  6. JavaScript字符串&数字间转换

    比较操作符的操作数可以是任意类型.然而,只有数字和字符串才能真正执行边角操作,因此那些不是数字和字符串的操作数都讲进行类型转换,类型转换规则如下:      如果操作数为对象,那么对象转换为原始值:如 ...

  7. JS 浮点型数字运算(转)

    示例: var num1=3.3; var num2=7.17; var ret=parseFloat(num1)+parseFloat(num2); //ret的值为:10.469999999999 ...

  8. 你可能不知道的 JavaScript 中数字取整

    网上方法很多,标题党一下,勿拍 ^_^!实际开发过程中经常遇到数字取整问题,所以这篇文章收集了一些方法,以备查询. 常用的直接取整方法 直接取整就是舍去小数部分. 1.parseInt() parse ...

  9. JavaScript非数字(中文)排序

    直接上代码: var arr=[ {name:"张散步",age:"23",sports:"篮球",number:"231123& ...

随机推荐

  1. mongo常用语法

    首先要能进入控制台,进不去自己解决. 基本操作: show users:显示用户 show dbs:显示数据库列表 use <db name> 切换/创建数据库 show collecti ...

  2. Nginx总结(三)基于端口的虚拟主机配置

    前面讲了如何配置基于IP的虚拟主机,大家可以去这里看看nginx系列文章:https://www.cnblogs.com/zhangweizhong/category/1529997.html 今天就 ...

  3. 【阿里云IoT+YF3300】4.Alink物模型之事件触发

    名词解释:设备的功能模型之一,设备运行时的事件,事件一般包含需要被外部感知和处理的通知信息,可包含多个输出参数.如,某项任务完成的信息,或者设备发生故障或告警时的温度等,事件可以被订阅和推送. 在工控 ...

  4. python 17 内置模块

    目录 1. 序列化模块 1.1 json 模块 1.2 pickle 模块 2. os 模块 3. sys 模块 4. hashlib 加密.摘要 4.1 加密 4.2 加盐 4.3 文件一致性校验 ...

  5. 水果机抽奖(CocosCreator)

    推荐阅读:  我的CSDN  我的博客园  QQ群:704621321 一.前言       在前面给大家分享了大转盘的抽奖方式,这是现在游戏使用较多的一种抽奖方式,今天给大家介绍另一抽奖方式--水果 ...

  6. BZOJ2655 Calc - dp 拉格朗日插值法

    BZOJ2655 Calc 参考 题意: 给定n,m,mod,问在对mod取模的背景下,从[1,m]中选出n个数相乘可以得到的总和为多少. 思路: 首先可以发现dp方程 ,假定dp[m][n]表示从[ ...

  7. CodeForces 1082 G Petya and Graph 最大权闭合子图。

    题目传送门 题意:现在有一个图,选择一条边,会把边的2个顶点也选起来,最后会的到一个边的集合 和一个点的集合 , 求边的集合 - 点的集合最大是多少. 题解:裸的最大权闭合子图. 代码: #inclu ...

  8. hdu 4614 Vases and Flowers(线段树)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=4614 题意: 给你N个花瓶,编号是0  到 N - 1 ,初始状态花瓶是空的,每个花瓶最多插一朵花. ...

  9. 解决go get下载包失败问题

    由于某些不可抗力的原因,国内使用go get命令安装包时会经常会出现timeout的问题.本文介绍几个常用的解决办法. 从github克隆 golang在github上建立了一个镜像库,如https: ...

  10. Failed to read artifact descriptor for xxx:jar:版本号

    解决步骤: 1.先删除对应的本地仓库下的文件夹,比如org.apache.maven.plugins:maven-resource,删掉plugins文件夹 2.项目右键-->Maven--&g ...