0

I recently came across this weird bug in Rails.

class PaymentsController < ApplicationController

  def new
    @payment = current_user.payments.build(:invoice_id => params[:invoice_id])
    @title = "Make payment" 
  end

end

class Payment < ActiveRecord::Base

  attr_accessible :amount, :date, :invoice_id

  after_initialize :set_amount

  private

  def set_amount
    if new_record? && invoice.present?
      self.amount ||= invoice.amount_payable
    end
  end

end

When I call this action from an invoice like so...

<%= link_to "Make payment", new_payment_path(:invoice_id => invoice.id) %>

...the payment form appears with the correct invoice preselected in the dropdown field (which is correct).

The payment amount is populated with the correct amount in about 90% of all cases.

However, sometimes it is not populated with the 2-digit amount_payable from the database, but rather some other weird value such as:

87.31999999999999

(where 87.32 is the decimal type value stored in the SQLite database)

Can somebody tell me what is causing this rounding error or point me in the right direction?

Thanks for any help.

By the way, this is my database schema:

create_table "invoices", :force => true do |t|
  t.decimal  "amount_payable", :precision => 8, :scale => 2
end
Tintin81
  • 9,821
  • 20
  • 85
  • 178
  • Out of curiosity, do you have any validations that set the integer to round to two decimal places? – thank_you Mar 31 '13 at 18:22
  • What happens if you use the `round(2)` method within your validation? Do you still get back the decimal error? – thank_you Mar 31 '13 at 18:34
  • I just checked that but I it seems I am not using any rounding validations anywhere in my app. Thanks for your help, though! – Tintin81 Mar 31 '13 at 18:49

1 Answers1

5

You should never store Money as floats --- what's going on here is floating point arithmetic, and it's very common for calculations with money.

A better point is to store everything as integer for cents. So, you may have in your database a "amount_in_cents" column that is an integer.

Then, you add a getter/setter for 'amount' so you can use #amount all over the place.

class Payment < ActiveRecord::Base

  def amount
    amount_in_cents / 100.0
  end

  def amount=(other)
    self.amount_in_cents = (other * 100).to_i
  end
end
Jesse Wolgamott
  • 40,197
  • 4
  • 83
  • 109
  • This doesn't shoot the problem exactly, but indeed looks like an elegant solution at many places. – Nerve Mar 31 '13 at 18:40
  • OK, sound reasonable. I have to admit that I haven't done much Rails programming before. But how come they store money values as floats all over the place, even in some Rails tutorial books that I read? I suppose they do it for the sake of simplicity? – Tintin81 Mar 31 '13 at 18:48
  • @Tintin81 yes it's for simplicity's sake. The money gem suggests this, as well as these articles: http://railsforum.com/viewtopic.php?id=39211 http://stackoverflow.com/questions/1019939/ruby-on-rails-best-method-of-handling-currency-money http://www.ruby-forum.com/topic/48754 http://vladzloteanu.wordpress.com/2010/01/11/why-you-shouldnt-use-float-for-currency-floating-point-issues-explained-for-ruby-and-ror/ http://qugstart.com/blog/ruby-and-rails/ruby-floats-bigdecimals-and-money-currency/ http://www.peterboling.com/2009/11/18/rails-money-float-decimal – Jesse Wolgamott Mar 31 '13 at 18:55
  • I just realised that I probably misread your answer. Indeed I am NOT using floats to store my money values but Rails `decimal` type. Some of the articles your posted above suggest to use exactly that type, so I still don't get why I am getting to rounding errors. I will post an excerpt of my database schema above in a second. – Tintin81 Mar 31 '13 at 19:10
  • @Tintin81 you should be using Integers. end of story. – Jesse Wolgamott Mar 31 '13 at 22:45
  • Just out of curiosity: Should I change the field type of `quantity` and `tax_rate` as well? – Tintin81 Apr 01 '13 at 09:22
  • @Tintin81 I would have quantity as integer unless you can purchase partial quantities. For TaxRate, I would store as a decimal – Jesse Wolgamott Apr 01 '13 at 23:22
  • OK, so I was on the right track by storing `tax_rate` as decimal. I guess that format is easier to handle because it will never exceed 5 characters. In my case, products *can* be purchases in partial quantities, so I guess storing `quantity` as an integer is the way to go? Right now, I am storing it as decimal for reasons of simplicity and it works pretty well so far. – Tintin81 Apr 02 '13 at 10:02
  • quantity as an integer makes sense for me, but it's not as important as the currency column. – Jesse Wolgamott Apr 02 '13 at 13:52