zoukankan      html  css  js  c++  java
  • 记一次 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 中浮点型精度的误差,是非常基础但是却又经常不被重视的问题。文中分享的方案,足以覆盖项目中的所有情况,但如果用在其它地方或项目中,在一些极端情况下可能会有问题。

    参考文档

  • 相关阅读:
    【Java EE 学习 81】【CXF框架】【CXF整合Spring】
    【Java EE 学习 80 下】【调用WebService服务的四种方式】【WebService中的注解】
    【Java EE 学习 80 上】【WebService】
    【Java EE 学习 79 下】【动态SQL】【mybatis和spring的整合】
    【Java EE 学习 79 上】【mybatis 基本使用方法】
    【Java EE 学习 78 下】【数据采集系统第十天】【数据采集系统完成】
    【Java EE 学习 78 中】【数据采集系统第十天】【Spring远程调用】
    【Java EE 学习 78 上】【数据采集系统第十天】【Service使用Spring缓存模块】
    【Java EE 学习 77 下】【数据采集系统第九天】【使用spring实现答案水平分库】【未解决问题:分库查询问题】
    【Java EE 学习 77 上】【数据采集系统第九天】【通过AOP实现日志管理】【通过Spring石英调度动态生成日志表】【日志分表和查询】
  • 原文地址:https://www.cnblogs.com/xiaoyucoding/p/11976237.html
Copyright © 2011-2022 走看看