Currency Calculations in Ruby

You're doing some currency calculations in your app. It seems to be working well. But after a while, strange discrepancies emerge. The books stop balancing. People get mad. All because the code treated currency like any other number. In this article, Julio Sampaio shows us which of Ruby's number classes are unsuitable for currency, and walks us through better options.

Money, regardless of the currency it is in, seems like a floating-point number. But it's a mistake to use floats for currency.

Float numbers (hence, float objects) are, by definition, inexact real numbers that make use of the double-precision floating-point representation characteristic of the native architecture. Inexact numbers make accountants unhappy.

In this article, you’ll be guided through some quick examples that will help you to address the available options for dealing with money data in Ruby and Rails.

What's a Float?

As we said, float objects have different arithmetic. This is the main reason they are inexact numbers, especially because Ruby (like most languages) uses a fixed number of binary digits to represent floats. In other words, Ruby converts floating numbers from decimal to binary and vice versa.

When you dive deep into the binary representation of real numbers (aka decimal numbers), some of them can't be exactly represented, so the only remaining option is for the system to round them off.

If you think ahead and consider common math structures, such as periodic tithes, you may understand that they can't be entirely represented within a fixed number since the pi number, for example, is infinite. Floats can usually hold up to 32 or 64 bits of precision, which means the number will be cut off when it reaches the limit.

Let’s analyze a classic example:

1200 * (14.0/100)

This is a straightforward way to calculate the percentage of a number. Fourteen percent of 1200 should be 168; however, the result of this execution within Ruby will be

1200 * (14.0/100)
=> 168.00000000000003

However, if you add just 0.1% to the formula, you get something different:

1200 * (14.1/100)
=> 169.2

Alternatively, you could round the value to the nearest possible one, defining how many decimal places are desired:

(my_calculation).round(2)

Indeed, it is not guaranteed when it comes to more complex calculations, especially if you perform comparisons of these values.

If you're interested in understanding the real science behind it, I highly recommend reading the Oracle's appendix: What Every Computer Scientist Should Know About Floating-Point Arithmetic. It explains, in detail, the whys behind the inaccurate nature of float numbers.

The Trustworthy BigDecimal

Consider the following code snippet:

require "bigdecimal"
BigDecimal("45.99")

This code can easily represent a real logic embracing an eCommerce cart’s amount. In the end, the real value being manipulated will always be 45.99 instead of 45.9989 or 45.99000009, for example.

This is the precise nature of BigDecimal. For usual arithmetic calculations, float will perform the same way; however, it is unpredictable, which is the danger of using it.

When it's run with BigDecimal, the same percentage calculation we did in the previous section results in

require "bigdecimal"
(BigDecimal(1200) * (BigDecimal(14)/BigDecimal(100))).to_s("F")
=> 168.0

This is just a short version to allow rapid execution in an irb console.

Originally, when you print the direct BigDecimal object, you’ll get its scientific notation, which is not what we want here. The to_s method receives the given argument due to formatting settings and displays the equivalent floating value of the BigDecimal. For further details on this topic, refer to Ruby docs.

In case you need to determine a limit for decimal places, it has the truncate method, which will do the job for you:

(BigDecimal(1200) * (BigDecimal("14.12")/BigDecimal(100))).truncate(2).to_s("F")
=> 169.44

The RubyMoney Project

RubyMoney was created after thinking about these problems. It is an open-source community of Ruby developers aiming to facilitate developers' lives by providing great libraries to manipulate money data in the Ruby ecosystem.

The project is composed of seven libraries, three of which stand out in importance:

  • Money: A Ruby library for dealing with money and currency conversion. It provides several object-oriented options to handle money in robust and modern applications, regardless of whether they are for the web.
  • Money-rails: An integration of RubyMoney for Ruby on Rails, mixing all the money's library power with Rails flexibility.
  • Monetize: A library for converting various objects into money objects. It works more like an auxiliary library for applications that deal with a lot of String parsing, for example.

The project has four other interesting libraries:

  • EUcentralbank: A library that helps calculate exchange rates by making use of published rates from the European Central Bank.
  • Google_currency: An interesting library for currency conversion using Google Currency rates as a reference.
  • Money-collection: An auxiliary library for accurately calculating the sum/min/max of money objects.
  • Money-heuristics: A module for heuristic analyses of string input for the money gem.

The “Money” Gem

Let’s start with the most famous one: the money gem. Among its main features are the following:

  • A money class that holds relevant monetary information, such as the value, currency, and decimal marks.
  • Another class called Money::Currency that wraps information regarding the monetary unit being used by the developer.
  • By default, it works with integers rather than floating-point numbers to avoid the aforementioned errors.
  • The ability to exchange money from one currency to another, which is super cool.

Other than that, we also get the high flexibility offered by consistent and object-oriented structures to manipulate money data, just like any other model within your projects.

Its usage is pretty simple, just install the proper gem:

gem install money

A quick example involving a fixed amount of money would be

my_money = Money.new(1200, "USD")
my_money.cents #=> 1200
my_money.currency #=> Currency.new("USD")

As you can see, money is represented based on cents. Twelve hundred cents is equivalent to 12 dollars.

Just like you did with BigDecimal, you can also play around and do some basic math with these objects. For example,

cart_amount = Money.new(10000, "USD") #=> 100 USD
discount = Money.new(1000, "USD") #=> 10 USD

cart_amount - discount == Money.new(9000, "USD") #=> 90 USD

Interesting, isn’t it? That’s the nature of the objects we mentioned. When coding, it really feels like you’re manipulating monetary values rather than inexpressive and ordinary numbers.

Currency Conversions

If you’ve got your own exchange rate system, you can perform currency conversions through an exchange bank object. Consider the following:

Money.add_rate("USD", "BRL", 5.23995)
Money.add_rate("BRL", "USD", 0.19111)

Whenever you need to exchange values between them, you may run the following code:

Money.us_dollar(100).exchange_to("BRL") #=> Money.new(523, "BRL")

The same applies to any arithmetic and comparison evaluations you may want to perform.

Make sure to refer to the docs for more of the provided currency attributes, such as iso_code (which returns the international three-digit code of that currency) and decimal_mark (the char between the value and the fraction of the money data), among others.

Oh, I almost forgot; once you’ve installed the money gem, you can access a BigDecimal method called to_money that automatically performs the conversion for you.

The “monetize” gem

It is important to understand the role each library plays within the RubyMoney project. Whenever you need to convert a different Ruby object (a String, for example) into Money, then monetize is what you’re looking for.

First, make sure to install the gem dependency or add it to your Gemfile:

gem install monetize

Obviously, money also needs to be installed.

The parse method is also very useful when you receive money data in a different format; for example,

Money.parse("£100") == Money.new(100, "GBP") #=> true

Although the scenarios in which you’d use this parsing method are restricted, it can be very useful when you receive a value formatted alongside its currency code from an HTTP request. On the web, everything is text, so converting from string to money can be very useful.

However, be careful with how your system manipulates the values and if they can be hacked somehow. Financial systems are always covered by multiple security layers to ensure that the value you’re receiving is the real value of that transaction.

The “monetize-rails” gem

This is the library that deals with the same money manipulation operations, but within a Rails app.

Why do we need a second library just to make it work alongside Rails? Well, you can certainly make use of the money gem alone within Rails projects for ordinary math operations. However, it won’t work properly when your Rails structures need to communicate with money’s features.

Consider the following example:

class Cart < ActiveRecord::Base
  monetize :amount_cents
end

This is a real, functional Rails model object. You can use it along with databases (even including aliases when you want a different model attribute name), Mongoid, REST web services, etc.

All the features we’ve been in contact with so far also apply to this gem. Usually, only additional settings are necessary to make it run, which should be placed into the config/initializers/money.rb file:

MoneyRails.configure do |config|

  # set the default currency
  config.default_currency = :usd

end

This will set the default currency to the one you provide. However, during development, the chances are that you may need to perform exchange conversions or handle more than one currency throughout the models.

If so, money-rails allows us to configure a model-level currency definition:

class Cart < ActiveRecord::Base

  # Use GPB as model level currency
  register_currency :eur

  monetize :amount_cents, as "amount"
  monetize :discount_cents, with_currency: :eur

  monetize :converted_value, with_currency: :usd

end

Note that once everything is set up, it is really easy to make use of money types alongside your projects.

Wrapping Up

In this blog post, we’ve explored some available options to deal with money values within the Ruby and Rails ecosystems. Some important points are summarized below:

  • If you’re dealing with the calculation of float numbers, especially if they represent money data, go for BigDecimal or Money instances.
  • Try to stick to one system only to avoid further inconsistencies alongside your development.
  • The money library is the core of the whole RubyMoney system, and it is very robust and straightforward. Money-rails is the equivalent version for Rails applications, and monetize is necessary whenever you need to parse from any value to Money objects.
  • Avoid using Float. Even if your app doesn’t need to calculate anything now, the chances are that an unadvised dev will do it in the future. You might not be there to stop it.

Remember, the official docs should always be a must-to. BigDecimal is filled with great explanations and examples of its usage, and the same is true of RubyMoney gem projects.

author photo

Julio Sampaio

Julio is responsible for all aspects of software development such as backend, frontend, and user relationship at his current company. He graduated in Analysis and System Development and is currently enrolled in a postgraduate software engineering course.


“We’ve looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release.”
Michael Smith
Try Error Monitoring Free for 15 Days
Are you using Bugsnag, Rollbar, or Airbrake for your monitoring? Honeybadger includes exception, uptime, and check-in monitoring — all for probably less than you’re paying now. Discover why so many companies are switching to Honeybadger here.
Try Error Monitoring Free for 15 Days
Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
Try Error Monitoring Free for 15 Days
"Wow — Customers are blown away that I email them so quickly after an error."
Chris Patton
Try Error Monitoring Free for 15 Days