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

AOC 2022 - Day 05

aoc2022/day05.livemd

AOC 2022 - Day 05

Mix.install([
  {:kino_aoc, "~> 0.1"}
])

AOC Helper

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2022", "5", System.fetch_env!("LB_AOC_SESSION"))

Part 1

Code

defmodule PartOne do
  def parse_line(line, {:stack, stacks}) do
    if String.trim(line) == "" do
      {:moves, build_stacks(stacks)}
    else
      {:stack, [parse_row(line) | stacks]}
    end
  end

  def parse_line(line, {:moves, stacks}) do
    {:moves, line |> parse_move() |> perform_move(stacks)}
  end

  def parse_move(move) do
    parts = String.split(move, " ")
    count = Enum.at(parts, 1) |> String.trim() |> String.to_integer()
    from = Enum.at(parts, 3) |> String.trim()
    to = Enum.at(parts, 5) |> String.trim()

    {count, from, to}
  end

  def perform_move({count, from, to}, stack) do
    from_stack = Map.get(stack, from)
    to_stack = Map.get(stack, to)

    {from_stack, to_stack} =
      Enum.reduce(1..count, {from_stack, to_stack}, fn _, {from_stack, to_stack} ->
        {item, from_stack} = List.pop_at(from_stack, -1)
        to_stack = to_stack ++ [item]

        {from_stack, to_stack}
      end)

    stack
    |> Map.put(from, from_stack)
    |> Map.put(to, to_stack)
  end

  def parse_row(row) do
    row
    |> String.codepoints()
    |> Enum.chunk_every(4)
    |> Enum.map(&parse_crate/1)
  end

  def parse_crate(crate) do
    case Enum.at(crate, 1) do
      " " -> nil
      value -> value
    end
  end

  def build_stacks([keys | rows]) do
    keys
    |> Enum.with_index()
    |> Enum.reduce(%{}, fn {key, index}, map ->
      stack = build_stack(rows, index)

      Map.put(map, key, stack)
    end)
  end

  def build_stack(rows, index) do
    rows
    |> Enum.map(&Enum.at(&1, index))
    |> Enum.reject(&is_nil/1)
  end

  def solve(input) do
    IO.puts("--- Part One ---")
    IO.puts("Result: #{run(input)}")
  end

  def run(input) do
    input
    |> String.split("\n")
    |> Enum.reduce({:stack, []}, fn line, acc -> parse_line(line, acc) end)
    |> elem(1)
    |> Enum.map(fn {_k, v} -> Enum.at(v, -1) end)
    |> Enum.join()
  end
end

Test

ExUnit.start(autorun: false)

defmodule PartOneTest do
  use ExUnit.Case, async: true
  import PartOne

  @input "    [D]    
[N] [C]    
[Z] [M] [P]
 1   2   3 

move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2"
  @expected "CMZ"

  test "part one" do
    actual = run(@input)
    assert actual == @expected
  end
end

ExUnit.run()

Solution

PartOne.solve(puzzle_input)

Part 2

Code

defmodule PartTwo do
  def parse_line(line, {:stack, stacks}) do
    if String.trim(line) == "" do
      {:moves, build_stacks(stacks)}
    else
      {:stack, [parse_row(line) | stacks]}
    end
  end

  def parse_line(line, {:moves, stacks}) do
    {:moves, line |> parse_move() |> perform_move(stacks)}
  end

  def parse_move(move) do
    parts = String.split(move, " ")
    count = Enum.at(parts, 1) |> String.trim() |> String.to_integer()
    from = Enum.at(parts, 3) |> String.trim()
    to = Enum.at(parts, 5) |> String.trim()

    {count, from, to}
  end

  def perform_move({count, from, to}, stack) do
    from_stack = Map.get(stack, from)
    to_stack = Map.get(stack, to)

    {from_stack, items} =
      Enum.reduce(1..count, {from_stack, []}, fn _, {from_stack, items} ->
        {item, from_stack} = List.pop_at(from_stack, -1)

        {from_stack, items ++ [item]}
      end)

    stack
    |> Map.put(from, from_stack)
    |> Map.put(to, to_stack ++ Enum.reverse(items))
  end

  def parse_row(row) do
    row
    |> String.codepoints()
    |> Enum.chunk_every(4)
    |> Enum.map(&parse_crate/1)
  end

  def parse_crate(crate) do
    case Enum.at(crate, 1) do
      " " -> nil
      value -> value
    end
  end

  def build_stacks([keys | rows]) do
    keys
    |> Enum.with_index()
    |> Enum.reduce(%{}, fn {key, index}, map ->
      stack = build_stack(rows, index)

      Map.put(map, key, stack)
    end)
  end

  def build_stack(rows, index) do
    rows
    |> Enum.map(&Enum.at(&1, index))
    |> Enum.reject(&is_nil/1)
  end

  def solve(input) do
    IO.puts("--- Part Two ---")
    IO.puts("Result: #{run(input)}")
  end

  def run(input) do
    input
    |> String.split("\n")
    |> Enum.reduce({:stack, []}, fn line, acc -> parse_line(line, acc) end)
    |> elem(1)
    |> Enum.map(fn {_k, v} -> Enum.at(v, -1) end)
    |> Enum.join()
  end
end

Test

ExUnit.start(autorun: false)

defmodule PartTwoTest do
  use ExUnit.Case, async: true
  import PartTwo

  @input "    [D]    
[N] [C]    
[Z] [M] [P]
 1   2   3 

move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2"
  @expected "MCD"

  test "part two" do
    actual = run(@input)
    assert actual == @expected
  end
end

ExUnit.run()

Solution

PartTwo.solve(puzzle_input)