Advent of Code 2023 - Day 19

Here’s my solution, it went pretty well !

import Input

defmodule Day19 do
  def part1(input) do
    {map, parts} = parse_input(input)

    parts
    |> Enum.map(&walk(map, "in", &1))
    |> Enum.zip(parts)
    |> Enum.map(&score/1)
    |> Enum.sum()
  end

  defp walk(map, wf_name, part) do
    conds = Map.fetch!(map, wf_name)

    conds
    |> Enum.find(fn
      {field, ">", thresh, _} ->
        part
        |> Map.fetch!(field)
        |> Kernel.>(thresh)

      {field, "<", thresh, _} ->
        part
        |> Map.fetch!(field)
        |> Kernel.<(thresh)

      _ ->
        true
    end)
    |> then(fn
      {_, _, _, res} -> res
      default -> default
    end)
    |> case do
      "A" -> "A"
      "R" -> "R"
      new_wf_name -> walk(map, new_wf_name, part)
    end
  end

  defp score({"R", _}), do: 0
  defp score({"A", %{"x" => x, "m" => m, "a" => a, "s" => s}}), do: x + m + a + s

  def part2(input) do
    {map, _} = parse_input(input)

    combs(map, Map.fetch!(map, "in"), %{
      "x" => {1, 4001},
      "m" => {1, 4001},
      "a" => {1, 4001},
      "s" => {1, 4001}
    })
  end

  defp combs(map, [{field, op, thresh, res} | tail], threshs) do
    {new_thresh, rev_thresh} = split_treshs(threshs, field, op, thresh)
    combs(map, res, new_thresh) + combs(map, tail, rev_thresh)
  end

  defp combs(map, [key], threshs), do: combs(map, key, threshs)

  defp combs(map, "A", threshs) do
    threshs
    |> Enum.map(fn {_, {min, max}} -> max - min end)
    |> Enum.map(&max(&1, 0))
    |> Enum.reduce(1, &(&1 * &2))
  end

  defp combs(map, "R", threshs), do: 0
  defp combs(map, key, threshs), do: combs(map, Map.fetch!(map, key), threshs)

  defp split_treshs(threshs, field, "<", thresh) do
    new_threshs =
      Map.update!(threshs, field, fn {min, max} ->
        {min, min(max, thresh)}
      end)

    rev_threshs =
      Map.update!(threshs, field, fn {min, max} ->
        {max(min, thresh), max}
      end)

    {new_threshs, rev_threshs}
  end

  defp split_treshs(threshs, field, ">", thresh) do
    new_threshs =
      Map.update!(threshs, field, fn {min, max} ->
        {max(min, thresh + 1), max}
      end)

    rev_threshs =
      Map.update!(threshs, field, fn {min, max} ->
        {min, min(max, thresh + 1)}
      end)

    {new_threshs, rev_threshs}
  end

  defp parse_input([workflows, parts]) do
    {
      workflows |> Enum.map(&parse_workflow/1) |> Map.new(),
      Enum.map(parts, &parse_part/1)
    }
  end

  defp parse_workflow(input) do
    input
    |> Utils.splitrim(~r/[{},]/)
    |> Enum.map(&Utils.splitrim(&1, ":"))
    |> Enum.map(fn
      [solo] ->
        solo

      [<<field::bytes-size(1), op::bytes-size(1), thresh::binary>>, res] ->
        {field, op, String.to_integer(thresh), res}
    end)
    |> then(fn [name | conds] -> {name, conds} end)
  end

  defp parse_part(input) do
    input
    |> Utils.splitrim(~r/[{},]/)
    |> Enum.map(&Utils.splitrim(&1, "="))
    |> Enum.map(fn [field, value] -> {field, String.to_integer(value)} end)
    |> Map.new()
  end

  def run() do
    part2(~i[19]c)
  end

  def bench() do
    Benchmark.mesure_milliseconds(&run/0)
  end
end

Input is just a collection of sigils (such as ~i) to manipulate inputs (like returning parts separated by an empty line).