# CompareChain
**Chained semantic comparisons for Elixir.**
[![Package](https://img.shields.io/hexpm/v/compare_chain?logo=elixir&style=for-the-badge)](https://hex.pm/packages/compare_chain)
[![Downloads](https://img.shields.io/hexpm/dt/compare_chain?logo=elixir&style=for-the-badge)](https://hex.pm/packages/compare_chain)
[![Build](https://img.shields.io/github/actions/workflow/status/CargoSense/compare_chain/ci.yml?branch=main&logo=github&style=for-the-badge)](https://github.com/CargoSense/compare_chain/actions/workflows/ci.yml)
## Key Features
CompareChain provides convenience macros for:
- chained comparisons (`a < b < c`)
- semantic comparisons using structural operators (`<`, `>`, `<=`, `>=`, `==`, `!=`, `===`, and `!==`)
- combinations (`and`, `or`, and `not`)
## Installation
Add `compare_chain` to your project's dependencies in `mix.exs` and run `mix deps.get`:
```elixir
def deps do
[
{:compare_chain, "~> 0.5"}
]
end
```
## Usage
Import CompareChain to enable access to `CompareChain.compare?/1` and `CompareChain.compare?/2`:
```elixir
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
# Complex expressions
iex> compare?(%{a: ~T[16:00:00]}.a <= ~T[17:00:00], Time)
true
```
See [CompareChain on HexDocs](https://hexdocs.pm/compare_chain) for more.
## Background
Say you have an interval of time bounded by two `%Date{}` structs, `start_date` and `end_date`, and you want to know whether or not a third `date` falls within that interval. How would you write this in Elixir?
```elixir
Date.compare(start_date, date) == :lt and
Date.compare(date, end_date) == :lt
```
The above code is verbose, somewhat hard to read, and potentially incorrect (though not obviously so). What if `date` is considered "within" the interval inclusive of the `start_date` or `end_date`? To include the bounds in the comparison, you'd instead write the expression like this:
```elixir
Date.compare(start_date, date) != :gt and
Date.compare(date, end_date) != :gt
# …or, even more verbosely:
Date.compare(start_date, date) in [:lt, :eq] and
Date.compare(date, end_date) in [:lt, :eq]
```
To spot the difference between these two cases, you must keep in mind:
- the order of the arguments passed to `Date.compare/2`,
- the specific comparison operators for each clause (`==` vs. `!=`), and
- the specific comparison atoms for each clause (`:lt` vs. `:gt`).
Contrast this example with equivalent Python code:
```python
# excluding bounds
start_date < date < end_date
# including bounds
start_date <= date <= end_date
```
Much easier to read! Why can't you write this in Elixir? Two reasons:
1. Structural comparison operators
2. Chained vs. nested comparisons
### Structural Comparison Operators
Operators like `<` do _structural_ comparison (instead of _semantic_ comparison). From the [`Kernel` docs](https://hexdocs.pm/elixir/Kernel.html#module-structural-comparison):
> …**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`, and `max/2`. For example:
>
> ```elixir
> ~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 a `compare/2` function, such as `Date.compare/2`:
>
> ```elixir
> 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 valid code, it does _not_ tell you if `~D[2017-03-31]` is a later date than `~D[2017-04-01]` as you might expect.
Instead, you'd use `Date.compare/2`.
### Chained vs. Nested Comparisons
Additionally, even if `~D[2017-03-31] > ~D[2017-04-01]` did semantic comparison, you still couldn't write the interval check like you do in Python. In Python, an expression like `1 < 2 < 3` is syntactic sugar for `(1 < 2) and (2 < 3)` (a series of "chained" expressions).
Elixir doesn't provide an equivalent syntactic sugar. Instead, `1 < 2 < 3` is evaluated as `(1 < 2) < 3` (a series of "nested" expressions). `(1 < 2) < 3` evaluates to `true < 3` which is _probably_ not what you want!
### A Solution!
CompareChain addresses these complexities with the macro `CompareChain.compare?/2`:
```elixir
import CompareChain
# excluding bounds
compare?(start_date < date < end_date, Date)
# including bounds
compare?(start_date <= date <= end_date, Date)
```
`CompareChain.compare?/2` rewrites these expressions as:
```elixir
# 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
```
Your code is more readable while remaining correct!
`CompareChain.compare?/1` also enables chained comparison using the structural operators:
```elixir
compare?(1 < 2 < 3)
```
## Acknowledgements
Thanks to [Ben Wilson](https://github.com/benwilson512) and [Michael Crumm](https://github.com/mcrumm) for the helpful discussions and their guidance!
Thanks as well to the folks who participated in the [elixir-lang-core](https://groups.google.com/g/elixir-lang-core) discussion, particularly Cliff whose [idea](https://groups.google.com/g/elixir-lang-core/c/W2TeQm5r1H4/m/ctVuN_woBgAJ) I shamelessly built off.
## License
CompareChain is freely available under the [MIT License](https://opensource.org/licenses/MIT).