In this post, you’ll learn how to sort a list of structs (or maps) by two dates.
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 Date
s.
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.