Save Ukraine

Sorting by multiple criterias in Elixir

Christian Kruse,

Term ordering

The easiest solution would be using the term ordering rules. Elixir has specific rules how it compares e.g. tuples:

Tuples are compared by size, then element by element.

We can take advantage of that and use Enum.sort_by/2 to create a tuple for each element and thus get the desired sorting order:

persons
|> Enum.sort_by(& {&1[:location], &1[:lastname], &1[:firstname]})

This gives us the following list:

[
  %{firstname: "Fernando", lastname: "Tapia Rico", location: "Amsterdam, Netherlands"},
  %{firstname: "Aleksei", lastname: "Magusev", location: "Gothenburg, Sweden"},
  %{firstname: "Eric", lastname: "Meadows-Jönsson", location: "Gothenburg, Sweden"},
  %{firstname: "José", lastname: "Valim", location: "Kraków, Poland"},
  %{firstname: "Andrea", lastname: "Leopardi", location: "L'Aquila, Italy"},
  %{firstname: "James", lastname: "Fish", location: "Unknown"}
]

But what if we want to sort the location descending? That isn't possible with this method.

Stable sorting

Stability to the rescue! When talking about sorting stability means that multiple equal elements are sorted in the same order as they appear in the input. Since sorting in Elixir is stable, we can pipe this list to Enum.sort_by/2 multiple times, with increasing importance of criterias:

persons
|> Enum.sort_by(& &1[:firstname])
|> Enum.sort_by(& &1[:lastname])
|> Enum.sort_by(& &1[:location], :desc)

After the first step the Enum.sort_by/2 call the list looks like this:

[
  %{firstname: "James", lastname: "Fish", location: "Unknown"},
  %{firstname: "Andrea", lastname: "Leopardi", location: "L'Aquila, Italy"},
  %{firstname: "Aleksei", lastname: "Magusev", location: "Gothenburg, Sweden"},
  %{firstname: "Eric", lastname: "Meadows-Jönsson", location: "Gothenburg, Sweden"},
  %{firstname: "Fernando", lastname: "Tapia Rico", location: "Amsterdam, Netherlands"},
  %{firstname: "José", lastname: "Valim", location: "Kraków, Poland"}
]

After that the list gets sorted by firstname, and since equal elements get sorted in the same order as they appear in the input we now get a list first sorted by lastname and then by firstname:

[
  %{firstname: "Aleksei", lastname: "Magusev", location: "Gothenburg, Sweden"},
  %{firstname: "Andrea", lastname: "Leopardi", location: "L'Aquila, Italy"},
  %{firstname: "Eric", lastname: "Meadows-Jönsson", location: "Gothenburg, Sweden"},
  %{firstname: "Fernando", lastname: "Tapia Rico", location: "Amsterdam, Netherlands"},
  %{firstname: "James", lastname: "Fish", location: "Unknown"},
  %{firstname: "José", lastname: "Valim", location: "Kraków, Poland"}
]

Now we call Enum.sort_by/3 with :desc as the last parameter and finally get our sorted list:

[
  %{firstname: "James", lastname: "Fish", location: "Unknown"},
  %{firstname: "Andrea", lastname: "Leopardi", location: "L'Aquila, Italy"},
  %{firstname: "José", lastname: "Valim", location: "Kraków, Poland"},
  %{firstname: "Aleksei", lastname: "Magusev", location: "Gothenburg, Sweden"},
  %{firstname: "Eric", lastname: "Meadows-Jönsson", location: "Gothenburg, Sweden"},
  %{firstname: "Fernando", lastname: "Tapia Rico", location: "Amsterdam, Netherlands"}
]

Nota bene: this method is not elixir specific, so you can use it in other languages with stable sorting as well.