Ruby 由小数的精度问题引出设计问题

woaigithub · 2012年12月06日 · 最后由 woaigithub 回复于 2012年12月07日 · 8816 次阅读

irb(main):025:0> 1.9+18.99
=> 20.889999999999997

这种小数如何控制精度呢?我想要的结果是 20.89!如何才能得到呢?

难道要这么做吗? "%.2f" % BigDecimal.new((1.9+18.99).to_s)

(1.9+18.99).round(2)

(1.9+18.99).round(2)

以前有人问过,参考下: http://ruby-china.org/topics/6982

#1 楼 @woaigithub

其实 20.889999999999997 比 20.89 精确,因为后者只有四位有效数字...

如果是算钱,从出生起就得用 bigdecimal 控制住: BigDecimal.new('1.9') + BigDecimal.new('18.99')

#5 楼 @luikore 步步为赢啊!!!

有关有小数的计算,如果不用BigDecimal的话基本上都做不到精确的计算,即使是1/3*3这样的算式也得不到准确的结果

#5 楼 @luikore 是啊,财务系统很难做的 #7 楼 @ywjno 是啊,不知道什么时候结果就出问题

难道不是:

[1] pry(main)> (190 + 1899)/100.0
=> 20.89

#9 楼 @Saito 你的意思是把小数都换算成整数,然后再除 100 或者 1000 之类的,用来保存 2 位或者 3 位,这样就可以避免小数的精度问题。是不是?

#10 楼 @woaigithub 算钱基本就是这么算的,后台全部以分结算。

我知道结算最小单位用分。 那就是说换算值钱都转成整数来计算,然后需要结果的时候再除以 100。 加入我两个小数需要乘呢? 1.25*58.12 这个难道需要变成 (125*5812)/1000

小数点后面这么长,这点误差,实际使用中,只要 round(2) 就没事了。

(1.9+18.99).round(2)

#11 楼 @Saito 结算最小单位用分。 那就是说换算值钱都转成整数来计算,然后需要结果的时候再除以 100。 加入我两个小数需要乘呢? 1.25*58.12 这个难道需要变成 (125*5812)/1000

#14 楼 @woaigithub 你给我一个这样的场景来..

一般意义上的商品都是不可分割的。没人会说我要 58.12 个什么东西。

如果你是卖切糕的。那另当别论。

你的系统就应该设计成按克来结算,而不是按斤。这样才不会出现说我要 58.12 斤的切糕的情况。

#15 楼 @Saito 有点通了。 就是说结算单位小一点,但是用户看到的可能是斤,公斤,吨之类的大单位。这样结算的重量就不会是小数。 单价可能会有小数,把这个小数转换成整数,和最小结算单位一起进行计算,完毕之后再除以 100。 但是有这么一个场景,就是我要抽取 1% 的佣金,这时候不就会出现小数*小数了吗?

#15 楼 @Saito 举个例子。 一个西瓜,税前 4.67 元/公斤,结算单位是克,买 12 公斤。收取 1% 税。

1 公斤 = 1000 克 结算价 4.67 元/公斤 = 4670 元/克 (4670 * 12000 + 4670 * 12000 * 0.01)/ 1000000 = 56.6004

最终含税金额: 56.6004.round(2) = 56.60

是这么搞的吗?

#17 楼 @woaigithub 抽税是这样的,你不可能抽到分以下的。

所以你还是必须选择,或者四舍五入。或者断尾.. 百分比抽税是比较难对账的。

#9 楼 @Saito

[1] pry(main)> (190 + 1899)/100.0
=> 20.89

你说的这个 100,如果在电子商务系统中,是不是应该根据结算单位和显示单位来定呢? 如果显示是千克,结算用克,就该是 1000 吧。

结算单位又如何选择呢? 为什么选择克呢,怎么不选则毫克呢? 看来引出来不少问题!

#18 楼 @Saito 多问几句。 是不是和小数有关的计算,或者说财务方面的计算,都是这么干的呢?就是因为精度的问题,是不是呢?

#19 楼 @woaigithub 看你卖什么东西。如果是什么 氰 X 钾 什么的话那结算单位就不一样。

卖西瓜肯定不会出现毫克。

#21 楼 @Saito 是不是有的时候,还是要先换算成整数,防止计算精度问题导致结果出错,然后在需要舍入的地方才进行舍入,尽量延迟舍入较好。

#20 楼 @woaigithub 不一定。有些系统真的是用 BigDecimal 搞的。

但是整数结算是能解决大部分问题的。

#23 楼 @Saito 就是说还有从一开始就把输入用 bigdecimal 包起来,中间所有的过程中的小数,也都用 bigdecimal 包起来。

#24 楼 @woaigithub 最后包一次。但是这个解决不了问题。因为钱是没有分以下的。

最终还是得 round.

#25 楼 @Saito 就是说整数结算可以消除小数精度问题,换算到多大的整数,以及结算单位的选择都是有关联的,需要同时考虑。宗旨就是尽量保证结算过程使用整数,只在最后需要变成金额的时候处以 100,或者 1000,或者 10000 之类的整数。

#26 楼 @woaigithub 一般保证金额是分就好了。

真的要做抽成这种,不得不损失精度,来解决现实与计算结果不匹配的情况。那就晚做。

#27 楼 @Saito 把容易出现精度问题的地方延迟来做。

@woaigithub @Saito 整数处理的话,还有打 0.85 折,利息 1% 之类的事情哦,bigdecimal 在 10 进制加减乘是可以完全不丢的

#29 楼 @luikore 丢不丢是一部分。能不能跟现实匹配是另一部分..

即使精确到后面四位小数,取不出来钱什么都是白搭。最终还是得 round. 貌似银行都是直接断尾,不四舍五入。

ps: bigdecimal 比直接整数加减可慢多了...

银行的算法是:只要是我欠你的钱 (利息), 就断尾。你欠我的钱 (贷款), 就进位。

就辟谣...

#31 楼 @Saito 求辟谣,这段代码估计辟谣不了啊,没有敢啊!!

需要 登录 后方可回复, 如果你还没有账号请 注册新账号