billylanchantin
CompareChain - Semantic, chained comparisons for Elixir
CompareChain
Announcing CompareChain - a small library to aid with comparisons.
Examples
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
# Semantic comparisons + 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
Sales pitch
Working with comparison operators in Elixir can lead to a fair bit of boilerplate. This is because the normal infix comparison operators like < do structural comparison:
iex> ~D[2017-03-31] < ~D[2017-04-01]
false
When you try that, you get a warning: warning: invalid comparison with struct literal ~D[2017-03-31]. Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison...
To do semantic comparison, you need to use the proper module’s compare/2 function:
iex> Date.compare(~D[2017-03-31], ~D[2017-04-01]) == :lt
true
This ends up reading like RPN where :lt acts somewhat like a postfix operator. The issue is compounded when you need to perform more complicated logic:
iex> Date.compare(~D[2017-03-31], ~D[2017-04-01]) == :lt and Date.compare(~D[2017-04-01], ~D[2017-04-02]) == :lt
true
You end up with a verbose mix of infix and pseudo-postfix operators.
Additionally, Elixir does not support chained comparisons like 1 < 2 < 3:
iex> 1 < 2 < 3
false
When you try that, you get a warning: Elixir does not support nested comparisons...
Enter CompareChain
CompareChain provides some helper macros that allow you to
- chain infix operators
- perform semantic comparison with infix operators
- combine (chained) comarisons with
and,or, andnot
After calling import CompareChain, you get macros compare?/{1,2}. With compare?/1 can do operations like:
iex> compare?(1 < 2 < 3)
true
iex> compare?(1 < 2 > 3)
false
With compare?/2 can do comparisons like:
iex> compare?(~D[2017-03-31] < ~D[2017-04-01], DateTime)
true
The idea is that you provide a module with a suitable compare/2 function as the second argument just like with functions like Enum.sort/2. The macro then rewrites your expression using the module you provide.
You can write complicated expressions if you wish:
iex> yesterday = ~D[2022-11-04]
iex> today = ~D[2022-11-05]
iex> tomorrow = ~D[2022-11-06]
iex> compare?(yesterday < today < tomorrow and not (today >= tomorrow), Date)
true
iex> compare?(%{a: ~T[16:00:00]}.a <= ~T[17:00:00], Time)
true
You can also do fancier things by defining a custom module:
defmodule DateTimeWithInfinity do
def compare(:infinity, _), do: :gt
def compare(_, :infinity), do: :lt
def compare(:neg_infinity, _), do: :lt
def compare(_, :neg_infinity), do: :gt
def compare(%DateTime{} = dt1, %DateTime{} = dt2) do
DateTime.compare(dt1, dt2)
end
end
This module supports :infinity as a value that is always greater than every date time, and :neg_infinity that is always less than every datetime. This is super useful for defining ranges that are open on one side:
range1 = %{starts_at: ~U[2022-01-01T00:00:00Z], ends_at: ~U[2022-02-01T00:00:00Z]}
range2 = %{starts_at: ~U[2022-01-10T00:00:00Z], ends_at: :infinity}
compare?(
range2.starts_at <= range1.starts_at <= range2.ends_at or
range2.starts_at <= range1.ends_at <= range2.ends_at,
DateTimeWithInfinity)
#=> true
Future work
If you try it out and like it and/or find any problems, let me know! Issues and PRs are welcome.
Acknowledgements
Shoutout to @benwilson512 and @mcrumm for the helpful discussions and guidance! ![]()
And thank you to all the folks who participated in the elixir-lang-core discussion. In particular, thanks to Cliff (sorry I don’t know your handle) whose idea I shamelessly built off of: https://groups.google.com/g/elixir-lang-core/c/W2TeQm5r1H4/m/ctVuN_woBgAJ
Most Liked
billylanchantin
CompareChain Release: v0.3.0 (2023-01-28)
- Docs: CompareChain
- Git: CargoSense/compare_chain
Hi (upwards of) tens of users! I’ve just released a new version of CompareChain.
There are two main changes:
-
You can now use
==and!=as wellcompare?(~T[00:00:00] == ~T[11:11:11], Time) #=> false compare?(~T[00:00:00] != ~T[11:11:11], Time) #=> true(No idea why I didn’t do this in the first place..)
-
You can now use Elixir >= 1.13.0 instead of being restricted to 1.14
I could probably go much lower if I stopped usingMacro.prewalker/1.
As always, issues and PRs are welcome! ![]()
billylanchantin
CompareChain Release: v0.6.0 (2025-01-09)
- Docs: CompareChain — compare_chain v0.6.0
- GitHub: GitHub - CargoSense/compare_chain: Chained semantic comparisons for Elixir. · GitHub
This release is largely to fix a warning: Elixir 1.17+ warned about the code a < b in one of the doctests.
It also shifts version support: it’s now Elixir 1.15 to 1.18 officially. Older versions of Elixir should still work, but they’re no longer “officially” supported.
Changelog
v0.6.0 (2025-01-09)
- Drop official support for Elixir 1.13 and 1.14 (though they should still work)
- Currently support Elixir 1.15 to 1.18
- Change docs so they no longer warn about a doctest
- Miscellanea: renamed LISENCE file, added CODEOWNERS file, etc.
As always, issues and PRs welcome ![]()
sbuttgereit
It gets even more fun when you compare ranges to ranges since you may have overlapping ranges (e.g. Less Than, Not Overlapping vs. Less Than, Overlapping). For my project these range types originate in the database so I put similar functionality to what you’ve done here in the library I use close to where the database custom types are defined.
Nice to see a this kind of problem being looked at in a public library.
Popular in Announcing
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









