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 %>