Notes to self

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.

Check out my book
Deployment from Scratch is unique Linux book about web application deployment. Learn how deployment works from the first principles rather than YAML files of a specific tool.
by Josef Strzibny
RSS