开发者

Ruby Float Round Error Bug?

I am getting the following rounding error when I try to Unit test the class below:

class TypeTotal
    attr_reader  :cr_amount,  :dr_amount,
                 :cr_count,  :dr_count

    def initialize()
        @cr_amount=Float(0);    @dr_amount=Float(0)
        @cr_count=0;            @dr_count= 0
    end

    def increment(is_a_credit, amount, count=1)
        case is_a_credit        
         when true
            @cr_amount = 开发者_JS百科Float(amount)+ Float(@cr_amount)
            @cr_count += count
         when false
            @dr_amount = Float(amount)+ Float(@dr_amount)
            @dr_count += count
        end
    end
end

Unit Test:

require_relative 'total_type'
require 'test/unit'

class TestTotalType < Test::Unit::TestCase 
  #rounding error
  def test_increment_count()
    t = TypeTotal.new()
       t.increment(false, 22.22, 2)       
       t.increment(false, 7.31, 3) 
     assert_equal(t.dr_amount, 29.53)    
  end
end

Output:

  1) Failure:
test_increment_count(TestTotalType) [total_type_test.rb:10]:
<29.529999999999998> expected but was
<29.53>.

1 tests, 1 assertions, 1 failures, 0 errors, 0 skips

I am using Floats because it was recommended in the Pick Ax book for dollar values because they shouldn't be effective by round errors.

I am running on ruby 1.9.2p290 (2011-07-09) [i386-mingw32] on Windows 7 64-bit Home and Windows XP 32-bit Pro.

I've tried

  • casting my variables to floats
  • removing += and spelling out increment

The behavior appears random:

  • 12.22 + 7.31 works
  • 11.11 + 7.31 doesn't work
  • 11.111 + 7.31 works

Any ideas whats going wrong?


Are you sure that was the advice given? I'd expect the advice to be not to use Floats, precisely because they use binary floating point arithmetic, and so are prone to rounding errors. From the Float documentation:

Float objects represent inexact real numbers using the native architecture's double-precision floating point representation.

If you could quote the exact advice you're referring to, that would help.

I would suggest you use BigDecimal instead, or use an integer with implicit units of "cents" or "hundreds of a cent" or something similar.


The problems with float are already mentioned.

When you test with floats, you should not use assert_equal but assert_in_delta.

Example:

require 'test/unit'

class TestTotalType < Test::Unit::TestCase 
  TOLERANCE = 1E-10 #or another (small) value

  #rounding error
  def test_increment_count()
    t = TypeTotal.new()
       t.increment(false, 22.22, 2)       
       t.increment(false, 7.31, 3) 
     #~ assert_equal(t.dr_amount, 29.53)  #may detect float problems
     assert_in_delta(t.dr_amount, 29.53, TOLERANCE)    
  end
end        


The solution was to use Big Decimal:

require 'bigdecimal'

class TypeTotal
    attr_reader  :cr_amount,  :dr_amount,
                 :cr_count,  :dr_count

    def initialize()
        @cr_amount=BigDecimal.new("0");  @cr_count=0,
        @dr_amount=BigDecimal.new("0");  @dr_count=0

    end

    def increment(is_a_credit, amount, count=1)
        bd_amount = BigDecimal.new(amount)
        case is_a_credit        
         when true
            @cr_amount= bd_amount.add(@cr_amount, 14)
            @cr_count += count
         when false
            @dr_amount= bd_amount.add(@dr_amount, 14)
            @dr_count = count
        end
    end

The Pick Ax (p53) book used float for currency as an example but had a foot note explaining that you need to either add .5 cent to when you display the value or use Big Decimal.

Thanks for are your help!

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜