billylanchantin

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, and not

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! :slight_smile:

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

billylanchantin

CompareChain Release: v0.3.0 (2023-01-28)

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 well

    compare?(~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 using Macro.prewalker/1.

As always, issues and PRs are welcome! :slight_smile:

billylanchantin

billylanchantin

CompareChain Release: v0.6.0 (2025-01-09)

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 :slight_smile:

sbuttgereit

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.

Where Next?

Popular in Announcing Top

deadtrickster
I’ve just released stable versions of my Prometheus Elixir libs: Elixir client [docs]; Ecto collector [docs]; Plugs instrumenter/Export...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43806 214
New
josevalim
EDIT: since Ecto 3.0 final version is out, this post was amended to use the final versions in the instructions below. Hi everyone, We a...
New
kip
ex_cldr provides localisation and internationalisation support based upon the data from the Unicode CLDR project. Unicode released CLDR ...
407 13056 120
New
MRdotB
I needed to reuse React components from my Chrome extension in my Phoenix/LiveView backend. I noticed that for Svelte/Vue, there are live...
New
kip
Image is an image processing library for Elixir. It is based upon the fabulous vix library that provides a libvips wrapper for Elixir. I...
622 19010 194
New
treble37
Just looking for a little feedback on a tiny helper library I built - Sometimes I find the need to convert maps with atom keys to maps w...
New
hpopp
After just over two years in development, this latest version of Pigeon is what I finally consider done in regards to my original vision ...
New
Qqwy
TypeCheck: Fast and flexible runtime type-checking for your Elixir projects. Core ideas Type- and function specifications are const...
336 14534 100
New
wfgilman
I’ve cleaned up and open sourced three financial libraries I was using for my company. They are bindings for the APIs of these three comp...
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29703 241
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
aesmail
Hello guys, I have finally made it. I created an admin interface for a framework. It’s been on my todo list for years and with the curre...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54250 245
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

We're in Beta

About us Mission Statement