Lessons learned getting started with RubyMoney
Recently, I worked on currency conversion functionality in a Rails app and ended up running into several gotchas. For your benefit, here they are. This assumes you've installed gems money
and rails-money
.
The bank does not necessarily convert through a 3rd common currency
First point, there is an entity called the Bank that provides currency conversion functionality. It is a class that you configure to load into the Money gem. Without it, you can store and work with Money objects that hold a value and a currency, but not convert between them.
Unless all of your conversions involve your base currency on one side, you'll have to decide if you want to call your third party to make each exchange, or convert twice, through your base currency. The Bank gem does not necessarily make that decision for you. I found that one gem converted through the base currency automatically, while another did not.
Not all currencies have two decimal places
RubyMoney works with currency as integers, which makes a lot of sense. A "rough edge," however, is that the subunit (cents, pence, etc) is not immediately obvious when viewing Money instances. The gem holds its own table of currencies, where it defines how many decimal places a particular currency has. Access this table via Money::Currency.table
, and look for an attribute called subunit_to_unit
. For example, the Japanese Yen doesn't have decimal places, so its subunit_to_unit
is just 1, resulting in 100.to_money("JPY")
stored as 100. 100.to_money("USD")
is stored as 10000, because USD's subunit_to_unit
is 100.
Formatting the thousands separator correctly
Money instances can be formatted via #format
. We're using this now and I recommend this method. This uses rails-money's built-in formatting which does a lot for you. The documentation will tell you mostly everything, except an important detail about properly displaying the thousands separator -- periods or commas in 1,000,000, for instance.
Because we could have a currency that is written differently across multiple countries, thousands_separator depends on setting locale properly. Under the hood, RubyMoney looks to I18n.locale
to figure out dots vs. commas. To get this working, install the rails-i18n
gem, which will allow your site to handle a standardized set of locales, even if you don't have translations for any other language but your default.
Use #to_money to handle user input
As established, different currencies have different decimal places. Always use #to_money
to parse decimal places correctly. ActiveRecord attribute assignment does this automatically, but otherwise I found myself wary. See below:
[5] pry(main)> '100.00'.to_money
=> #<Money fractional:10000 currency:USD>
[6] pry(main)> Money.new(100.0, "USD")
=> #<Money fractional:100 currency:USD>
[9] pry(main)> SomeModel.new(some_currency_column: 100.0)
=> #<SomeModel:0x0055eff4c3ef08 id: nil, some_currency_column_cents: 10000, some_currency_column_currency: "USD">
Summation uses the first currency in the list
When summing values represented in mixed currencies, we realized we needed to know the currency of the final sum. It appears it's just the currency of the first Money instance in the list. See below:
[1] pry(main)> Money.new(100, "USD") + Money.new(100, "EUR")
=> #<Money fractional:212 currency:USD>
[2] pry(main)> Money.new(100, "EUR") + Money.new(100, "USD")
=> #<Money fractional:189 currency:EUR>
[3] pry(main)> Money.new(100, "EUR") + Money.new(100, "USD") + Money.new(100, "USD")
=> #<Money fractional:278 currency:EUR>
[4] pry(main)> Money.new(100, "USD") + Money.new(100, "EUR") + Money.new(100, "EUR")
=> #<Money fractional:324 currency:USD>
Hope this helps.