Advent of Code 2025 - Day 5

Easy puzzles two days in a row. I suspect tomorrow’s puzzle will not be that easy…

defmodule Day05 do
  def part1(input) do
    {ranges, ingredients} = parse(input)
    Enum.count(ingredients, &in_any_range?(&1, ranges))
  end

  defp in_any_range?(ingredient, ranges) do
    Enum.any?(ranges, &(ingredient in &1))
  end

  def part2(input) do
    {ranges, _ingredients} = parse(input)
    ranges = ranges
    |> Enum.sort
    |> combine_ranges
    Enum.reduce(ranges, 0, &(Range.size(&1) + &2))
  end

  defp combine_ranges([r]), do: [r]
  defp combine_ranges([r1, r2 | rest]) do
    case Range.disjoint?(r1, r2) do
      true ->
        [r1 | combine_ranges([r2 | rest])]
      false ->
        r = min(r1.first, r2.first) .. max(r1.last, r2.last)
        combine_ranges([r | rest])
    end
  end

  defp parse([ranges, ingredients]) do
    ranges = Enum.map(ranges, fn range ->
      [first, last] = String.split(range, "-")
      String.to_integer(first) .. String.to_integer(last)
    end)
    ingredients = Enum.map(ingredients, &String.to_integer(&1))
    {ranges, ingredients}
  end
end

4 Likes

For the first time I needed a helper private function with multiple heads to handle a recursion properly.

  defmodule Day5 do
    @input "day5_1.input" |> File.read!() |> String.split("\n\n", trim: true)

    def calc([ranges, ids] \\ @input) do
      ranges =
        ranges
        |> String.split(["\s", "\n"], trim: true)
        |> Enum.map(&String.split(&1, "-", trim: true))
        |> Enum.map(fn [b, e] -> String.to_integer(b)..String.to_integer(e)//1 end)

      ids
      |> String.split(["\s", "\n"], trim: true)
      |> Enum.reduce({0, []}, fn id, {count, ids} ->
        id = String.to_integer(id)

        Enum.reduce_while(ranges, {count, ids}, fn range, {count, ids} ->
          if id in range, do: {:halt, {count + 1, [id | ids]}}, else: {:cont, {count, ids}}
        end)
      end)
    end

    def in_ranges([ranges, _ids] \\ @input) do
      ranges
      |> String.split(["\s", "\n"], trim: true)
      |> Enum.map(&String.split(&1, "-", trim: true))
      |> Enum.map(fn range -> Enum.map(range, &String.to_integer/1) end)
      |> Enum.sort()
      |> merge([])
      |> Enum.sum_by(fn [f, l] -> l - f + 1 end)
    end

    defp merge([], acc), do: Enum.reverse(acc)

    defp merge([range | rest], []), do: merge(rest, [range])

    defp merge([[f2, l2] | rest], [[f1, l1] | acc]) do
      if f2 <= l1 + 1 do
        merged = [f1, max(l1, l2)]
        merge(rest, [merged | acc])
      else
        merge(rest, [[f2, l2], [f1, l1] | acc])
      end
    end
  end
4 Likes

Yep, today was an easy one; parsing takes almost more lines than the rest of the code :).

defmodule Y2025.Day05 do
  def fresh?(_id, []), do: false

  def fresh?(id, [{a, b} | rest]) do
    cond do
      id < a -> false
      a <= id && id <= b -> true
      true -> fresh?(id, rest)
    end
  end

  def parse(s) do
    [ranges, ids] = String.split(s, "\n\n")

    ranges =
      ranges
      |> String.split("\n")
      |> Enum.map(fn line ->
        [a, b] = String.split(line, "-")
        {String.to_integer(a), String.to_integer(b)}
      end)
      |> Enum.sort() # <- important for fresh? and do_part2 to work!

    ids = ids |> String.split("\n") |> Enum.map(&String.to_integer/1)

    {ranges, ids}
  end

  def part1(s) do
    {ranges, ids} = parse(s)

    ids
    |> Enum.filter(fn id -> fresh?(id, ranges) end)
    |> length()
  end

  def part2(s) do
    {ranges, _} = parse(s)

    do_part2(ranges, 0)
  end

  def do_part2([], n), do: n
  def do_part2([{a, b}], n), do: n + b - a + 1

  def do_part2([{a, b}, {c, d} | rest], n) do
    cond do
      b < c -> do_part2([{c, d} | rest], n + b - a + 1)
      b >= d -> do_part2([{a, b} | rest], n)
      # b >= c & b < d
      true -> do_part2([{a, d} | rest], n)
    end
  end
end

3 Likes

Nice - TIL that Range module exists :). cc: @bjorng

2 Likes

I don’t want to say I’m disappointed by the puzzles this year, but… so far I kind of am :frowning: I expected at least one curly one so far… maybe tomorrow!

2 Likes

That is why I created RangeSet:

Parse

[fresh, ingridients] = String.split(puzzle_input, "\n\n")

fresh =
  fresh
  |> String.split()
  |> Enum.map(fn range ->
    [a, b] = range |> String.split("-") |> Enum.map(&String.to_integer/1)

    a..b//1
  end)
  |> RangeSet.new()

ingridients =
  ingridients
  |> String.split()
  |> Enum.map(&String.to_integer/1)

Part 1

Enum.count(ingridients, & &1 in fresh)

Part 2

Enum.count(fresh)
7 Likes

Same solution as everyone I guess :smiley:

defmodule AdventOfCode.Solutions.Y25.Day05 do
  alias AoC.Input

  def parse(input, _part) do
    [ranges, ids] = input |> Input.read!() |> String.split("\n\n")
    ranges = ranges |> lines() |> Enum.map(&parse_range/1)
    ids = ids |> lines() |> Enum.map(&String.to_integer/1)
    {ranges, ids}
  end

  defp lines(text), do: String.split(text, "\n", trim: true)

  defp parse_range(str) do
    [a, b] = String.split(str, "-")
    String.to_integer(a)..String.to_integer(b)
  end

  def part_one({ranges, ids}) do
    Enum.count(ids, fn id -> Enum.any?(ranges, &(id in &1)) end)
  end

  def part_two({ranges, _}) do
    ranges
    |> Enum.sort_by(fn a..b//_ -> {a, b} end)
    |> reduce_ranges()
    |> Enum.sum_by(&Range.size/1)
  end

  defp reduce_ranges([a..b//_, c..d//_ | rest]) when c > b do
    [a..b | reduce_ranges([c..d | rest])]
  endI

  defp reduce_ranges([a..b//s, c..d//_ | rest]) when c in a..b//s do
    reduce_ranges([a..max(b, d) | rest])
  end

  defp reduce_ranges([last]) do
    [last]
  end
end

Except @hauleth 's , that RangeSet could come in handy for some other puzzles as well :smiley: I have the same thing but for rectangles, there is always that puzzle each year :slight_smile:

2 Likes
defmodule Aoc2025.Solutions.Y25.Day05 do
  alias AoC.Input

  def parse(input, _part) do
    Input.read!(input)
    |> String.split("\n\n", trim: true, parts: 2)
  end

# couln't help myself
  def get_fresh!(ranges) do
    ranges
    |> String.split("\n", trim: true)
    |> Enum.map(&String.split(&1, "-"))
    |> Enum.map(fn [x, y] -> {String.to_integer(x), String.to_integer(y)} end)
  end

  def get_available(ingredients) do
    ingredients
    |> String.split("\n", trim: true)
    |> Enum.map(fn x -> String.to_integer(x) end)
  end


  def part_one([ranges, ingredients]) do
    fresh = get_fresh!(ranges)
    available = get_available(ingredients)

    Enum.count(available, fn a ->
      Enum.any?(fresh, fn {start, stop} -> a >= start and a <= stop end)
    end)
  end

  def part_two([ranges, _ingredients]) do
    fresh = get_fresh!(ranges)

    Enum.sort(fresh, fn {a, _b}, {c, _d} -> a < c end)
    |> merge_ranges()
    |> sum_ranges()
  end

  def merge_ranges([{_a, b} = s1, {c, _d} = s2 | rest]) when c > b,
    do: [s1 | merge_ranges([s2 | rest])]

  def merge_ranges([{a, b}, {c, d} | rest]) when c in a..b//1,
    do: merge_ranges([{a, max(b, d)} | rest])

  def merge_ranges(last), do: last

  def sum_ranges(ranges) do
    Enum.reduce(ranges, 0, fn {start, stop}, sum -> sum + (stop - start) + 1 end)
  end
end

No Range was harmed during development

3 Likes

It was nice that I got to use Range, and pattern matching on Range’s. I’ve never done that before. I didn’t try it, but I suspect that if tried to do something like accumulate all the values into a MapSet or something, it would take forever and use up all your memory.

Edit: Duh, of course it would with over 300 trillion values.

defmodule RAoc.Solutions.Y25.Day05 do
  alias AoC.Input

  def parse(input, _part) do
    Input.read!(input)
    |> String.split("\n\n")
    |> then(fn [ranges_str, ingredients_str] ->
      ranges =
        String.trim(ranges_str)
        |> String.split("\n")
        |> Enum.map(fn range_str ->
          range_str
          |> String.split("-")
          |> then(fn [i1_str, i2_str] ->
            String.to_integer(i1_str)..String.to_integer(i2_str)
          end)
        end)

      ingredients =
        String.trim(ingredients_str)
        |> String.split("\n")
        |> Enum.map(&String.to_integer/1)

      {ranges, ingredients}
    end)
  end

  def part_one({ranges, ingredients}) do
    ingredients |> Enum.filter(&is_fresh?(&1, ranges)) |> Enum.count()
  end

  def part_two({ranges, _ingredients}) do
    count_fresh_ranges(ranges)
  end

  defp is_fresh?(ingredient, ranges) do
    Enum.any?(ranges, fn range -> ingredient in range end)
  end

  defp count_fresh_ranges(ranges) do
    union_ranges(ranges)
    |> Enum.sum_by(&Range.size/1)
  end

  defp union_ranges(ranges) do
    ranges
    |> Enum.sort(fn r1_s.._//1, r2_s.._//1 ->
      r1_s <= r2_s
    end)
    |> union_into([])
  end

  defp union_into([], acc), do: acc
  defp union_into([range | []], acc), do: [range | acc]

  defp union_into([range1 | [range2 | ranges]], acc) do
    if Range.disjoint?(range1, range2) do
      union_into([range2 | ranges], [range1 | acc])
    else
      s1..e1//1 = range1
      s2..e2//1 = range2
      union_into([min(s1, s2)..max(e1, e2) | ranges], acc)
    end
  end
end
2 Likes

RangeSet is awesome!

2 Likes

The key is to sort the ranges. Too bad I’ve not thought about doing so :wink:. The result is, of course, far too complex.


defmodule AdventOfCode.Solution.Year2025.Day05 do
  # Part 1
  def in_interval?(i, {l, h}), do: l <= i and i <= h

  def in_any_interval?(ingredient, intervals),
    do: Enum.any?(for interval <- intervals, do: in_interval?(ingredient, interval))

  def part1(input) do
    {intervals, ingredients} = parse(input)
    Enum.count(ingredients, &in_any_interval?(&1, intervals))
  end

  # Part 2
  def distinct?({a, b}, {c, d}), do: b < c or d < a
  def merge_overlapping({a, b}, {c, d}) when c < a, do: merge_overlapping({c, d}, {a, b})
  def merge_overlapping({a, b}, {_c, d}), do: {a, max(b, d)}

  def merge([], intervals_in_stock), do: intervals_in_stock

  def merge([to_add | to_merge], intervals_in_stock) do
    case Enum.split_while(intervals_in_stock, &distinct?(&1, to_add)) do
      {_, []} ->
        merge(to_merge, [to_add | intervals_in_stock])

      {distincts, [overlapping | in_stock]} ->
        merged_interval = merge_overlapping(to_add, overlapping)
        merge([merged_interval | to_merge], distincts ++ in_stock)
    end
  end

  def add_interval_sizes(intervals) do
    intervals |> Enum.map(fn {l, h} -> h - l + 1 end) |> Enum.sum()
  end

  def part2(input) do
    elem(parse(input), 0)
    |> merge([])
    |> add_interval_sizes()
  end

  def parse(input) do
    [intervals, ingredients] = String.split(input, "\n\n", trim: true)

    p_intervals =
      for int <- String.split(intervals, "\n") do
        [l, h] = String.split(int, "-")
        {String.to_integer(l), String.to_integer(h)}
      end

    p_ingredients =
      for ing <- String.split(ingredients, "\n", trim: true), do: String.to_integer(ing)

    {p_intervals, p_ingredients}
  end
end
2 Likes

Don’t beat yourself up over it. I came to it after my first attempt became too complex. Lost quite some time on that dead track.

1 Like

One the plus side, you were solving a harder problem than everyone else (I sense a bunch of folks, me included, were a bit underwhelmed by the simplicity of Day 5’s problem), so you had more fun than the rest of us! :slight_smile:

1 Like
#!/usr/bin/env elixir

# Advent of Code 2025. Day 5

Mix.install([
  {:nimble_parsec, "~> 1.4.2"},
])

defmodule M do
  import NimbleParsec

  parse = repeat(
    integer(min: 1)
    |> ignore(string("-"))
    |> integer(min: 1)
    |> tag(:range)
    |> ignore(string("\n"))
  )
  |> ignore(string("\n"))
  |> repeat(
    integer(min: 1)
    |> unwrap_and_tag(:id)
    |> ignore(string("\n"))
  )

  defparsec :parse, parse

  # don't know if there is a way to get ranges and ids directly from NimbleParsec
  # transform() allows us to extract them
  def transform([], ranges, ids), do: {ranges, ids}
  def transform([{:range, [lo,hi]} | lst], ranges, ids), do: transform(lst, [lo..hi | ranges], ids)
  def transform([{:id, id} | lst], ranges, ids), do: transform(lst, ranges, [id | ids])

  def union(lo1.._hi1//1 = range1, lo2.._hi2//1 = range2) when lo2 < lo1 do
    union(range2, range1)
  end
  def union(lo1..hi1//1, lo2..hi2//1) when lo2 == hi1+1 do
    lo1..hi2
  end
  def union(_lo1..hi1//1, lo2.._hi2//1) when hi1 < lo2 do
    false # cannot union the ranges
  end
  def union(lo1..hi1//1, lo2..hi2//1) when lo2 >= lo1 and lo2 <= hi1 and hi2 <= hi1 do
    lo1..hi1
  end
  def union(lo1..hi1//1, lo2..hi2//1) when hi1 >= lo2 and hi1 <= hi2 or lo2 >= lo1 and lo2 <= hi1 do
    lo1..hi2
  end

  def add([], range_to_add, disjoint_ranges), do: [range_to_add | disjoint_ranges]
  def add([range | ranges], range_to_add, disjoint_ranges) do
    # Try to union both ranges
    case union(range, range_to_add) do
      false -> add(ranges, range_to_add, [range | disjoint_ranges])
      range -> add(disjoint_ranges++ranges, range, []) # retry to add the new range
    end
  end
end

{ranges, ids} = File.read!("../day05.txt")
  |> M.parse()
  |> then(fn {:ok, lst, "", %{}, _, _} -> lst end)
  |> M.transform([], [])

# Part 1
Enum.count(ids, fn id -> Enum.any?(ranges, fn range -> id in range end) end)
|> IO.inspect(label: "Day 5. Part 1")

# Part 2
ranges
|> Enum.reduce([], fn range, ranges -> M.add(ranges, range, []) end)
|> Enum.map(&Range.size/1)
|> Enum.sum()
|> IO.inspect(label: "Day 5. Part 2")
1 Like

2025 Dec 05

Cafeteria

defmodule Cafeteria do
  defp parse_range(line) do
    case Regex.run(~r"(\d+)-(\d+)", line) do
      [_match, min_str, max_str] ->
        {String.to_integer(min_str), String.to_integer(max_str)}
    end
  end

  defp parse_ids(id_lines, acc \\ []) do
    case id_lines do
      [] ->
        acc
      ["" | _] ->
        acc
      [line | rest] ->
        acc = [String.to_integer(String.trim(line)) | acc]
        parse_ids(rest, acc)
    end
  end

  def parse(lines) do
    {range_lines, id_lines} = Enum.split_while(lines, fn line -> String.trim(line) != "" end)
    ranges = Enum.map(range_lines, &parse_range/1)
      |> Enum.sort()
    ids = parse_ids(tl(id_lines))
    {ranges, Enum.reverse(ids)}
  end

  def id_covered?(id, ranges) do
    case ranges do
      [{minr, maxr} | rest] ->
        cond do
          minr > id ->
            false
          maxr >= id ->
            true
          true ->
            id_covered?(id, rest)
        end
      [] ->
        false
    end
  end  
end
{test_ranges, test_ids} = """
3-5
10-14
16-20
12-18

1
5
8
11
17
32
""" |> String.split("\n")
  |> Cafeteria.parse()
  |> IO.inspect()

Enum.filter(test_ids, fn id -> Cafeteria.id_covered?(id, test_ranges) end)
{input_ranges, input_ids} = File.stream!(__DIR__ <> "/dec-05-input.txt")
  |> Cafeteria.parse()
Enum.filter(input_ids, fn id -> Cafeteria.id_covered?(id, input_ranges) end)
  |> Enum.count()

Part 2

defmodule Cafeteria2 do
  def merge_ranges(ranges, merged_ranges \\ []) do
    case ranges do
      [] ->
        Enum.reverse(merged_ranges)
      [{minr, maxr} | rest] ->
        case merged_ranges do 
          [] ->
            merge_ranges(rest, [{minr, maxr}])
          [{min_mr, max_mr} | rest_merged] ->
            if max_mr < minr do
              merge_ranges(rest, [{minr, maxr}, {min_mr, max_mr} | rest_merged])
            else  # merge ranges
              merge_ranges(rest, [{min_mr, max(maxr, max_mr)} | rest_merged])
            end
        end
    end
  end

  def sum_ranges(ranges) do
    merge_ranges(ranges)
      |> Enum.reduce(0, fn {minr, maxr}, sum ->
        sum + maxr - minr + 1
      end)
  end
end
Cafeteria2.sum_ranges(input_ranges)
1 Like

Part 1

[one, two] =
  File.read!("ids.txt")
  |> String.split("\n\n")

ranges = one
  |> String.split("\n")
  |> Enum.map(fn x -> x |> String.split("-") |> Enum.map(&String.to_integer/1) end)

two
  |> String.split("\n")
  |> Enum.map(&String.to_integer/1)
  |> Enum.count(&(Enum.any?(ranges, fn [a, b] -> a <= &1 and &1 <= b end)))
  |> IO.inspect

Part 2

[one, _] =
  File.read!("ids.txt")
  |> String.split("\n\n")

ranges = one
  |> String.split("\n")
  |> Enum.map(fn x -> x |> String.split("-") |> Enum.map(&String.to_integer/1) end)

defmodule Aoc do
  def touch?([x1, x2], [y1, y2]), do: max(x1, y1) - min(x2, y2) <= 1

  def merge([x1, x2], [y1, y2]), do: [Enum.min([x1, x2, y1, y2]), Enum.max([x1, x2, y1, y2])]

  def loop(ranges, previous) when ranges == previous, do: ranges

  def loop(ranges, _) do
    Enum.map(ranges, fn range ->
      ranges
      |> Enum.filter(&(touch?(range, &1)))
      |> Enum.reduce(range, &(merge(&1, &2)))
    end)
    |> Enum.uniq
    |> loop(ranges)
  end
end

Aoc.loop(ranges, [])
|> Enum.map(fn [a, b]-> b - a + 1 end)
|> Enum.sum
|> IO.inspect

1 Like

Just throwing mine in the pot - part 2 completes in 0.1ms on an old i5.

def part2(ranges) do
  ranges
  |> Enum.sort_by(& &1.first)
  |> Enum.reduce([hd(ranges)], fn b, [a | rest] = disjoint ->
    if Range.disjoint?(a, b) do
      [b | disjoint]
    else
      [min(a.first, b.first)..max(a.last, b.last) | rest]
    end
  end)
  |> Enum.sum_by(&Range.size/1)
end
1 Like

Stream.transform is slightly too general for this, but it’s fun to use:

[ranges_string, _ingredients_string] =
  File.read!("input.txt")
  |> String.split("\n\n")

ranges_string
|> String.split("\n")
|> Enum.map(&Regex.run(~r/(\d+)-(\d+)/, &1, capture: :all_but_first))
|> Enum.map(fn [a, b] -> Range.new(String.to_integer(a), String.to_integer(b)) end)
|> Enum.sort()
|> Stream.transform(
  fn -> nil end,
  fn r, last_r ->
    cond do
      is_nil(last_r) ->
        {[], r}
      Range.disjoint?(r, last_r) ->
        {[last_r], r}
      true ->
        {[], Range.new(min(r.first, last_r.first), max(r.last, last_r.last))}
    end
  end,
  fn last_r ->
    {[last_r], nil}
  end,
  fn _ -> nil end
)
|> Stream.map(&Range.size/1)
|> Enum.sum()
|> IO.inspect()
1 Like

For part 2, I flatten a list of range firsts and lasts and keep a running total of how many ranges overlap our place in the list.

defmodule Day05 do
  def part1(file) do
    {ranges, ids} = parse(file)
    Enum.count(ids, fn id -> Enum.any?(ranges, &(id in &1)) end)
  end

  def part2(file) do
    {ranges, _} = parse(file)
    firsts = Enum.frequencies_by(ranges, & &1.first)
    lasts = Enum.frequencies_by(ranges, &(&1.last + 1))

    Enum.sort(Map.keys(firsts) ++ Map.keys(lasts))
    |> Enum.scan({0, 0}, fn x, {_, c} -> {x, c + (firsts[x] || 0) - (lasts[x] || 0)} end)
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.sum_by(fn [{a, count}, {b, _}] -> if count > 0, do: b - a, else: 0 end)
  end

  def parse(file) do
    [rngs, ids] = file |> File.read!() |> split("\n\n") |> Enum.map(&split(&1, "\n"))
    {Enum.map(rngs, &to_rng/1), Enum.map(ids, &to_int/1)}
  end

  def split(str, on), do: String.split(str, on, trim: true)
  def to_int(str), do: str |> Integer.parse() |> elem(0)
  def to_rng(str), do: str |> split("-") |> Enum.map(&to_int/1) |> then(&apply(Range, :new, &1))
end
1 Like