Skip to content

Sorting a list of Structs or Maps by two dates in Elixir

Posted on:2024-06-08 | 3 min read

In this post, you’ll learn how to sort a list of structs (or maps) by two dates.

TL;DR

Elixir official Enum.sort/2 docs tell you how to sort by a date using the Date module, for example:

iex> Enum.sort([~D[2024-05-05], ~D[2024-05-01], ~D[2024-05-03]], {:asc, Date})
[~D[2024-05-01], ~D[2024-05-03], ~D[2024-05-05]]

Using descendant order:

iex> Enum.sort([~D[2024-05-05], ~D[2024-05-01], ~D[2024-05-03]], {:desc, Date})
[~D[2024-05-05], ~D[2024-05-03], ~D[2024-05-01]]

But, what when you need to use a key from a map?

Elixir’s Enum.sort_by/3 accepts a mapper function to extract the said value. From the docs:

This function maps each element of the enumerable using the provided mapper function. The enumerable is then sorted by the mapped elements using the sorter, which defaults to :asc and sorts the elements ascendingly.

In the following example, we extract the date1 from the Map with &(&1.date1). Then the sorter function {:asc, Date} is applied to the extracted Dates.

iex> data = [
      %{name: "A", date1: ~D[2024-05-01]},
      %{name: "C", date1: ~D[2024-05-03]},
      %{name: "B", date1: ~D[2024-05-02]}
]
iex> Enum.sort_by(data, &(&1.date1), {:asc, Date})
[
  %{name: "A", date1: ~D[2024-05-01]},
  %{name: "B", date1: ~D[2024-05-02]},
  %{name: "C", date1: ~D[2024-05-03]}
]

Now, how do we sort by two dates?

Let’s say you have the following data, and you need to sort first by date1, and then by date2.

iex> data = [
      %{name: "B", date1: ~D[2024-05-01], date2: ~D[2024-02-03]},
      %{name: "A", date1: ~D[2024-05-01], date2: ~D[2024-02-02]},
      %{name: "D", date1: ~D[2024-05-02], date2: ~D[2024-05-05]},
      %{name: "C", date1: ~D[2024-05-01], date2: ~D[2024-02-05]}
    ]

By following the Enum.sort_by/3 docs

As in sort/2, avoid using the default sorting function to sort structs, as by default it performs structural comparison instead of a semantic one. In such cases, you shall pass a sorting function as third element or any module that implements a compare/2 function.

The key to sorting by two dates or any other value is: pass a sorting function as third element or any module that implements a compare/2 function.

Let’s create a module with a compare/2 function that sorts by two dates. This function will accept two tuples as arguments. Each tuple will be of size of 2 containing the first and second date subsequently.

SortByTwoDates module

defmodule SortByTwoDates do
  @moduledoc """
  Sort by two dates module.
  """

  @spec compare({Date.t(), Date.t()}, {Date.t(), Date.t()}) :: :lt | :eq | :gt
  def compare({first_date_1, first_date_2}, {second_date_1, second_date_2}) do
    case Date.compare(first_date_1, second_date_1) do
      :eq -> Date.compare(first_date_2, second_date_2)
      val -> val
    end
  end
end

The function compares the first dates, if they are equal, we compare the second dates and return the result.

If the first dates are not equal, we return whatever the result of comparing the first dates is, as there’s no need to do further comparisons.

Let’s test this module!

iex> Enum.sort_by(data, &{&1.date1, &1.date2}, {:asc, SortByTwoDates})
[
  %{name: "A", date1: ~D[2024-05-01], date2: ~D[2024-02-02]},
  %{name: "B", date1: ~D[2024-05-01], date2: ~D[2024-02-03]},
  %{name: "C", date1: ~D[2024-05-01], date2: ~D[2024-02-05]},
  %{name: "D", date1: ~D[2024-05-02], date2: ~D[2024-05-05]}
]
iex> Enum.sort_by(data, &{&1.date1, &1.date2}, {:desc, SortByTwoDates})
[
  %{name: "D", date1: ~D[2024-05-02], date2: ~D[2024-05-05]},
  %{name: "C", date1: ~D[2024-05-01], date2: ~D[2024-02-05]},
  %{name: "B", date1: ~D[2024-05-01], date2: ~D[2024-02-03]},
  %{name: "A", date1: ~D[2024-05-01], date2: ~D[2024-02-02]}
]

That’s it! You can implement your module to sort by more date values or any other value by using your OwnModule.compare/2 function.