Maciej Satkiewicz
Maciej Satkiewicz

Reputation: 163

Inconsistent conversion of Float into Decimal in Ruby

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)

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 (?!)

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)>

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?

PS. I wrote a snippet of code playing around with this issue:

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

Upvotes: 6

Views: 4540

Answers (2)

Stefan
Stefan

Reputation: 114178

So why is this happening?

TL;DR different precisions are used.

Long answer:

64.4.to_d calls bigdecimal/util's Float#to_d:

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

Unless specified, it uses an implicit precision of Float::DIG which is 15 for current implementations:

Float::DIG
#=> 15

So 64.4.to_d is equivalent to:

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

BigDecimal#* on the other hand converts a given float argument via:

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

DBL_DIG is the C-equivalent of Float::DIG, so it's basically:

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

That said, you can get the expected result if you provide the precision explicitly, either:

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

or:

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

and of course by explicitly converting f via to_d:

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

Isn't this a bug?

It looks like one, you should file a bug report.

Note that neither 0.644E2, nor 0.6440000000000001E2 is an exact representation of the given floating point number. As already noted by Eli Sadoff, 64.4's exact value is 64.400000000000005684341886080801486968994140625, so the most exact BigDecimal representation would be:

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

IMO, 64.4.to_d should return just that.

Upvotes: 5

Eli Sadoff
Eli Sadoff

Reputation: 7308

This is not a bug. f == f.to_d returns false, so if f == 1.to_d * f is true, then f.to_d == 1.to_d * f must be false because f != f.to_d. The == method for BigDecimal is intended to compare BigDecimals not BigDecimal to float. Sometimes the equality will work, but for some fs the BigDecimal representation is exact whereas the float is not.

Edit: See Is Floating Point Math Broken for more of an explanation.

Upvotes: 3

Related Questions