V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
unspring
V2EX  ›  程序员

为什么不同语言对 99.1*1.05 的四舍五入结果不一样

  •  
  •   unspring · 86 天前 · 3686 次点击
    这是一个创建于 86 天前的主题,其中的信息可能已经有所发展或是发生改变。

    今天开发的时候碰到一个问题

    nodejs 在计算

    (99.1 *1.05).toFixed(2)

    时的输出是 104.05

    而 Ruby 计算

    (99.1*1.05).round(2)

    时的输出是 104.06

    我还试了下其他语言 Python 和 nodejs 是一样的

    Excel 和 Ruby 的输出是一样的

    99.1*1.05 的结果是 104.05499999999999 但不同语言对这个数字的舍入处理却不同

    感觉 nodejs 这么流行的语言不太会出现这种问题

    发个贴来问下大家的看法

    38 条回复    2024-03-04 22:17:11 +08:00
    masterjoess
        1
    masterjoess  
       86 天前
    因为 toFixed 不是四舍五入
    Puteulanus
        2
    Puteulanus  
       86 天前
    104.06 感觉像奇进偶舍的结果
    henix
        3
    henix  
       86 天前
    js 的四舍五入应该是:Math.round(99.1*1.05*100)/100 // => 104.06
    codehz
        4
    codehz  
       86 天前   ❤️ 6
    因为 ruby 的实现里
    https://github.com/ruby/ruby/blob/master/numeric.c#L2536C13-L2536C25
    发现数字的小数位数大于 14 位就会用另一个算法了
    jhdxr
        5
    jhdxr  
       86 天前   ❤️ 2
    因为这些不是一个问题/bug 而是一个 feature 。

    Floating Point Precision ,属于我能想到的最常见的科班知道而非科班甚至不会意识到这东西存在的一个问题。
    thinkershare
        6
    thinkershare  
       86 天前   ❤️ 1
    浮点数常用的舍入有 6/7 种,每种舍入都有自己的具体使用场景。你先搞清楚,每个方法的具体舍入算法再看。
    只要使用的是 IEEE64 ,则最终同一个算法的结果应该是相同的。
    unspring
        7
    unspring  
    OP
       86 天前 via Android
    @henix 这个做法感觉更像是 double 转成 long 再转回来,感觉不是通用解法

    程序员在意识到这个问题之前不会这么处理,而这个问题也难以发现
    unspring
        8
    unspring  
    OP
       86 天前 via Android
    @codehz 震惊,居然能迅速翻到源码

    js 对这种场景居然就没处理吗
    unspring
        9
    unspring  
    OP
       86 天前 via Android
    @masterjoess toFixed 会自动四舍五入,而且 mathjs 和 lodash 也是一样会算成 104.05
    wildnode
        10
    wildnode  
       86 天前   ❤️ 1
    首先,Node.js 不是一门语言,它只是 JS 的一个运行时,所以本质上是 JS 浮点数精度问题

    ```js
    let num = 99.1 *1.05;
    let adjustedNum = num + (Number.EPSILON * Math.pow(10, 2));
    console.log(adjustedNum.toFixed(2)); // 104.06
    ```

    这样可以实现你想要的效果,但是有风险,不能用于生产。

    生产推荐使用 decimal.js 或者 big.js
    aloxaf
        11
    aloxaf  
       86 天前
    @unspring #8 实际上大部分语言对浮点数运算都没有特殊处理,因为二进制就是没办法精确表示十进制小数,你压根处理不完特殊情况。0.1 + 0.2 都不等于 0.3 ,也没有哪个语言为浮点数重载一下等于号。
    你要是追求精确,就不该用浮点数。
    lyxxxh2
        12
    lyxxxh2  
       86 天前
    某篇文章:
    在 Python3 中 ,round()函数 并不是四舍五入,而是四舍六入五成双,遵循向偶数靠拢的原则,为奇数则进 1 ,为偶数则不进位。


    所以我不用 round,自己封装 fixed
    ```
    function toFixed($str, $precision)
    {
    return number_format((float) $str, $precision, '.', '');
    }
    ```
    nitmali
        13
    nitmali  
       86 天前
    toFixed(2)就是在第三位上四舍五入
    codehz
        14
    codehz  
       86 天前
    @unspring 那个方法效率很低啊(可能慢数十倍),而且也不是彻底的方案,因为其实一开始出现这个数字就表示前面计算的结果已经无法精确表达了,还有很多边界条件没处理到
    nitmali
        15
    nitmali  
       86 天前
    而且其中还涉及到一个银行家算法,不是传统意义上的四舍五入。
    masterjoess
        16
    masterjoess  
       86 天前
    @unspring 如果你觉得 IEEE 754 银行家舍入也算(小学学的)四舍五入,那你确定说的对
    sankooc
        17
    sankooc  
       86 天前
    sankooc
        18
    sankooc  
       86 天前   ❤️ 1
    (2.34).toFixed(1); // '2.3'
    (2.35).toFixed(1); // '2.4'; it rounds up
    (2.55).toFixed(1); // '2.5'
    // it rounds down as it can't be represented exactly by a float and the closest representable float is lower
    (2.449999999999999999).toFixed(1); // '2.5'
    // it rounds up as it's less than Number.EPSILON away from 2.45.
    // This literal actually encodes the same number value as 2.45
    ---
    toFixed 方法很明显不是咱们理解的四舍五入
    realJamespond
        19
    realJamespond  
       86 天前
    二进制没法表示示例中的 10 进制小数,所以有误差,相当于 10 进制表示 1/3
    min
        20
    min  
       86 天前
    “感觉 nodejs 这么流行的语言不太会出现这种问题”

    对 js 的严谨程度这么有信心吗?
    codehz
        21
    codehz  
       86 天前   ❤️ 1
    https://bugs.ruby-lang.org/issues/14635
    看了一下关联的 issue ,这个可能和浮点数通常的问题有些不一样
    yesterdaysun
        22
    yesterdaysun  
       86 天前
    https://en.wikipedia.org/wiki/Rounding
    可选的舍入方式有 6 种, 常说的四舍五入对应 infinity 这种, 在 c#里面也叫 AwayFromZero, 但是这个会有统计学误差, 所以另一种常见的舍入方式是 even, c#里叫 ToEven, Java 里叫 HalfEven, 也就是上面有人提到的银行家舍入

    不同的语言, 不同的函数使用的舍入规则都是不一样, 比如 toFixed 和 Math.round 用的就是不一样的, MySQL 的 decimal 和 float 规则不一样, 如果追求 100%精确的话就得去看文档他们用的到底是哪一种方案, 或者 Java/c#这种可以有选项让你控制使用哪一种舍入规则
    mxT52CRuqR6o5
        23
    mxT52CRuqR6o5  
       86 天前
    按照四舍五入规则的话最终结果应该是 104.05 ,但受限于浮点数精度问题计算结果不准导致舍入问题
    如果是和钱有关的场景就不能直接用浮点数去算(所有语言都是),比如 js 里可以用 dicimal.js
    https://mikemcl.github.io/decimal.js/
    在控制台中运行
    Decimal('99.1').mul('1.05').toFixed(2)
    可以得到 104.06
    54xavier
        24
    54xavier  
       86 天前
    不就是浮点运算精度的问题吗?
    vituralfuture
        25
    vituralfuture  
       86 天前 via Android
    私以为不是浮点数精度问题而是输出时的截断策略问题,各种语言应当遵守 IEEE 754 ,也就是浮点数的二进制表示方法是相同的,同一架构下浮点数的计算方法也应该是相同的,只是一般输出时自动截断小数点后多少位,截断的过程包括了舍入,而不同语言截断的策略不同,输出自然不同

    如何验证?
    使用各种语言计算这个值,将得到的浮点数的二进制表示输出,注意输出的应该是 32 位的二进制。然后逐字节比较,应当是完全相同的

    另外楼上提到的浮点数精度问题,在无法容忍浮点数带来的误差的场景下,应该使用十进制数,这个在许多语言都有提供,只是性能低很多
    tool2d
        26
    tool2d  
       86 天前
    我程序是自研算法,对于 104.05499999999999 这类的数字,在最后一位有效位,进行四舍五入处理,这样就变成了 104.05500000000, 然后把尾巴 0 去掉。

    这样付出的代价,是浮点精度少一位,换来的是大部分情况下,整整齐齐的小数。
    charlie21
        27
    charlie21  
       86 天前 via Android
    js 四舍五入用 Intl.NumberFormat (大约 2021 年开始被广泛使用)

    https://developer.aliyun.com/article/1377609 像这种提都没提过 Intl 的呢这是老文章了

    https://juejin.cn/post/6979515365227233294

    https://zhuanlan.zhihu.com/p/356991916?
    v21984
        28
    v21984  
       86 天前
    toFixed 四舍六入五留双
    hcwhan
        29
    hcwhan  
       86 天前   ❤️ 1
    因为 toFixed 不是四舍五入 使用的是银行家舍入 需要四舍五入用 Math.round

    1 , 四舍五入
    当舍去位的数值大于等于 5 时,在舍去该位的同时向前位进一;当舍去位的数值小于 5 时,则直接舍去该位。
    2 , 银行家舍入
    所谓银行家舍入法,其实质是一种四舍六入五取偶(又称四舍六入五留双)法。其规则是:当舍去位的数值小于 5 时,直接舍去该位;当舍去位的数值大于等于 6 时,在舍去该位的同时向前位进一;当舍去位的数值等于 5 时,如果前位数值为奇,则在舍去该位的同时向前位进一,如果前位数值为偶,则直接舍去该位。
    liuhuihao
        30
    liuhuihao  
       86 天前   ❤️ 1
    @hcwhan @v21984 toFixed 既不是银行家舍入也不是标准四舍五入,而是有一套自己的规则。

    测试 (2.335).toFixed(2) => '2.33'
    按照银行家的舍入规则应该是 '2.34'

    测试 (2.55).toFixed(1) => '2.5'
    按照标准四舍五入应该是 '2.6'
    orange2023
        31
    orange2023  
       86 天前
    @liuhuihao 尝试(2.55)%1 得到小数部分
    orange2023
        32
    orange2023  
       86 天前
    我认为不能认为一个 float 比如 1.15 它真的就是 1.15 啊,可能实际是 0.1499999999999999
    unspring
        34
    unspring  
    OP
       86 天前 via Android
    确实按照四舍六入五成双的算法来算,结果应该是 104.05

    但是手写竖式计算结果是 104.055
    四舍五入后显然是 104.06

    这意味着包括 toFixed ,mathjs.round 在内的方法对这个数字的 rounding 都是不正确的

    这意味着 js 的浮点运算实际上并不准确,会出现小数点后两位之内的误差
    unspring
        35
    unspring  
    OP
       86 天前 via Android
    至少是和大家通常使用的普遍意义上的舍入是不同的
    lscho
        36
    lscho  
       86 天前 via iPhone
    稍微了解点 js 就知道 toFixed 和 round 不是一回事
    CRVV
        37
    CRVV  
       86 天前   ❤️ 1
    这两个浮点数的精确值,实际上
    99.1*1.05 是 104.0549999999999926103555480949580669403076171875
    104.055 是 104.05500000000000682121026329696178436279296875
    这两个数字之间没有其它的 float64 了

    这两个数字都不是刚好一半的情况,所以和舍入规则没关系
    不论用不用 银行家舍入,round(99.1*1.05, 2) 都是 104.05 ,round(104.055, 2) 都是 104.06

    Excel 可能是把 99.1*1.05 的结果直接算成了后面那个 104.05500000000000682121026329696178436279296875 ,然后再 round 当然就得到了 104.06
    Excel 应该也能正确处理各种 .1+.2 == .3 的情况

    这个问题在 Python 文档里面说得很清楚,https://docs.python.org/3/library/functions.html#round

    > Note: The behavior of round() for floats can be surprising: for example, round(2.675, 2) gives 2.67 instead of the expected 2.68. This is not a bug: it’s a result of the fact that most decimal fractions can’t be represented exactly as a float. See Floating Point Arithmetic: Issues and Limitations for more information.


    Ruby 好像是额外处理了这种情况,Ruby 的 2.675.round(2) 是 2.68

    irb(main):001:0> 104.054999999999978399500832892954349517822265625.round(2)
    => 104.05
    irb(main):002:0> 104.0549999999999926103555480949580669403076171875.round(2)
    => 104.06
    这个行为对我来说很 surprising ,还没写在文档里面。
    https://ruby-doc.org/core-2.5.1/Float.html#method-i-round
    CRVV
        38
    CRVV  
       86 天前
    顺便一说

    十进制
    99.1*1.005 = 99.5955
    round(99.5955, 3) = 99.596
    我猜 Excel 能得到这个结果

    Ruby
    irb(main):005:0> (99.1*1.005).round(3)
    => 99.595

    其它语言当然也是 99.595
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5269 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 03:36 · PVG 11:36 · LAX 20:36 · JFK 23:36
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.