Working with decimals in Elixir

Integers are not enough, and floats are flawed? Decimals to the rescue! A short guide of what’s important when working with decimals in Elixir.

This post is about the Decimal 2.0 module from decimal Hex package.

As with every module in Elixir, running h Module and Module.module_info in IEx is a good place to start.

Among other things, it will tell us that the Decimal module follows the following standards:

Installation

Decimals are not part of the stdlib, and so before using the Decimal module, we need to declare is as a dependency in mix.exs:

def deps do
  [{:decimal, "~> 2.0"}]
end

There was some discussion of inclusion in the standard library, but the Elixir core team is reluctant to add something that works well as a separate module.

Contexts

Since using decimals is about getting the right precision in the first place, there is a little setting called context, which can be changed on the fly.

Decimal.Context.get/0 revels what’s our current settings:

iex(1)> Decimal.Context.get()
%Decimal.Context{
  flags: [],
  precision: 28,
  rounding: :half_up,
  traps: [:invalid_operation, :division_by_zero]
}

You’ll probably care about the maximum precision of calculations and how rounding is done.

Decimal.Context.set/1 can set different values. It accepts the %Decimal.Context struct.

Creation and conversion

We can create a decimal with new/1 or from_float/1 functions:

iex(1)> Decimal.new(55)
#Decimal<55>
iex(2)> Decimal.from_float(20.5)
#Decimal<20.5>

The other way around, we can use to_integer/1 and to_float/2.

To check something is actually indeed a decimal:

iex(8)> require Decimal
Decimal
iex(9)> Decimal.is_decimal("2.5")
false
iex(10)> Decimal.is_decimal(Decimal.new(2))
true

is_decimal/1 is a macro; therefore, we have to require the Decimal module first.

Addition and subtraction

We can use add/2 to get the sum of two decimals:

iex(1)> Decimal.add(6, 7)
#Decimal<13>
iex(2)> Decimal.add(Decimal.new(6), Decimal.from_float(7.5))
#Decimal<13.5>
iex(3)> Decimal.add(Decimal.new(6), "7.5")
#Decimal<13.5>

Note that instead of using the from_float/1 function, we can pass a string (“7.5”). We can do that in any of the Decimal functions on this page.

Similarly we can substract a value from a decimal with sub/2 function:

iex(1)> Decimal.sub(Decimal.new(6), 7)
#Decimal<-1>

Multiplication and division

We can multiply with mult/2:

iex(1)> Decimal.mult(Decimal.new(5), Decimal.new(3))
#Decimal<15>
iex(2)> Decimal.mult("Inf", -1)
#Decimal<-Infinity>

And devide with div/2:

iex(1)> Decimal.div(1, 3)
#Decimal<0.3333333333333333333333333333>
iex(2)> Decimal.div(1, 0)
** (Decimal.Error) division_by_zero
    (decimal) lib/decimal.ex:481: Decimal.div/2

Nobody solved the division by zero, so all you get is Decimal.Error.

Using contexts, we could change the precision to one we care about like this:

iex(1)> Decimal.Context.set(%Decimal.Context{Decimal.Context.get() | precision: 9})
:ok
iex(2)> Decimal.div(1, 3)
#Decimal<0.333333333>

Comparison

It’s super important not to expect that comparisons with < and > signs would work with Decimal.

Instead, we can use equal?/2 and compare/2 functions.

Checking equality is easy:

iex(1)> Decimal.equal?(-1, 0)
false
iex(2)> Decimal.equal?(0, "0.0")
true

Comparison returns one of :lt, :gt, and :eq atoms:

iex(1)> Decimal.compare(-1, 0)
:lt
iex(2)> Decimal.compare(0, -1)
:gt
iex(3)> Decimal.compare(0, 0)
:eq

Alternatively, there are gt?/2, lt?/2, min/2, max/2 functions as well.

← BUY THE PRE-RELEASE

I am writing a complete guide on web application deployment. Ruby with Puma, Python with Gunicorn, NGINX, PostgreSQL, Redis, networking, processes, systemd, backups, and all your usual suspects. 23 chapters and 3 scripted demonstrations already available.

More →