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.