When working with Elixir dates, you may need to group a list of DateTime
values by month and year—say, for rendering an archive or timeline view. But what if you also want to include months that don’t have any entries?
Let’s say you have:
datetimes = [
~U[2023-06-01 10:00:00Z],
~U[2024-06-01 12:00:00Z],
~U[2024-06-15 08:00:00Z],
~U[2024-07-01 09:00:00Z],
~U[2024-09-20 17:00:00Z]
]
June 2023 to September 2024 covers a span of 16 months, but only a few months contain data. We want to group by all months in that range—even empty ones.
Here's the complete, idiomatic code:
# Step 1: Group datetimes by {year, month}
groups =
Enum.group_by(datetimes, fn dt ->
dt
|> DateTime.to_date()
|> then(&{&1.year, &1.month})
end)
# Step 2: Find range
min = Enum.min_by(datetimes, & &1)
max = Enum.max_by(datetimes, & &1)
start_date = Date.new!(min.year, min.month, 1)
end_date = Date.new!(max.year, max.month, 1)
# Step 3: Generate all months between start and end
all_months =
Stream.iterate(start_date, &Date.add(&1, 32))
|> Stream.map(&{&1.year, &1.month})
|> Stream.uniq()
|> Enum.take_while(&(Date.compare(Date.new!(&1 |> elem(0), &1 |> elem(1), 1), end_date) != :gt))
# Step 4: Map to display labels and data
grouped =
all_months
|> Enum.map(fn {year, month} ->
label = Calendar.strftime(Date.new!(year, month, 1), "%B %Y")
{label, Map.get(groups, {year, month}, [])}
end)
This gives a sorted list of tuples like:
[
{"June 2023", [~U[2023-06-01 10:00:00Z]]},
{"July 2023", []},
{"August 2023", []},
...
{"July 2024", [~U[2024-07-01 09:00:00Z]]},
{"August 2024", []},
{"September 2024", [~U[2024-09-20 17:00:00Z]]}
]
You can now render this in your .heex
template like so:
<%= for {label, datetimes} <- @grouped_datetimes do %>
<h2><%= label %></h2>
<ul>
<%= if Enum.empty?(datetimes) do %>
<li><em>No entries</em></li>
<% else %>
<%= for dt <- datetimes do %>
<li><%= Calendar.strftime(dt, "%Y-%m-%d %H:%M") %></li>
<% end %>
<% end %>
</ul>
<% end %>
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.