Rails-Money Gem and to_f
Recently I came across an interesting problem that involved using the to_f method and the problems it can bring, especially when dealing with monetary values.
My problem involved the inexact arithmetic used in to_f in Ruby, adding three numbers together, and comparing them against a single total number. If the two totals were the same then returning true (which continued the process) or false if they did not match and returning an error. All numbers were manually added in as floats.
A total was put in (x). The three numbers to be summed were put in. The three numbers were summed (y). The two totals (x) and (y) equaled the same, but an error was being thrown. Time to investigate.
This looked like it should work. All amounts were being captured on the front-end and were in a string. For example, if the total_amount was ‘1000.38’, .to_f simply converts it from a string to a float. Then I checked the total being returned from the summed numbers.
The summed total = 4065.9700000000003… But how? The two numbers were 2439.48 and 1626.49. How were these being summed to the number shown? I quickly checked in my spotlight tool on my mac and 4065.97 was being returned.
It has to do with how Ruby specifically deals with storing decimal numbers. Due to floats' different arithmetic and the use of a fixed number of binary digits to represent floats, we find our issue! Ruby converts floating numbers from decimal to binary and back again, so some decimal numbers cannot be represented accurately. To be allocated accurately you may need to clear all your space in your RAM and then some.
Here is a link I found on another blog that may help explain it in more detail -Artimetic Float Link.
So we found the issue, the summing of the numbers is being inaccurately represented in Ruby. Now to find the best fix.
Looking around for a fix, I found two possibilities. Firstly, I could simply use the round method to keep the total to a fixed number of decimals I choose.
This would remove my issue of a long decimal. Simple.
But then I found the Money gem and more specifically the money-rails gem. The money gem adapted for use in rails. The money gem was created for dealing with money and currency conversion. This provided an object-oriented approach to dealing with currency.
Here are 5 points I found in another blog that lists some of the main benefits of using this gem.
moneyclass that holds relevant monetary information, such as the value, currency, and decimal marks.
- Another class called
Money::Currencythat wraps information regarding the monetary unit being used by the developer.
- The ability to exchange money from one currency to another, which is super cool.
- The high flexibility offered by object-oriented structures.
This gem gives you huge flexibility when dealing with currency, from converting it to another currency to choosing the rate of conversion to a different currency.
Moving to my issue, I found an interesting problem that could occur without reading the documentation carefully. My totals were being returned in a String but the input already had a decimal place as well. So ‘1626.49’ would be converted to 1626.49 by to_f. But the money-rails gem doesn’t talk in detail about dealing with a float number (it specifically tries to deal with integers to avoid the float issue). Typically money-rails will round up to the nearest cent hence why I was seeing this number being rounded to 1626 when using Money.new as it would not deal with the decimal places (you need a different method to deal with subunits).
Note: This meant I was receiving a money object containing 1626 pennies! So if I then put .format on the end, I would receive £16.26 back not £1626.
I was then comparing my two totals and getting a false positive. Why? Because both totals were being rounded and compared, this could lead to incorrect totals being compared, and due to rounding the method still returning true (something we definitely wouldn’t want).
This meant using a different method instead called from_amount which would allow me to deal with the decimal numbers.
But this leads to another issue (importance of checking work). As mentioned, the params were returning the input amounts as a String. The .from_amount does not deal with string values, only numerical ones.
As you can see this isn’t ideal. If you had simply tried to add the three amounts together as strings then you would end up with a long string of numbers with one added on to the end of the last. However, it is representing my currency and total accurately and is stored in an object allowing me to manipulate it more easily.
The to_f issue made me dig deeper into the best ways of dealing with money as well as the safest. The money / money-rails gems were explicitly built to deal with currency in an OOP way. I would recommend looking into it further if you want to build something dealing with currency. Just make sure you are checking your outputs are being dealt with correctly!