Provides convenience macros for comparisons which do:
- chained comparisons like
a < b < c
- semantic comparisons using the structural operators:
<
,>
,<=
,>=
,==
,!=
,===
, and!==
- combinations using
and
,or
, andnot
iex> import CompareChain
# Chained comparisons
iex> compare?(1 < 2 < 3)
true
# Semantic comparisons
iex> compare?(~D[2017-03-31] < ~D[2017-04-01], Date)
true
# Chained, semantic comparisons
iex> compare?(~D[2017-03-31] < ~D[2017-04-01] < ~D[2017-04-02], Date)
true
# Semantic comparisons with logical operators
iex> compare?(~T[16:00:00] <= ~T[16:00:00] and not (~T[17:00:00] <= ~T[17:00:00]), Time)
false
# More complex expressions
iex> compare?(%{a: ~T[16:00:00]}.a <= ~T[17:00:00], Time)
true
Add compare_chain
to your list of dependencies in mix.exs
:
def deps do
[
{:compare_chain, "~> 0.5"}
]
end
Documentation can be found at https://hexdocs.pm/compare_chain.
Once installed, you can add:
import CompareChain
to your defmodule
and you will have access to CompareChain.compare?/1
and CompareChain.compare?/2
.
CompareChain
was originally motivated by the following situation:
You have an interval of time bounded by a two
%Date{}
structs:start_date
andend_date
. You want to know if some thirddate
falls in that interval. How do you write this?
In Elixir, we'd write this as follows:
Date.compare(start_date, date) == :lt and
Date.compare(date, end_date) == :lt
This is verbose and therefore a little hard to read.
It's also potentially incorrect.
What if date
is considered "within" the interval even if it equals start_date
or end_date
?
To include the bounds in our comparison, we'd instead write the expression like this:
Date.compare(start_date, date) != :gt and
Date.compare(date, end_date) != :gt
(We could have written Date.compare(start_date, date) in [:lt, :eq]
, but != :gt
is faster.)
In order to spot the difference between these two cases, you have to keep several things in mind:
- The order of the arguments passed to
Date.compare/2
- The specific comparison operators for each clause:
==
vs.!=
- The specific comparison atoms for each clause:
:lt
vs.:gt
Since this is hard to read, it's easy to introduce bugs. Contrast this with how you'd write the equivalent code in Python:
start_date < date < end_date # excluding bounds
start_date <= date <= end_date # including bounds
This is much easier to read. So why can't we write this in Elixir? Two reasons:
- Structural comparison operators
- Chained vs. nested comparisons
Operators like <
do structural comparison instead of semantic comparison.
From the Kernel
docs:
... comparisons in Elixir are structural, as it has the goal of comparing data types as efficiently as possible to create flexible and performant data structures. This distinction is specially important for functions that provide ordering, such as
>/2
,</2
,>=/2
,<=/2
,min/2
, andmax/2
. For example:~D[2017-03-31] > ~D[2017-04-01]will return
true
because structural comparison compares the:day
field before:month
or:year
. In order to perform semantic comparisons, the relevant data-types provide acompare/2
function, such asDate.compare/2
:iex> Date.compare(~D[2017-03-31], ~D[2017-04-01]) :lt
In other words, although ~D[2017-03-31] > ~D[2017-04-01]
is perfectly valid code, it does not tell you if ~D[2017-03-31]
is a later date than ~D[2017-04-01]
like you might expect.
Instead, you need to use Date.compare/2
.
Additionally, even if ~D[2017-03-31] > ~D[2017-04-01]
did do semantic comparison, you still couldn't write the interval check like you do in Python.
This is because in Python, an expression like 1 < 2 < 3
is syntactic sugar for (1 < 2) and (2 < 3)
, aka a series of "chained" expressions.
Elixir does not provide an equivalent syntactic sugar.
Instead, 1 < 2 < 3
is evaluated as (1 < 2) < 3
, aka a series of "nested" expressions.
Since (1 < 2) < 3
simplifies to true < 3
, that's probably not what you want!
Elixir will even warn you when you attempt an expression like that:
warning: Elixir does not support nested comparisons. Something like
x < y < z
is equivalent to
(x < y) < z
which ultimately compares z with the boolean result of (x < y). Instead, consider joining together each comparison segment with an "and", for example,
x < y and y < z
CompareChain
attempts to address both of these issues with the macro CompareChain.compare?/2
.
Its job is to take code similar to how you'd like to write it and rewriting it to be semantically correct.
For our motivating example, we'd write this:
import CompareChain
compare?(start_date < date < end_date, Date) # excluding bounds
compare?(start_date <= date <= end_date, Date) # including bounds
And at compile time, CompareChain.compare?/2
rewrites those to be:
# excluding bounds
Date.compare(start_date, date) == :lt and
Date.compare(date, end_date) == :lt
# including bounds
Date.compare(start_date, date) != :gt and
Date.compare(date, end_date) != :gt
This way your code is more readable while still remaining correct.
CompareChain.compare?/1
is also available in case you only need chained comparison using the structural operators:
compare?(1 < 2 < 3)
Though I find this case comes up less often.
As a happy accident, CompareChain.compare?/2
always uses fewer characters than its compare/2
counterpart:
compare?(a <= b, Date)
# vs.
Date.compare(a, b) != :gt
(Assuming you've already included import CompareChain
, of course!)
Because it's shorter and more readable, these days I always use CompareChain
for any semantic comparison, chained or not.