zoukankan      html  css  js  c++  java
  • 0.57 * 100 === 56.99999999999999 之谜

    前言

    在最近业务开发中, 作者偶遇到了一个与 JavaScript 浮点数相关的 Bug。

    这里就简单描述下背景: 在提现相关业务时, 会将展示给用户以元为单位的数值转化为以分为单位的数值。 例如, 0.57元 转化为 57 分

    转化方法很简单

    // 小程序代码 onInput: 监听Input事件
    onInput(e) {
        let value = e.target.value;
        //限制除数字和小数点以外的字符输入
        if (!/^d*.{0,2}d{0,2}$/.test(value)) {
            value = value
                .replace(/[^d.]/g, '')
                .replace(/^./g, '')
                .replace(/.{2,}/g, '.')
                // 保留数字小数点后两位
                .replace(/^(.*..{2}).*$/, '$1');
        }
        //...
        this.setData({
            cash: +value * 100  // 乘100, 将元转化为分
        })
    }
    

    这段看似没有问题的代码, 提交给后台时, 接口却返回参数值格式不正确。

    最初, 怀疑是正则表达式有疏漏, 但测试了一下没有问题, 然后就尝试了用户输入的数值 0.57, 却发现计算值却出人意料, 也就是题目中的 0.57 * 100 === 56.99999999999999

    前端开发同学或多或少都应该看到过0.1 + 0.2 === 0.30000000000000004这个经典问题。 作者当初也抱着好奇的态度看了相关文章, 说来惭愧, 想到自己无论如何也不会开发0.1 + 0.2的业务, 也只是了解到了为什么会是这样的结果就浅尝辄止了。

    如今踩了坑, 只能说是自己跳进了当年挖的坑, 那今天就将这个坑填上。

    本文文章会讲述以下几个问题, 已经熟悉同学就可以不用看啦。

    1. 为什么 0.1 + 0.2 === 0.30000000000000004
    2. 为什么 0.57 * 100 === 56.99999999999999
    3. 为什么 0.57 * 1000 === 570

    Why 0.1 + 0.2 === 0.30000000000000004 ?

    要解答这个问题始终绕不过JavaScript中最基础也是最核心的浮点数的格式存储。 在JS中, 无论整数还是小数都是Number类型, 它的实现遵循IEEE 754, 是标准的Double双精度浮点数, 使用固定的64位来表示。

    看到这里你可能就不想看下去了。好好好, 那就后面再说, 这里就用大白话简单讲解, 详细内容在文章后面阅读。

    实际上, JS中的数字都会转化为二进制存储下来, 由于数字存储限定了64位, 但现实世界中, 数字是无穷的, 所以一定会有数字超出这个存储范围。超出这个范围的数字在存储时就会丢失精度。

    同时, 我们都知道, 整数十进制转二进制时, 是除以二去余数, 这是可以除尽的! 但我们可能不知道的是, 小数十进制转化为二进制的计算方法是, 小数部分*2, 取整数部分, 直至小数部分为0, 如果永远不为零, 在超过精度时的最后一位时0舍入1。

    /* 0.1 转化为二进制的计算过程 */
    
    0.1 * 2 = 0.2 > 取0
    0.2 * 2 = 0.4 > 取0
    0.4 * 2 = 0.8 > 取0
    0.8 * 2 = 1.6 > 取1
    0.6 * 2 = 1.2 > 取1
    0.2 * 2 = 0.4 > 取0
    ...
    后面就是循环了

    到这里, 我们就可以发现一些端倪了

    // 使用toString(2), 将10进制输出为二进制的字符串
    0.1.toString(2);
    // "0.00011001100110011001100110011001100110011001100110011001100..."
    0.2.toString(2);
    // "0.001100110011001100110011001100110011001100110011001100110011..."
    
    // 二进制相加结果, 由于超过精度, 取52位, 第53位舍0进1
    > "0.010011001100110011001100110011001100110011001100110011,1"
    // 最后存储下来的结果是
    const s = "0.010011001100110011001100110011001100110011001100110100"
    // 用算法处理一下。
    a = 0;
    s.split('').forEach((i, index) => { a += (+i/Math.pow(2, index+1))});
    // a >> 0.30000000000000004

    以上论述过程仍有一些疑惑之处

    1. 为什么小数转化为二进制后, 52位以后就超过精度了?

    这些都与64位双精度浮点数是如何存储的有关, 我们放到最后再说。

    Why 0.57 * 100 === 56.99999999999999 ?

    Why 0.57 * 1000 === 570 ?

    阅读完上面一节, 对小数的乘法我们也可以有一些自己的猜测了。

    0.57这个数值在存储时, 本身的精度不是很准确, 我们用toPrecision这个方法可以获取小数的精度。

    0.57.toPrecision(55)
    // "0.5699999999999999511501869164931122213602066040039062500"
    

    作者最初的想法有点愚蠢, 0.57的实际值是0.56999.., 那0.57 * 100也就是0.56999... * 100, 那结果就是56.99999999999999啦。

    而此时, 路总问了我一个问题, 为什么0.57 * 1000 === 570 而不是 569.99999..., 不求甚解的我只能先回答”应该是精度丢失吧”

    然而, 我”小小的眼睛里充满了大大的疑惑”…

    后来想了下, 其实我们都知道, 计算机的乘法实际上是累加计算, 并不是我们想的按位相乘。

    // 伪代码
    (0.57) * 100
    = (0.57) * (64 + 32 + 4)
    = (0.57二进制) * (2^6 + 2^5 + 2^2)
    = 0.57二进制 * 2^6 + 0.57二进制 * 2^5 + 0.57 * 2^2
    

    由于精度丢失, 这个是真的丢失啦, 在二进制转十进制时, 结果就是56.99999…了

    同理, (0.57 * 1000)也不是简单的乘, 也是累加起来的, 只是最后精度丢失时舍0进1, 结果就是570而已。

    解决问题

    对于大部分业务来讲, 确定数字精度后, 使用Math.round就可以了。 例如本文最初遇到的BUG

    const value = Math.round(0.57 * 100);

    而我们不太确定精度的浮点数运算, 通用的解决方案都是将小数转化为整数, 进行计算后, 再转化为小数就好了。

    以下是引用[1]

    /**
     * 精确加法
     */
    function add(num1, num2) {
      const num1Digits = (num1.toString().split('.')[1] || '').length;
      const num2Digits = (num2.toString().split('.')[1] || '').length;
      const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
      return (num1 * baseNum + num2 * baseNum) / baseNum;
    }

    当然已经有成熟的工具库可以使用了, 例如Math.js, BigDecimal.js, number-precision等等, 使用哪个任君挑选

    IEEE754标准下的浮点数存储

    其实下面这段内容来自于Wiki

    64位如图进行划分

    64位Double浮点数存储格式

    第0位: 是符号的标志位 第1-11位: 指数位 第12-63位: 尾数

    计算公式

    0.1为例, 0.1的二进制是0.00011001100110011001100110011001100110011001100110011001100...

    那么, 首先, 该数是正数, 标志位 sign = 0

    其次, 将小数转化为科学计数法, 指数位-4即exponent = 2 ^10 - 4 = 1019

    1.1001100110011001100110011001100110011001100110011001100... * 2^-4

    由于科学计数法, 第一个数始终是1, 所以可以忽略存储, 只要存后面的52位就可以了

    如果超过了52位, 就是对第53位舍0进1, 结果也就是100110011001100110011001100110011001100110011001101了。

    Double精度的浮点数存储大概就是这个样子了, 这也解答了上述的疑惑。

  • 相关阅读:
    2019 上海网络赛 J stone name (01背包)
    CodeForces
    2019 年百度之星·程序设计大赛
    CodeForces
    POJ 3728 The merchant (树形DP+LCA)
    HihoCoder
    HihoCoder 1055 刷油漆 (树上背包)
    HI3518E平台ISP调试环境搭建
    有用的调节
    【HI3520DV200】sample
  • 原文地址:https://www.cnblogs.com/login123/p/12100916.html
Copyright © 2011-2022 走看看