今天开发的时候碰到一个问题
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 这么流行的语言不太会出现这种问题
发个贴来问下大家的看法
1
masterjoess 262 天前
因为 toFixed 不是四舍五入
|
2
Puteulanus 262 天前
104.06 感觉像奇进偶舍的结果
|
3
henix 262 天前
js 的四舍五入应该是:Math.round(99.1*1.05*100)/100 // => 104.06
|
4
codehz 262 天前 6
因为 ruby 的实现里
https://github.com/ruby/ruby/blob/master/numeric.c#L2536C13-L2536C25 发现数字的小数位数大于 14 位就会用另一个算法了 |
5
jhdxr 262 天前 2
因为这些不是一个问题/bug 而是一个 feature 。
Floating Point Precision ,属于我能想到的最常见的科班知道而非科班甚至不会意识到这东西存在的一个问题。 |
6
thinkershare 262 天前 1
浮点数常用的舍入有 6/7 种,每种舍入都有自己的具体使用场景。你先搞清楚,每个方法的具体舍入算法再看。
只要使用的是 IEEE64 ,则最终同一个算法的结果应该是相同的。 |
7
unspring OP |
9
unspring OP @masterjoess toFixed 会自动四舍五入,而且 mathjs 和 lodash 也是一样会算成 104.05
|
10
wildnode 262 天前 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 |
11
aloxaf 262 天前
@unspring #8 实际上大部分语言对浮点数运算都没有特殊处理,因为二进制就是没办法精确表示十进制小数,你压根处理不完特殊情况。0.1 + 0.2 都不等于 0.3 ,也没有哪个语言为浮点数重载一下等于号。
你要是追求精确,就不该用浮点数。 |
12
lyxxxh2 262 天前
某篇文章:
在 Python3 中 ,round()函数 并不是四舍五入,而是四舍六入五成双,遵循向偶数靠拢的原则,为奇数则进 1 ,为偶数则不进位。 所以我不用 round,自己封装 fixed ``` function toFixed($str, $precision) { return number_format((float) $str, $precision, '.', ''); } ``` |
13
nitmali 262 天前
toFixed(2)就是在第三位上四舍五入
|
14
codehz 262 天前
@unspring 那个方法效率很低啊(可能慢数十倍),而且也不是彻底的方案,因为其实一开始出现这个数字就表示前面计算的结果已经无法精确表达了,还有很多边界条件没处理到
|
15
nitmali 262 天前
而且其中还涉及到一个银行家算法,不是传统意义上的四舍五入。
|
16
masterjoess 262 天前
@unspring 如果你觉得 IEEE 754 银行家舍入也算(小学学的)四舍五入,那你确定说的对
|
17
sankooc 262 天前
|
18
sankooc 262 天前 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 方法很明显不是咱们理解的四舍五入 |
19
realJamespond 262 天前
二进制没法表示示例中的 10 进制小数,所以有误差,相当于 10 进制表示 1/3
|
20
min 262 天前
“感觉 nodejs 这么流行的语言不太会出现这种问题”
对 js 的严谨程度这么有信心吗? |
21
codehz 262 天前 1
https://bugs.ruby-lang.org/issues/14635
看了一下关联的 issue ,这个可能和浮点数通常的问题有些不一样 |
22
yesterdaysun 262 天前
https://en.wikipedia.org/wiki/Rounding
可选的舍入方式有 6 种, 常说的四舍五入对应 infinity 这种, 在 c#里面也叫 AwayFromZero, 但是这个会有统计学误差, 所以另一种常见的舍入方式是 even, c#里叫 ToEven, Java 里叫 HalfEven, 也就是上面有人提到的银行家舍入 不同的语言, 不同的函数使用的舍入规则都是不一样, 比如 toFixed 和 Math.round 用的就是不一样的, MySQL 的 decimal 和 float 规则不一样, 如果追求 100%精确的话就得去看文档他们用的到底是哪一种方案, 或者 Java/c#这种可以有选项让你控制使用哪一种舍入规则 |
23
mxT52CRuqR6o5 262 天前
按照四舍五入规则的话最终结果应该是 104.05 ,但受限于浮点数精度问题计算结果不准导致舍入问题
如果是和钱有关的场景就不能直接用浮点数去算(所有语言都是),比如 js 里可以用 dicimal.js https://mikemcl.github.io/decimal.js/ 在控制台中运行 Decimal('99.1').mul('1.05').toFixed(2) 可以得到 104.06 |
24
54xavier 262 天前
不就是浮点运算精度的问题吗?
|
25
vituralfuture 262 天前 via Android
私以为不是浮点数精度问题而是输出时的截断策略问题,各种语言应当遵守 IEEE 754 ,也就是浮点数的二进制表示方法是相同的,同一架构下浮点数的计算方法也应该是相同的,只是一般输出时自动截断小数点后多少位,截断的过程包括了舍入,而不同语言截断的策略不同,输出自然不同
如何验证? 使用各种语言计算这个值,将得到的浮点数的二进制表示输出,注意输出的应该是 32 位的二进制。然后逐字节比较,应当是完全相同的 另外楼上提到的浮点数精度问题,在无法容忍浮点数带来的误差的场景下,应该使用十进制数,这个在许多语言都有提供,只是性能低很多 |
26
tool2d 262 天前
我程序是自研算法,对于 104.05499999999999 这类的数字,在最后一位有效位,进行四舍五入处理,这样就变成了 104.05500000000, 然后把尾巴 0 去掉。
这样付出的代价,是浮点精度少一位,换来的是大部分情况下,整整齐齐的小数。 |
27
charlie21 262 天前 via Android
js 四舍五入用 Intl.NumberFormat (大约 2021 年开始被广泛使用)
https://developer.aliyun.com/article/1377609 像这种提都没提过 Intl 的呢这是老文章了 https://juejin.cn/post/6979515365227233294 https://zhuanlan.zhihu.com/p/356991916? |
28
v21984 262 天前
toFixed 四舍六入五留双
|
29
hcwhan 262 天前 1
因为 toFixed 不是四舍五入 使用的是银行家舍入 需要四舍五入用 Math.round
1 , 四舍五入 当舍去位的数值大于等于 5 时,在舍去该位的同时向前位进一;当舍去位的数值小于 5 时,则直接舍去该位。 2 , 银行家舍入 所谓银行家舍入法,其实质是一种四舍六入五取偶(又称四舍六入五留双)法。其规则是:当舍去位的数值小于 5 时,直接舍去该位;当舍去位的数值大于等于 6 时,在舍去该位的同时向前位进一;当舍去位的数值等于 5 时,如果前位数值为奇,则在舍去该位的同时向前位进一,如果前位数值为偶,则直接舍去该位。 |
30
liuhuihao 262 天前 1
|
31
orange2023 262 天前
@liuhuihao 尝试(2.55)%1 得到小数部分
|
32
orange2023 262 天前
我认为不能认为一个 float 比如 1.15 它真的就是 1.15 啊,可能实际是 0.1499999999999999
|
33
jsjhlk 262 天前
|
34
unspring OP 确实按照四舍六入五成双的算法来算,结果应该是 104.05
但是手写竖式计算结果是 104.055 四舍五入后显然是 104.06 这意味着包括 toFixed ,mathjs.round 在内的方法对这个数字的 rounding 都是不正确的 这意味着 js 的浮点运算实际上并不准确,会出现小数点后两位之内的误差 |
35
unspring OP 至少是和大家通常使用的普遍意义上的舍入是不同的
|
36
lscho 262 天前 via iPhone
稍微了解点 js 就知道 toFixed 和 round 不是一回事
|
37
CRVV 262 天前 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 |
38
CRVV 262 天前
顺便一说
十进制 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 |