记一次 JavaScript 浮点型数字误差引发的问题
需求
车间的工人在生产出来产品后,需要完成初步的自检,并通过手机上报。在实际生产中,用户(工人)不方便进行数值的输入,因而表单中的一些项设计成 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 浮点型数字误差引发的问题的更多相关文章
- [WinForm]TextBox只能输入数字或者正浮点型数字
关键代码: /// <summary> /// 只能输入数字[KeyPress事件] /// </summary> /// <param name="textB ...
- JavaScript将数字转换为大写金额
用JavaScript将数字转换为大写金额,好了 0.0 To code! var digitUppercase = function(n) { var fraction = ['角', '分']; ...
- QDoubleSpinBox浮点型数字调节框
样式: import sys from PyQt5.QtWidgets import QApplication, QWidget, QDoubleSpinBox class Demo(QWidget) ...
- JavaScript 格式化数字、金额、千分位、保留几位小数、舍入舍去…
JavaScript 格式化数字.金额.千分位.保留几位小数.舍入舍去… 类库推荐 1. Numeral.js 一个用于格式化和操作数字的JavaScript库.数字可以被格式化为货币,百分比,时间, ...
- 前端 javascript 数据类型 数字
1.数字(Number) JavaScript中不区分整数值和浮点数值,JavaScript中所有数字均用浮点数值表示. 转换: parseInt(..) 将某值转换成数字,不成功则NaN pa ...
- JavaScript字符串&数字间转换
比较操作符的操作数可以是任意类型.然而,只有数字和字符串才能真正执行边角操作,因此那些不是数字和字符串的操作数都讲进行类型转换,类型转换规则如下: 如果操作数为对象,那么对象转换为原始值:如 ...
- JS 浮点型数字运算(转)
示例: var num1=3.3; var num2=7.17; var ret=parseFloat(num1)+parseFloat(num2); //ret的值为:10.469999999999 ...
- 你可能不知道的 JavaScript 中数字取整
网上方法很多,标题党一下,勿拍 ^_^!实际开发过程中经常遇到数字取整问题,所以这篇文章收集了一些方法,以备查询. 常用的直接取整方法 直接取整就是舍去小数部分. 1.parseInt() parse ...
- JavaScript非数字(中文)排序
直接上代码: var arr=[ {name:"张散步",age:"23",sports:"篮球",number:"231123& ...
随机推荐
- css布局之居中
CSS布局之居中 本文主要是介绍水平居中,垂直居中,还有水平垂直居中的方法 水平居中 1.行内元素水平居中 使用text-align:center;就可以实现行内元素的水平居中,但是记得要在父元素中设 ...
- ajax方法请求成功后,没有执行success的方法
$.ajax( { type: "POST", url: "AddSupplier.aspx", dataType:"text", data ...
- SpringBoot中快速实现邮箱发送
前言 在许多企业级项目中,需要用到邮件发送的功能,如: 注册用户时需要邮箱发送验证 用户生日时发送邮件通知祝贺 发送邮件给用户等 创建工程导入依赖 <!-- 邮箱发送依赖 --> < ...
- Python day01 课堂笔记
今天是第一天学习Python课程,主要从计算机基础,Python的历史,环境 ,变量,常量,注释,用户交互,基础数据类型 ,简单的if条件语句和while循环语句这几个来学习,重点的掌握内容是pyth ...
- Swoole引擎原理的快速入门干货
更多内容,欢迎关注微信公众号:全菜工程师小辉~ 过去一年使用PHP和Java两种技术栈完成了一个游戏服务器项目.由于项目中有高频的网络请求,所以PHP技术栈尝试使用Swoole引擎(基于事件的高性能异 ...
- python 26 反射
目录 1. 类的反射 1.1 实例对象 1.2 类 1.3 其他模块 -- 其他.py文件 1.4 本模块 -- 当前.py文件 2. 函数与方法的区别 2.1区别的方法: 2.2 总结 3. 特殊的 ...
- unity游戏开发_对象池
现在假设游戏中我们需要实现一个这样功能,按下鼠标左键,发射一颗子弹,3秒之后消失.在这个功能中,我们发射了上百上千发子弹,就需要实例化生成上百上千次.这时候,我们就需要使用对象池这个概念,每次实例化生 ...
- Android进阶之路(2)-详解MVP
### MVP简介 >MVP 全称:Model-View-Presenter :MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的[地方](https://baike.baidu.co ...
- Java中集合的概述
一.集合和数组的区别 1.数组(可以存储基本数据类型)是用来存现对象的一种容器,但是数组的长度固定,不适合在对象数量未知的情况下使用. 2.集合(只能存储对象,对象类型可以不一样)的长度可变,可在多数 ...
- (二)快速搭建 ASP.net core Web 应用
目录 1. 新建项目并上传Github 2. 关联Jenkins实现持续集成 3. 已经磨好枪了,开始写代码 1. 新建项目并上传Github 新建 ASP.NET Core Web 应用程序,勾选“ ...