348

I'm working on a very basic shopping cart system.

I have a table items that has a column price of type integer.

I'm having trouble displaying the price value in my views for prices that include both Euros and cents. Am I missing something obvious as far as handling currency in the Rails framework is concerned?

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Barry Gallagher
  • 6,156
  • 4
  • 26
  • 30
  • if someone uses sql, then `DECIMAL(19, 4)` **is a popular choice** check [this](http://stackoverflow.com/questions/224462/storing-money-in-a-decimal-column-what-precision-and-scale) also check [here](http://www.thefinancials.com/Default.aspx?SubSectionID=curformat) World Currency Formats to decide how many decimal places to use , hope helps. – Shaiju T Oct 28 '15 at 12:52

13 Answers13

523

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

# precision is the total number of digits
# scale is the number of digits to the 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 will probably just become a pain.

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

number_to_currency(price, :unit => "€")
#=> €1,234.01
rlandster
  • 7,294
  • 14
  • 58
  • 96
molf
  • 73,644
  • 13
  • 135
  • 118
  • 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 Jun 19 '09 at 21:28
  • 16
    Use number_to_currency helper, more info at http://api.rubyonrails.org/classes/ActionView/Helpers/NumberHelper.html#M001969 – mlibby Jun 19 '09 at 21:45
  • 51
    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 Jun 20 '09 at 02:19
  • 3
    There is also a Money gem, the Money class which can be stored as a text column using serialized or composed_of – Kris Jun 20 '09 at 09:13
  • 51
    @Sarah Mei: BigDecimals + decimal column format avoids precisely that. – molf Jun 20 '09 at 09:42
  • 1
    I would seriously use the Money gem that Ken Mayer suggested below. Even if it's a little more than what you need now, it sets you up for future ability. It just feels right, too. – Joshua Pinter Jan 18 '12 at 18:12
  • 123
    It's important not to just copy this answer blindly - *precision 8, scale 2* gives you a maximum value of **999,999.99**. If you need a number greater than a million then increase the precision! – Jon Cairns Jan 03 '13 at 11:01
  • 27
    It's also important to not just blindly use a scale of 2 if you're handling different currencies – some north-african and arab currencies like the Omani Rial or the Tunisian Dinar have a scale of 3, so *precision 8 scale 3* is more appropriate there. – Beat Richartz Apr 17 '13 at 09:37
  • 4
    In my experience BigDecimal becomes a huge pain, when you do a lot arithmetics. The number of used significant digits shoots up, and it becomes incredibly slow (I read somewhere the performance is O(n) where n = number of digits). A simple `BigDecimal('12.34') * BigDecimal('1.19').power(5) / BigDecimal(3)` results in a BigD with 45 significant digits uses. To avoid this, you'd have to write `BigDecimal('12.34').mult(BigDecimal('1.19').power(5), 10).div(BigDecimal(3), 10)` – Sebastian May 25 '13 at 08:42
  • Also, 8.62 is temporarily "8.619999999999999" until it actually gets saved in the db which could fail regex validations. – cbron Sep 20 '13 at 17:52
  • 3
    It's much safer to use Integers to store. Check out RubyMoney Gem: https://github.com/RubyMoney/money-rails – netwire Dec 24 '13 at 19:34
  • 4
    If you're adding the precision and scale at the model generation step, you have to wrap the parameter in quotes! **Wrong:** `rails g model Item price:decimal{8,2}`. **Correct:** `rails g model Item 'price:decimal{8,2}'`. This has been fixed in the [Rails usage documentation](https://github.com/rails/rails/pull/12642/files). If you don't quote it, your migration file will look like: `t.decimal8 :price, t.decimal2 :price` and cause errors. – Dennis Jan 21 '14 at 21:37
  • 3
    Know your type conversions. If you do any kind of computation, you have to make sure there isn't an implicit conversion to Float (e.g. BigDecimal * Float => Float). I end up doing a lot of "0.05.to_d" in my code to make sure that I'm working in an all BigDecimal world. – Ken Mayer Dec 20 '14 at 18:36
122

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 4.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| Money.new(value) }
  # ...

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.format 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
Peter Nixey
  • 16,187
  • 14
  • 79
  • 133
Ken Mayer
  • 1,884
  • 1
  • 13
  • 16
  • How can I hold a balance in a currency other than USD using this approach? – Shamaoke Nov 04 '10 at 05:41
  • 17
    I love this approach. But please note: make sure your migration for 'price' in this example doesn't allow nulls and defaults to 0 lest you go insane trying to figure out why this doesn't work. – Cory Nov 30 '10 at 01:41
  • +1 if you have to support multiple currencies/pricing strategies then the money gem is priceless :) – mhenrixon Mar 29 '11 at 19:12
  • 3
    I found the [money_column gem](https://github.com/tobi/money_column) (extracted from Shopify) to be very straight forward to use...easier than the money gem, if you don't need currency conversion. – talyric Aug 29 '11 at 21:56
  • 7
    It should be noted for all those using the Money gem that the Rails core team is discussing deprecating and removing "composed_of" from the framework. I suspect the gem will be updated to handle this if it happens, but if you are looking at Rails 4.0 you should be aware of this possibility – Peer Allan Oct 04 '12 at 12:23
  • 1
    Regarding @PeerAllan's comment about the removal of `composed_of` [here](http://blog.plataformatec.com.br/2012/06/about-the-composed_of-removal/) is more detail on that as well as an alternative implementation. – HerbCSO Oct 13 '13 at 15:05
  • 3
    Also, this is really reasy using the [rails-money](https://github.com/RubyMoney/money-rails) gem. – fotanus May 05 '14 at 22:04
  • The money-rails gem works just fine with Rails 4.1.4, and without using composed_of in the model. Now using Money 6.x. – sockmonk Sep 11 '14 at 19:13
  • rails 5 now has an attributes api so you can handle things more fluently now especially types like money – BenKoshy Jun 09 '17 at 08:49
28

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.

alex.zherdev
  • 23,914
  • 8
  • 62
  • 56
19

Just a little update and a cohesion of all the answers for some aspiring juniors/beginners in RoR development that will surely come here for some explanations.

Working with money

Use :decimal to store money in the DB, as @molf suggested (and what my company uses as a golden standard when working with money).

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

Few points:

  • :decimal is going to be used as BigDecimal which solves a lot of issues.

  • precision and scale should be adjusted, depending on what you are representing

    • If you work with receiving and sending payments, precision: 8 and scale: 2 gives you 999,999.99 as the highest amount, which is fine in 90% of cases.

    • If you need to represent the value of a property or a rare car, you should use a higher precision.

    • If you work with coordinates (longitude and latitude), you will surely need a higher scale.

How to generate a migration

To generate the migration with the above content, run in terminal:

bin/rails g migration AddPriceToItems price:decimal{8-2}

or

bin/rails g migration AddPriceToItems 'price:decimal{5,2}'

as explained in this blog post.

Currency formatting

KISS the extra libraries goodbye and use built-in helpers. Use number_to_currency as @molf and @facundofarias suggested.

To play with number_to_currency helper in Rails console, send a call to the ActiveSupport's NumberHelper class in order to access the helper.

For example:

ActiveSupport::NumberHelper.number_to_currency(2_500_000.61, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")

gives the following output

2500000,61€

Check the other options of number_to_currency helper.

Where to put it

You can put it in an application helper and use it inside views for any amount.

module ApplicationHelper    
  def format_currency(amount)
    number_to_currency(amount, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")
  end
end

Or you can put it in the Item model as an instance method, and call it where you need to format the price (in views or helpers).

class Item < ActiveRecord::Base
  def format_price
    number_to_currency(price, unit: '€', precision: 2, separator: ',', delimiter: '', format: "%n%u")
  end
end

And, an example how I use the number_to_currency inside a contrroler (notice the negative_format option, used to represent refunds)

def refund_information
  amount_formatted = 
    ActionController::Base.helpers.number_to_currency(@refund.amount, negative_format: '(%u%n)')
  {
    # ...
    amount_formatted: amount_formatted,
    # ...
  }
end
Community
  • 1
  • 1
Zalom
  • 696
  • 9
  • 18
13

If you are using Postgres (and since we're in 2017 now) you might want to give their :money column type a try.

add_column :products, :price, :money, default: 0
The Whiz of Oz
  • 6,763
  • 9
  • 48
  • 85
  • 1
    Internet seems very wary of this approach. Aka folks saying "Whatever you do, DONT use the postgres money type" - [Don't use money](https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_money) You can go [down the rabbit hole here](https://stackoverflow.com/questions/15726535/which-datatype-should-be-used-for-currency), but to save yoy time none of these half dozen answers provide a confident yes to using it and all suggest alternative approaches. – Evolve Jun 27 '23 at 02:54
9

Use money-rails gem. It nicely handles money and currencies in your model and also has a bunch of helpers to format your prices.

Troggy
  • 915
  • 8
  • 18
  • Yeah, I agree with this. Generally, I handle money by storing it as cents (integer) and using a gem like acts-as-money or money (money-rails) to handle the data in-memory. Handling it in integers prevents those nasty rounding errors. E.g. 0.2 * 3 => 0.6000000000000001 This, of course, only works if you don't need to handle fractions of a cent. – Chad M Mar 02 '16 at 18:34
  • This is very nice if you are using rails. Drop it in and don't worry about the issues with a decimal column. If you use this with a view, this answer may be helpful as well: https://stackoverflow.com/questions/18898947/rails-number-field-alternative-for-decimal-values – mooreds Apr 07 '16 at 20:57
5

Using Virtual Attributes (Link to revised(paid) Railscast) you can store your price_in_cents in an integer column and add a virtual attribute price_in_dollars in your product model as a getter and setter.

# Add a price_in_cents integer column
$ rails g migration add_price_in_cents_to_products price_in_cents:integer

# Use virtual attributes in your Product model
# app/models/product.rb

def price_in_dollars
  price_in_cents.to_d/100 if price_in_cents
end

def price_in_dollars=(dollars)
  self.price_in_cents = dollars.to_d*100 if dollars.present?
end

Source: RailsCasts #016: Virtual Attributes: Virtual attributes are a clean way to add form fields that do not map directly to the database. Here I show how to handle validations, associations, and more.

Thomas Klemm
  • 10,678
  • 1
  • 51
  • 54
3

Definitely integers.

And even though BigDecimal technically exists 1.5 will still give you a pure Float in Ruby.

Rostyslav Dzinko
  • 39,424
  • 5
  • 49
  • 62
moot
  • 31
  • 1
2

If someone is using Sequel the migration would look something like:

add_column :products, :price, "decimal(8,2)"

somehow Sequel ignores :precision and :scale

(Sequel Version: sequel (3.39.0, 3.38.0))

jethroo
  • 2,086
  • 2
  • 18
  • 30
2

My underlying APIs were all using cents to represent money, and I didn't want to change that. Nor was I working with large amounts of money. So I just put this in a helper method:

sprintf("%03d", amount).insert(-3, ".")

That converts the integer to a string with at least three digits (adding leading zeroes if necessary), then inserts a decimal point before the last two digits, never using a Float. From there you can add whatever currency symbols are appropriate for your use case.

It's definitely quick and dirty, but sometimes that's just fine!

Becca Royal-Gordon
  • 17,541
  • 7
  • 56
  • 91
  • Can't believe nobody upvoted you. This was the only thing that worked to get my Money object nicely into a form such that an API can take it. (Decimal) – Code-MonKy Feb 16 '17 at 12:40
2

I am using it on this way:

number_to_currency(amount, unit: '€', precision: 2, format: "%u %n")

Of course that the currency symbol, precision, format and so on depends on each currency.

facundofarias
  • 2,973
  • 28
  • 27
1

You can pass some options to number_to_currency (a standard Rails 4 view helper):

number_to_currency(12.0, :precision => 2)
# => "$12.00"

As posted by Dylan Markow

Community
  • 1
  • 1
blnc
  • 4,384
  • 1
  • 28
  • 42
0

Simple code for Ruby & Rails

<%= number_to_currency(1234567890.50) %>

OUT PUT => $1,234,567,890.50
Dinesh Vaitage
  • 2,983
  • 19
  • 16