Sanjibukai

Sanjibukai

How to handle small amounts/rates/fees when using an integer (e.g. like Money)

Hello everybody,

In order to deal with currencies, we are used to consider the amounts as cents and then using integers.
One can also simply use the Decimal type which will avoid rounding errors of floats.
Another solution can be to use a package that deal with the currency problem.

In elixir there is two main package: money and ex_money

But I have a question that always intrigued me… And it’s somehow introduced in the 6th falsehood that is documented in this list (from the ex_money doc):

Prices can’t have more precision than the smaller sub-unit of the currency. (counter-example: gas prices)

Indeed, while we are ultimately dealing with at least the cent in our day to day life, we can however face smaller amounts, like tenth of cents in gas prices or some fees (like sending a message that can cost 0.001$)
Let’s take that last example…
If I have to send an email every day, I expect to pay 3 cents at the end of the month (considering 30 days).

But what if I’m using a cumulative value?
The first day, the amount will still be 0 in cents (using an integer)., right?
So if I had to sum each day fee, I’ll never be able to sum the whole charge.

How those kind of little amounts are actually handled?

Edit: I noticed that the money package is using the integer type for the amount part, while ex_money is using a decimal type (to be exact it’s a numeric type in Postgres, but I guess it’s the same).

Does it mean that with ex_money I can store an amount like $0.001 while displaying $0, and still having the correct amount stored in the system?
I mean on day 2 the value will be correctly stored as $0.002 but still displayed $0…
Until the day 10 when the value stored will be $0.010 and hence correctly displayed as $0.01.

Did I correctly understand how the Decimal-like type will behave in ex_money?

Most Liked

kip

kip

ex_cldr Core Team

In ex_money (I’m the author) this is one reason for using decimal. Rounding to the currencies precision (and for some currencies and usages to a multiple) is deferred until the latest possible moment.

Using your example, its perfectly acceptable to have arbitrary precision on money arithmetic. This is relevant not only at the gas pump but in financial trading as well. So maybe I have a value $3.45678. Perfectly OK and in factor in financial instruments a requirement to at least 7 digits if I recall (it may not be that exactly).

Another example is when doing currency conversion - higher precision is desirable.

The at some point the result is credited to a bank account which means applying the appropriate rounding. There are at least three considerations:

  1. Rounding to the right number of decimal digits. 0,1,2, 3 and 4 digits are in use with different currencies around the world.

  2. Use the right rounding mode. Typically you would use bankers rounding which, in the Decimal library is mode :half_even and is also the default in ex_money.

  3. Apply any necessary round nearest for the currency. For example, in Australia there is no cash amount below 5c so you need to round to the nearest 5c for cash amounts. Similar for the Swiss franc.

Last example. Lets say you have $5.23 and you want to split it 3 ways.

hex> m = Money.new(:USD, "5.23")
#Money<:USD, 5.23>
iex> Money.div m, 3
{:ok, #Money<:USD, 1.743333333333333333333333333>}
iex> Money.div!(m, 3) |> Money.round
#Money<:USD, 1.74>
iex> Money.div!(m, 3) |> Money.round |> Money.mult!(3)
#Money<:USD, 5.22>

Where did the last cent go? Thats the reason I implemented Money.split/2.

iex> Money.split(m, 3)
{#Money<:USD, 1.74>, #Money<:USD, 0.01>}

Which allows you to decide what you do with the amount left over. Oh, and Money.split/2 takes an arbitrary precision money amount, rounds it correctly and does the math.

I don’t recommend using integers for money amounts for all of these reasons.

kip

kip

ex_cldr Core Team

ex_money will allow you to create, store, retrieve and do math on arbitrary precision money amounts.

It will correctly round the amount appropriate to the currency whenever you need that value. Calling Money.to_string/2 will apply the appropriate rounding, for example.

So yes, in your example, it would be appropriate to have the money amounts be arbitrary in precision and then only round on output.

You will still need to handle the issue of displaying rounded numbers that don’t add exactly to a rounded total (when viewed in a spreadsheet for example. In this case its probably better to round the numbers first and then sum.

kip

kip

ex_cldr Core Team

Updated the docs for ex_money since it is a major version change. I left the ex_money_sql docs alone since hex will pick up the latest version automatically.

Just ex_money_sql is enough as you found. Normally better, in my opinion, to let Hex resolve transitive dependencies rather than try to configure them yourself.

Its going to be basically the same or very slightly slower since the database does have to check each row it is summing to make sure they are the same currency. I favour correctness in this case over absolute speed.

Yes, its the same under the hood.

Here’s an example (which I’ve added as a test in the repo too):

query = from o in Organization,
          where: fragment("currency_code(payroll)") == "USD",
          select: sum(o.payroll)

Basically you have to wrap the composite value component in a fragment since its not natively supported in Ecto since its not cross-db compatible syntax. In this example, currency_code is the component of the composite field payroll that contains the currency code. In your schema you would have declared:

field :payroll, Money.Ecto.Composite.Type

Where Next?

Popular in Questions Top

gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
New
dokuzbir
Hello, I am trying to convert my lists to string without losing brackets.For start i have 3 map. They look like these buyer = %{ id: ...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Other popular topics Top

JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
myronmarston
The Elixir Typespec docs show the following syntax for keyword lists in typespecs: # ... | [key: type] # keyword lis...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
baxterw3b
Hi guys, i’m new in the Elixir world, and i have to say, that i love it! i’m having some problem to understand anonymous functions with ...
New
AstonJ
by Lance Halvorsen Elixir and Phoenix are generating tremendous excitement as an unbeatable platform for building modern web application...
460 27162 124
New
Nvim
Elixir appears to be a superior language to Python. I don’t see any advantage of Python over Elixir. Are there any?
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New

We're in Beta

About us Mission Statement