This article was originally written by Julio Sampaio on the Honeybadger Developer Blog.
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:
- EU_central_bank: 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
orMoney
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, andmonetize
is necessary whenever you need to parse from any value toMoney
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.
Top comments (0)