Ruby中将Float转换为Decimal不一致的问题

5

首先,取一个特定的浮点数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)

因此,使用 BigDecimal 进行乘法运算会将 f 转换为 BigDecimal。 因此,1.to_d * f(或 f * 1.to_d)可以看作是将 f 转换为 BigDecimal 的一种(低效但仍然有效的)形式。 然而对于这些特定值,我们有:

f.to_d == 1.to_d * f # => false (?!)

这难道不是一个bug吗?我本以为在用1.to_d乘以一个数时,Ruby会在内部调用f.to_d。但结果不同,例如对于 f = 64.4:

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

我不明白为什么浮点数表示错误应该成为这里的借口,但它显然是一个原因,那么为什么会发生呢?
PS.我写了一段代码来解决这个问题:https://github.com/Swarzkopf314/ruby_wtf/blob/master/multiplication_by_unit.rb

f == f.to_d 是否返回 true? - Eli Sadoff
在执行 .to_d 后,f 取什么值?内部表示通常是非常明显的。 - tadman
我编辑了我的问题。 - Maciej Satkiewicz
2个回答

4

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

简而言之,是因为使用了不同的精度。

详细解释:

64.4.to_d 调用了 bigdecimal/utilFloat#to_d 方法:

def to_d(precision=nil)
  BigDecimal(self, precision || Float::DIG)
end

除非特别说明,否则它使用浮点数的隐式精度Float::DIG,对于当前实现,该值为15
Float::DIG
#=> 15

所以64.4.to_d相当于:

BigDecimal(64.4, Float::DIG)
#=> #<BigDecimal:7fd7cc0aa838,'0.644E2',18(36)>

BigDecimal#*是另一种方法,它可以通过以下方式将给定的浮点参数转换

if (RB_TYPE_P(r, T_FLOAT)) {
    b = GetVpValueWithPrec(r, DBL_DIG+1, 1);
}

DBL_DIG是C语言中与Float::DIG等价的,因此基本上是这样的:

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

话虽如此,如果您明确提供精度,您可以获得预期的结果,方式如下:

f.to_d(16) == 1.to_d * f
#=> true

或者:

f.to_d == 1.to_d.mult(f, 15)
#=> true

当然,也可以通过使用 to_d 显式转换 f

f.to_d == 1.to_d * f.to_d
#=> true

这不是一个bug吗?

看起来是的,你应该提交一个bug报告。

请注意,0.644E20.6440000000000001E2都不是给定浮点数的精确表示。正如Eli Sadoff已经在注释中指出的那样,64.4的精确值是64.400000000000005684341886080801486968994140625,所以最准确的BigDecimal表示将是:

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

我认为64.4.to_d应该返回数字64.4。

3
这不是一个bug。f == f.to_d会返回false,所以如果f == 1.to_d * f为真,则f.to_d == 1.to_d * f必须为false,因为f != f.to_dBigDecimal==方法旨在比较BigDecimal而不是BigDecimalfloat。有时候等式会成立,但对于某些f来说,BigDecimal表示是精确的,而float则不是。
编辑:更多解释请参见浮点数计算出错了吗

1
是的,但我认为 f.to_d == 1.to_d * f 应该始终成立,但实际上并不是这样。就好像 fBigDecimal 中有两种不同的表示方式一样。对我来说,这种相等关系不总是成立是很疯狂的。 - Maciej Satkiewicz
64.4 无法被精确表示。它实际上被表示为 64.400000000000005684341886080801486968994140625。由于浮点数中没有精确的表示方式,而大十进制数确实有精确的表示方式,因此会出现问题。 - Eli Sadoff
你可以实际验证一下。将 f = 64.4,然后测试 f == 64.400000000000005684341886080801486968994140625。它会返回 true。 - Eli Sadoff
我非常清楚这一点。但我的观点是将Float转换为BigDecimal应该是一致的。如果将1.to_d * f视为将Float转换为BigDecimal的(不好,但仍然是)一种形式,则令人惊讶的是它产生的结果与f.to_d不同。这肯定会导致一个严重的错误。 - Maciej Satkiewicz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接