Powered by AppSignal & Oban Pro

Day 5: Cafeteria

2025/day-05.livemd

Day 5: Cafeteria

Mix.install([{:kino, "~> 0.11.3"}])

Day 5

sample_input = Kino.Input.textarea("Paste Sample Input")
real_input = Kino.Input.textarea("Paste Real Input")
defmodule Cafeteria do
  def part1(input) do
    {ranges, ids} = parse(input)

    Enum.count(ids, fn id -> Enum.find(ranges, & in_range?(id, &1)) end)
  end

  def part2(input) do
    {ranges, _ids} = parse(input)

    ranges
    |> Enum.reduce([], fn range, consolidated ->
      incorporate_range(range, consolidated)
    end)
    |> Enum.sum_by(fn {lower, upper} -> upper - lower + 1 end)
  end

  defp parse(input) do
    [raw_ranges, raw_ids] =
      input
      |> Kino.Input.read()
      |> String.split("\n\n")

    ranges =
      raw_ranges
      |> String.split("\n", trim: true)
      |> Enum.map(fn range ->
        range |> String.split("-") |> Enum.map(&String.to_integer/1) |> List.to_tuple()
      end)

    ids = raw_ids |> String.split("\n", trim: true) |> Enum.map(&String.to_integer/1)
    {ranges, ids}
  end

  defp in_range?(id, {lower, upper}) when id >= lower and id <= upper, do: true
  defp in_range?(_, _), do: false

  defp incorporate_range(range, remaining, visited \\ [])

  # no ranges left, add our new range at the end
  defp incorporate_range(range, [], visited), do: visited ++ [range]

  # new range is strictly less than the next range in the list
  defp incorporate_range({lower, upper}, [{e_lower, _} | _] = remaining, visited) when upper < e_lower do
    visited ++ [{lower, upper}] ++ remaining
  end

  # new range is strictly greater than the next range in the list
  # add that one to the visited list and continue
  defp incorporate_range({lower, upper}, [{e_lower, e_upper} | rest], visited) when lower > e_upper do
    incorporate_range({lower, upper}, rest, visited ++ [{e_lower, e_upper}])
  end

  # new range must overlap with the next range in the list
  # merge them together and then incorporate that combined range
  defp incorporate_range({lower, upper}, [{e_lower, e_upper} | rest], visited) do
    incorporate_range({min(lower, e_lower), max(upper, e_upper)}, rest, visited)
  end
end
Cafeteria.part1(sample_input)
Cafeteria.part1(real_input)
Cafeteria.part2(sample_input)
Cafeteria.part2(real_input)