Skip to content
Comparable Elixir protocol
Elixir Shell
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.circleci fix CI Feb 21, 2019
config initial commit Feb 20, 2019
lib implement comparable for Date, Time, DateTime May 29, 2019
priv/img docs Feb 21, 2019
scripts
test implement comparable for Date, Time, DateTime May 29, 2019
.credo.exs dev tools Feb 21, 2019
.dialyzer_ignore better defcomparable/3 syntax, gen_ne_test/3 and gen_eq_test/3 helpers May 29, 2019
.editorconfig
.formatter.exs dev tools Feb 21, 2019
.gitignore
CHANGELOG.md v1.0.0 May 29, 2019
README.md
VERSION v1.0.0 May 29, 2019
coveralls.json implement comparable for Date, Time, DateTime May 29, 2019
mix.exs use public hex.pm Jul 2, 2019
mix.lock better defcomparable/3 syntax, gen_ne_test/3 and gen_eq_test/3 helpers May 29, 2019

README.md

Comparable

Elixir protocol which describes how two Elixir terms can be compared. There are cases when we want to compare two terms of some data type not just by term value according standard Erlang/Elixir ordering rules but to use some meaningful business logic to do comparison. Main purpose of this package is to provide extended versions of standard kernel functions like ==/2, !=/2, >/2, </2, >=/2, <=/2 which will rely on Comparable protocol implementation for given pair of types. Protocol itself is very similar to Ord type class in Haskell.

Hex Documentation

Installation

The package can be installed by adding comparable to your list of dependencies in mix.exs:

def deps do
  [
    {:comparable, "~> 1.0.0"}
  ]
end

Real example

Kernel Elixir comparison functions work pretty fine with standard numeric types like integer or float (and it works even in nested terms like map):

iex> %{a: 1} == %{a: 1.0}
true

But if we try to apply Kernel equality function to terms containing custom Decimal numbers it will not work so good:

iex(1)> %{a: Decimal.new("1")} == %{a: Decimal.new("1.0")}
false

This is because the same decimal number can be presented as different Elixir term:

iex> Decimal.new("1") |> Map.from_struct
%{coef: 1, exp: 0, sign: 1}
iex> Decimal.new("1.0") |> Map.from_struct
%{coef: 10, exp: -1, sign: 1}

And here Comparable protocol can help us, let's implement it for Decimal type using existing Decimal.compare/2 helper:

use Comp

defcomparable left :: Decimal, right :: Decimal do
  left
  |> Decimal.compare(right)
  |> case do
    %Decimal{coef: 1, sign: 1} ->
      Comp.gt()

    %Decimal{coef: 1, sign: -1} ->
      Comp.lt()

    %Decimal{coef: 0} ->
      Comp.eq()

    %Decimal{coef: :qNaN} ->
      raise(
        "can't apply Comparable protocol to left = #{inspect(left)} and right = #{
          inspect(right)
        }"
      )
  end
end

And when protocol for Decimal type is implemented, we can use Comp.equal?/2 utility function instead of Kernel ==/2:

iex> Comp.equal?(%{a: Decimal.new("1")}, %{a: Decimal.new("1.0")})
true

which works as expected according meaning of Decimal numbers instead of just term values. Comparison based on Comparable protocol is very useful when for example we compare big nested structures which contain Decimals or other custom types (like Date, Time, NaiveDateTime, URI etc) in nested collections like lists, maps, tuples or other data types:

iex> x0 = %{a: [%{b: Decimal.new("1")}]}
%{a: [%{b: #Decimal<1>}]}
iex> x1 = %{a: [%{b: Decimal.new("1.0")}]}
%{a: [%{b: #Decimal<1.0>}]}
iex> x0 == x1
false
iex> Comp.equal?(x0, x1)
true

Utilities

use Comp expression provides utilities not only for equality, but for other comparison operations as well. Also it provides infix shortcuts for these utilities:

Kernel.fn/2 Comp.fn/2 Comp infix shortcut
x == y Comp.equal?(x, y) x <~> y
x != y Comp.not_equal?(x, y) x <|> y
x > y Comp.greater_than?(x, y) x >>> y
x < y Comp.less_than?(x, y) x <<< y
x >= y Comp.greater_or_equal?(x, y) x ~>> y
x <= y Comp.less_or_equal?(x, y) x <<~ y
max(x, y) Comp.max(x, y)
min(x, y) Comp.min(x, y)

Example of infix shortcuts usage:

iex> use Comp
Comp
iex> Decimal.new("1") <~> Decimal.new("1.0")
true
iex> Decimal.new("1") <|> Decimal.new("2")
true
iex> Decimal.new("2") >>> Decimal.new("1")
true
iex> Decimal.new("1") <<< Decimal.new("2")
true
iex> Decimal.new("1") ~>> Decimal.new("1.0")
true
iex> Decimal.new("1") <<~ Decimal.new("1.0")
true

Also there is additional Comp.compare/2 function if you want to work directly with Ord enum values:

iex> Comp.compare(1, 1)
:eq
iex> Comp.compare(1, 2)
:lt
iex> Comp.compare(2, 1)
:gt

Testing

use Comp expression also provides 2 utilities which can auto-generate tests for implementation of Comparable protocol for your types:

use Comp

gen_ne_test("not equal test", Decimal.new("1"), Decimal.new("2"))
gen_eq_test("equal test", Decimal.new("1"), Decimal.new("1.0"))
You can’t perform that action at this time.