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

2024-03-04 12:12:07 +08:00
unspring  unspring

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

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 这么流行的语言不太会出现这种问题

发个贴来问下大家的看法

4106 次点击
所在节点   程序员  程序员
38 条回复
codehz
codehz
2024-03-04 14:52:42 +08:00
https://bugs.ruby-lang.org/issues/14635
看了一下关联的 issue ,这个可能和浮点数通常的问题有些不一样
yesterdaysun
yesterdaysun
2024-03-04 15:02:05 +08:00
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
mxT52CRuqR6o5
2024-03-04 15:14:34 +08:00
按照四舍五入规则的话最终结果应该是 104.05 ,但受限于浮点数精度问题计算结果不准导致舍入问题
如果是和钱有关的场景就不能直接用浮点数去算(所有语言都是),比如 js 里可以用 dicimal.js
https://mikemcl.github.io/decimal.js/
在控制台中运行
Decimal('99.1').mul('1.05').toFixed(2)
可以得到 104.06
54xavier
54xavier
2024-03-04 15:56:01 +08:00
不就是浮点运算精度的问题吗?
vituralfuture
vituralfuture
2024-03-04 16:47:58 +08:00
私以为不是浮点数精度问题而是输出时的截断策略问题,各种语言应当遵守 IEEE 754 ,也就是浮点数的二进制表示方法是相同的,同一架构下浮点数的计算方法也应该是相同的,只是一般输出时自动截断小数点后多少位,截断的过程包括了舍入,而不同语言截断的策略不同,输出自然不同

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

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

这样付出的代价,是浮点精度少一位,换来的是大部分情况下,整整齐齐的小数。
charlie21
charlie21
2024-03-04 17:01:37 +08:00
js 四舍五入用 Intl.NumberFormat (大约 2021 年开始被广泛使用)

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

https://juejin.cn/post/6979515365227233294

https://zhuanlan.zhihu.com/p/356991916?
v21984
v21984
2024-03-04 17:23:10 +08:00
toFixed 四舍六入五留双
hcwhan
hcwhan
2024-03-04 17:23:49 +08:00
因为 toFixed 不是四舍五入 使用的是银行家舍入 需要四舍五入用 Math.round

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

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

测试 (2.55).toFixed(1) => '2.5'
按照标准四舍五入应该是 '2.6'
orange2023
2024-03-04 19:33:23 +08:00
@liuhuihao 尝试(2.55)%1 得到小数部分
orange2023
2024-03-04 19:35:57 +08:00
我认为不能认为一个 float 比如 1.15 它真的就是 1.15 啊,可能实际是 0.1499999999999999
jsjhlk
2024-03-04 19:48:04 +08:00
unspring
2024-03-04 21:39:53 +08:00
确实按照四舍六入五成双的算法来算,结果应该是 104.05

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

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

这意味着 js 的浮点运算实际上并不准确,会出现小数点后两位之内的误差
unspring
2024-03-04 21:44:10 +08:00
至少是和大家通常使用的普遍意义上的舍入是不同的
lscho
2024-03-04 21:58:44 +08:00
稍微了解点 js 就知道 toFixed 和 round 不是一回事
CRVV
2024-03-04 22:04:48 +08:00
这两个浮点数的精确值,实际上
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
2024-03-04 22:17:11 +08:00
顺便一说

十进制
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

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1020406

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX