Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Advent of Code - 2024 - Day 5

advent-of-code/2024/day05.livemd

Advent of Code - 2024 - Day 5

Mix.install([
  {:kino, "~> 0.14.2"},
  {:kino_vega_lite, "~> 0.1.13"},
  {:kino_explorer, "~> 0.1.23"}
])

Puzzle Input

puzzle_input = Kino.Input.textarea("Please paste the puzzle input:")

Part 1

part_1_test_input = Kino.Input.textarea("Please paste the test input for part 1:")
defmodule PrintingJob do
  defstruct pages: []

  def from_string(string) when is_binary(string) do
    pages =
      string
      |> String.split(",")
      |> Enum.map(&String.to_integer/1)

    %__MODULE__{pages: pages}
  end

  def member?(job = %__MODULE__{}, page) when is_integer(page) do
    Enum.member?(job.pages, page)
  end

  def find_index(job = %__MODULE__{}, find) when is_integer(find) do
    Enum.find_index(job.pages, fn page ->
      page == find
    end)
  end

  def move_page(job = %__MODULE__{}, moving, after: after_page) do
    moving_page_index = Enum.find_index(job.pages, &(&1 == moving))
    after_page_index = Enum.find_index(job.pages, &(&1 == after_page))

    new_pages =
      job.pages
      |> List.insert_at(after_page_index + 1, moving)
      |> List.delete_at(moving_page_index)

    %{job | pages: new_pages}
  end
end
defmodule PrintingJob.PageOrderingRule do
  defstruct [:before, :after]

  def from_string(notation) when is_binary(notation) do
    [b, a] =
      notation
      |> String.split("|")

    %__MODULE__{before: String.to_integer(b), after: String.to_integer(a)}
  end
end
defmodule PrintingJob.Validation.PageOrder do
  def check(rule = %PrintingJob.PageOrderingRule{}, job = %PrintingJob{}) do
    with :ok <- applies(rule, job), :ok <- passes(rule, job) do
      true
    else
      {:error, :rule_does_not_apply} ->
        true

      {:error, :rule_does_not_pass} ->
        false
    end
  end

  def failing(rules, job = %PrintingJob{}) when is_list(rules) do
    rules
    |> Enum.filter(fn rule ->
      :ok == applies(rule, job) &amp;&amp; {:error, :rule_does_not_pass} == passes(rule, job)
    end)
  end

  defp applies(rule = %PrintingJob.PageOrderingRule{}, job = %PrintingJob{}) do
    if PrintingJob.member?(job, rule.before) and PrintingJob.member?(job, rule.after) do
      :ok
    else
      {:error, :rule_does_not_apply}
    end
  end

  defp passes(rule = %PrintingJob.PageOrderingRule{}, job = %PrintingJob{}) do
    if PrintingJob.find_index(job, rule.before) < PrintingJob.find_index(job, rule.after) do
      :ok
    else
      {:error, :rule_does_not_pass}
    end
  end
end
defmodule SafetyManualUpdates do
  defstruct rules: [], jobs: []

  def from_string(string) when is_binary(string) do
    [rules, jobs] =
      string
      |> String.split("\n\n")

    rules =
      rules
      |> String.split("\n")
      |> Enum.map(&amp;PrintingJob.PageOrderingRule.from_string/1)

    jobs =
      jobs
      |> String.split("\n")
      |> Enum.map(&amp;PrintingJob.from_string/1)

    %__MODULE__{rules: rules, jobs: jobs}
  end

  def valid_jobs(updates = %__MODULE__{}) do
    valid =
      updates.jobs
      |> Enum.filter(fn job ->
        updates.rules
        |> Enum.all?(fn rule ->
          PrintingJob.Validation.PageOrder.check(rule, job)
        end)
      end)

    %{updates | jobs: valid}
  end

  def middle_pages(%__MODULE__{jobs: jobs}) do
    jobs
    |> Enum.map(fn %{pages: pages} ->
      middle = round(Enum.count(pages) / 2) - 1
      Enum.at(pages, middle)
    end)
  end

  def invalid_jobs(updates = %__MODULE__{}) do
    valid = valid_jobs(updates)
    invalid = updates.jobs -- valid.jobs
    %{updates | jobs: invalid}
  end

  def correct_jobs(updates = %__MODULE__{}) do
    new_updates = apply_page_order_rules(updates)

    # apply rules until page ordering is stable
    if updates.jobs == new_updates.jobs do
      new_updates
    else
      correct_jobs(new_updates)
    end
  end

  def apply_page_order_rules(updates = %__MODULE__{}) do
    corrected =
      updates.jobs
      |> Enum.map(fn job ->
        PrintingJob.Validation.PageOrder.failing(updates.rules, job)
        |> Enum.reduce(job, fn rule, acc ->
          if PrintingJob.Validation.PageOrder.check(rule, acc) do
            acc
          else
            PrintingJob.move_page(acc, rule.after, after: rule.before)
          end
        end)
      end)

    %{updates | jobs: corrected}
  end
end
part_1_test_input
|> Kino.Input.read()
|> SafetyManualUpdates.from_string()
|> SafetyManualUpdates.valid_jobs()
|> SafetyManualUpdates.middle_pages()
|> Enum.sum()
puzzle_input
|> Kino.Input.read()
|> SafetyManualUpdates.from_string()
|> SafetyManualUpdates.valid_jobs()
|> SafetyManualUpdates.middle_pages()
|> Enum.sum()

Part 2

part_2_test_input = Kino.Input.textarea("Please paste the test input for part 2:")
part_2_test_input
|> Kino.Input.read()
|> SafetyManualUpdates.from_string()
|> SafetyManualUpdates.invalid_jobs()
|> SafetyManualUpdates.correct_jobs()
|> SafetyManualUpdates.middle_pages()
|> Enum.sum()
puzzle_input
|> Kino.Input.read()
|> SafetyManualUpdates.from_string()
|> SafetyManualUpdates.invalid_jobs()
|> SafetyManualUpdates.correct_jobs()
|> SafetyManualUpdates.middle_pages()
|> Enum.sum()