Inconsistent conversion of Float into Decimal in Ruby

Mac*_*icz 6 ruby floating-point ruby-on-rails

First, take a specific float f:

f = [64.4, 73.60, 77.90, 87.40, 95.40].sample # take any one of these special Floats
f.to_d.class == (1.to_d * f).class # => true (BigDecimal)
Run Code Online (Sandbox Code Playgroud)

So multiplying by BigDecimal casts f to BigDecimal. Therefore 1.to_d * f (or f * 1.to_d) can be seen as a (poor, but still) form of converting f to BigDecimal. And yet for these specific values we have:

f.to_d == 1.to_d * f # => false (?!)
Run Code Online (Sandbox Code Playgroud)

Isn't this a bug? I'd assume that while multiplying by 1.to_d Ruby should invoke f.to_d internally. But the results differ, i.e. for f = 64.4:

f.to_d # => #<BigDecimal:7f8202038280,'0.644E2',18(36)>
1.to_d * f # => #<BigDecimal:7f82019c1208,'0.6440000000 000001E2',27(45)>
Run Code Online (Sandbox Code Playgroud)

I cannot see why floating-point representation error should be an excuse here, yet it's obviously a cause, somehow. So why is this happening?

附言。我写了一段代码来解决这个问题:

https://github.com/Swarzkopf314/ruby_wtf/blob/master/multiplication_by_unit.rb

Ste*_*fan 5

那么为什么会发生这种情况呢?

TL;DR 使用不同的精度。

长答案:

64.4.to_d来电:bigdecimal/utilFloat#to_d

def to_d(precision=nil)
  BigDecimal(self, precision || Float::DIG)
end
Run Code Online (Sandbox Code Playgroud)

除非指定,否则它使用隐式精度,Float::DIG15适用于当前实现:

Float::DIG
#=> 15
Run Code Online (Sandbox Code Playgroud)

所以64.4.to_d相当于:

BigDecimal(64.4, Float::DIG)
#=> #<BigDecimal:7fd7cc0aa838,'0.644E2',18(36)>
Run Code Online (Sandbox Code Playgroud)

BigDecimal#*另一方面通过以下方式转换给定的浮点参数:

if (RB_TYPE_P(r, T_FLOAT)) {
    b = GetVpValueWithPrec(r, DBL_DIG+1, 1);
}
Run Code Online (Sandbox Code Playgroud)

DBL_DIG是 的 C 等价形式Float::DIG,所以它基本上是:

BigDecimal(64.4, Float::DIG + 1)
#=> #<BigDecimal:7fd7cc098408,'0.6440000000 000001E2',27(36)>
Run Code Online (Sandbox Code Playgroud)

也就是说,如果您显式提供精度,则可以获得预期结果:

f.to_d(16) == 1.to_d * f
#=> true
Run Code Online (Sandbox Code Playgroud)

或者:

f.to_d == 1.to_d.mult(f, 15)
#=> true
Run Code Online (Sandbox Code Playgroud)

当然,通过显式f转换to_d

f.to_d == 1.to_d * f.to_d
#=> true
Run Code Online (Sandbox Code Playgroud)

这不是一个bug吗?

看起来像这样,您应该提交一份错误报告。

请注意,0.644E2、 或 都不0.6440000000000001E2是给定浮点数的精确表示。正如Eli Sadoff已经指出的64.4,的确切值是64.400000000000005684341886080801486968994140625,因此最准确的BigDecimal表示是:

BigDecimal('64.400000000000005684341886080801486968994140625')
#=> #<BigDecimal:7fd7cc04a0c8,'0.6440000000 0000005684 3418860808 0148696899 4140625E2',54(63)>
Run Code Online (Sandbox Code Playgroud)

IMO,64.4.to_d应该返回那个。