views:

7215

answers:

4

I'm in the process of learning Ruby on Rails and I've set myself the task of putting together a very basic shopping cart system. I have a table items that costs of a price column currently set to integer. I have no problem with inputting the data in cent, but when it comes to displaying the price in th view, well I obviously want it to be in Euros & cent. Can anyone tell me what the best currency/money handling practice is with RoR? An example?

+8  A: 

Common practice for handling currency is to use decimal type. Here is a simple example from "Agile Web Development with Rails"

add_column :products, :price, :decimal, :precision => 8, :scale => 2

This will allow you to handle prices from -999,999.99 to 999,999.99
You may also want to include a validation in your items like

def validate 
  errors.add(:price, "should be at least 0.01") if price.nil? || price < 0.01 
end

to sanity-check your values.

neutrino
This solution also enables you to use SQL sum and friends.
Larry K
+25  A: 

You'll probably want to use a DECIMAL type in your database. In your migration, do something like this:

# precision is the total amount of digits
# scale is the number of digits right of the decimal point
add_column :items, :price, :decimal, :precision => 8, :scale => 2

In Rails, the :decimal type is returned as BigDecimal, which is great for price calculation.

If you insist on using integers, you will have to manually convert to and from BigDecimals everywhere, which is probably a pain in the ass.

As pointed out by mcl, to print the price, use:

number_to_currency(price, :unit => "€")
#=> €1,234.01
molf
Thanks for the advice neutrino and molf! I have one follow up question - how can I display my prices with the accuracy of two decimal places using BigDecimal? Currently the `item.price` is returning "12.5" instead of "12.50" ...
Barry Gallagher
Use number_to_currency helper, more info at http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#M001969
mcl
Thanks for your help guys!
Barry Gallagher
Actually, it's much safer and easier to use an integer in combination with acts_as_dollars. Have you ever been bitten by floating-point comparison? If not, don't make this your first experience. :) With acts_as_dollars, you put stuff in in 12.34 format, it's stored as 1234, and it comes out as 12.34.
Sarah Mei
There is also a Money gem, the Money class which can be stored as a text column using serialized or composed_of
Kris
@Sarah Mei: BigDecimals + decimal column format avoids precisely that.
molf
+6  A: 

Just last week I dealt with this issue. I have a class FinancialDocument with an amount attribute which is an integer. I recommend storing money in cents in an integer column. It's trivially easy to multiply and divide by 100 and it prevents certain kinds of errors.

In my show view, I format the amount like this:

number_to_currency financial_document.amount.to_f / 100

In my add and edit views I offer dollars and cents inputs. That makes it really easy to ensure users only enter valid money amounts. I added these methods to the FinancialDocument model to help display preexisting values in those inputs:

def format_dollars
  if amount && amount > 0
    (amount / 100).floor()
  else
    ''
  end
end

def format_cents
  if amount && amount > 0
    amount % 100
  else
    '00'
  end
end

As mentioned, the model attribute is amount but the user enters amount_dollars and amount_cents. How does that work?

I added this to the model to handle the incomming money data:

# Declare attributes to handle dollars and cents, even though 
# they aren't DB columns.
attr_accessor :amount_dollars, :amount_cents

# Declare a Rails `before_save` callback method. ActiveRecord 
# will call this method every time it saves an instance of the 
# model.
before_save :set_amount_based_on_dollars_and_cents

def set_amount_based_on_dollars_and_cents
  d = 0
  c = 0
  d = @amount_dollars.to_i unless @amount_dollars.blank?
  c = @amount_cents.to_i unless @amount_cents.blank?
  self.amount = (d * 100) + c
end
Ethan
+2  A: 

Here's a fine, simple approach that leverages composed_of (part of ActiveRecord, using the ValueObject pattern) and the Money gem

You'll need

  • The Money gem (version 3.1.0)
  • A model, for example Product
  • An integer column in your model (and database), for example :price

Write this in your product.rb file:

  class Product < ActiveRecord::Base

  composed_of :price,
              :class_name => 'Money',
              :mapping => %w(price cents),
              :converter => Proc.new { |value| value.respond_to?(:to_money) ? value.to_money : Money.empty }

...

What you'll get:

  • Without any extra changes, all of your forms will show dollars and cents, but the internal representation is still just cents. The forms will accept values like "$12,034.95" and convert it for you. There's no need to add extra handlers or attributes to your model, or helpers in your view.
  • product.price = "$12.00" automatically converts to the Money class
  • product.price.to_s displays a decimal formatted number ("1234.00")
  • product.price.formatted displays a properly formatted string for the currency
  • If you need to send cents (to a payment gateway that wants pennies), product.price.cents.to_s
  • Currency conversion for free
Ken Mayer