需求

车间的工人在生产出来产品后,需要完成初步的自检,并通过手机上报。在实际生产中,用户(工人)不方便进行数值的输入,因而表单中的一些项设计成 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. c11标准

    在编译器vs13及其以上可以使用 编译器对语言的一种优化 1.变量初始化 int a=0,a(10),a{10};定义a的值的三种方式 2.nullptr 相当于c的null 有类型 更加的安全 3. ...

  2. Shrio使用Jwt达到前后端分离

    概述 前后端分离之后,因为HTTP本身是无状态的,Session就没法用了.项目采用jwt的方案后,请求的主要流程如下:用户登录成功之后,服务端会创建一个jwt的token(jwt的这个token中记 ...

  3. Flutter学习笔记(23)--多个子元素的布局Widget(Rwo、Column、Stack、IndexedStack、Table、Wrap)

    如需转载,请注明出处:Flutter学习笔记(23)--多个子元素的布局Widget(Rwo.Column.Stack.IndexedStack.Table.Wrap) 上一篇梳理了拥有单个子元素布局 ...

  4. 常用Http status code 如何记

    一直记不住http常用的status code,最近思考可以这样想.http无非就是客户端和服务端之间请求嘛.结果么要么成功,要么失败. 成功了,可以提示信息 -- Informational 1xx ...

  5. SSH密码和秘钥认证原理

    SSH登录方式主要分为两种: 1. 用户名密码验证方式 说明: (1) 当客户端发起ssh请求,服务器会把自己的公钥发送给用户: (2) 用户会根据服务器发来的公钥对密码进行加密: (3) 加密后的信 ...

  6. ElasticSearch:组合查询或复合查询

    Bool查询 允许在单独的查询中组合任意数量的查询,指定的查询语句表名哪些部分是必须匹配(must).应该匹配(should)或不能匹配(must_not) Bool过滤器 和查询功能一致,但是同等情 ...

  7. Ubuntu16.04中用yolov3训练自己的数据集

    一.配置yolo v3 参考yolo v3官网https://pjreddie.com/darknet/yolo/ 下载darknet后进行编译: git clone https://github.c ...

  8. ModelAndViewContainer、ModelMap、Model详细介绍【享学Spring MVC】

    每篇一句 一个开源的技术产品做得好不好,主要是看你能解决多少非功能性问题(因为功能性问题是所有产品都能够想到的) 前言 写这篇文章非我本意,因为我觉得对如题的这个几个类的了解还是比较基础且简单的一块内 ...

  9. 微信小程序室内地图导航开发-微信小程序JS加载esmap地图

    一.在微信小程序里显示室内三维地图 需要满足的两个条件 调用ESMap室内地图需要用到小程序web-view组件,想要通过 web-view 调用ESMap室内地图需要满足以下 2 个条件: 1. 小 ...

  10. 洛谷 P1135 【奇怪的电梯】

    题库 :洛谷 题号 :1135 题目 :奇怪的电梯 link :https://www.luogu.org/problemnew/show/P1135 一. 动态规划 : 思路 :这道题用动规来解决其 ...