aoc2022/day5.livemd
2022-12-12 20:11:40 +01:00

4.5 KiB

AOC 2022 - Day 5

Mix.install([
  {:req, "~> 0.3.3"},
  {:nimble_parsec, "~> 1.2.3"}
])

Puzzle description

Day 5: Supply Stacks.

Input

defmodule Load do
  def input do
    aoc_session = System.fetch_env!("LB_AOC_SESSION")
    input_url = "https://adventofcode.com/2022/day/5/input"
    Req.get!(input_url, headers: [cookie: "session=#{aoc_session}"]).body
  end
end

Solution

defmodule Parser do
  import NimbleParsec

  crate =
    ignore(string("["))
    |> utf8_char([?A..?Z])
    |> ignore(string("]"))

  empty_slot = string("   ") |> replace(:empty)

  slot = choice([crate, empty_slot])

  row =
    repeat_while(
      concat(
        slot,
        optional(ignore(string(" ")))
      ),
      :not_newline
    )
    |> ignore(string("\n"))
    |> tag(:row)

  rows =
    repeat_while(row, :not_index_row)
    |> tag(:rows)

  indices =
    repeat_while(
      ignore(string(" "))
      |> integer(min: 1)
      |> ignore(string(" "))
      |> optional(ignore(string(" "))),
      :not_newline
    )
    |> ignore(string("\n"))
    |> tag(:indices)

  instruction =
    ignore(string("move "))
    |> unwrap_and_tag(integer(min: 1), :amount)
    |> ignore(string(" from "))
    |> unwrap_and_tag(integer(min: 1), :from)
    |> ignore(string(" to "))
    |> unwrap_and_tag(integer(min: 1), :to)
    |> ignore(optional(string("\n")))
    |> tag(:instruction)

  instructions = instruction |> repeat() |> tag(:instructions) |> eos()

  defparsec(
    :crates,
    rows
    |> concat(indices)
    |> concat(ignore(string("\n")))
    |> concat(instructions)
  )

  defp not_newline(<<?\n, _::binary>>, context, _, _), do: {:halt, context}
  defp not_newline(_, context, _, _), do: {:cont, context}

  defp not_index_row(<<" 1", _::binary>>, context, _, _), do: {:halt, context}
  defp not_index_row(_, context, _, _), do: {:cont, context}
end

# Parser.rows(~s([S]     [C]
# [P] [M] [Z]
#  1   2   3)) |> IO.inspect()

# Parser.slot("   ") |> IO.inspect()
# Parser.slot("brt") |> IO.inspect()

defmodule Part1 do
  def run(input) do
    {:ok, data, _, _, _, _} =
      input
      |> Parser.crates()

    stacks =
      data
      |> Keyword.get(:rows)
      |> Keyword.values()
      |> List.zip()
      |> Enum.map(&Tuple.to_list/1)
      |> Enum.map(
        &Enum.reject(&1, fn
          :empty -> true
          _ -> false
        end)
      )

    instructions =
      data
      |> Keyword.get(:instructions)
      |> Keyword.values()

    instructions
    |> Enum.reduce(stacks, fn [amount: amount, from: from, to: to], acc ->
      from = from - 1
      to = to - 1

      {transfer, from_stack} =
        Enum.at(acc, from)
        |> Enum.split(amount)

      to_stack =
        transfer
        |> Enum.reverse()
        |> Enum.concat(Enum.at(acc, to))

      List.replace_at(acc, from, from_stack)
      |> List.replace_at(to, to_stack)
    end)
    |> Enum.map(&Enum.take(&1, 1))
    |> Enum.join()
  end
end

defmodule Part2 do
  def run(input) do
    {:ok, data, _, _, _, _} =
      input
      |> Parser.crates()

    stacks =
      data
      |> Keyword.get(:rows)
      |> Keyword.values()
      |> List.zip()
      |> Enum.map(&Tuple.to_list/1)
      |> Enum.map(
        &Enum.reject(&1, fn
          :empty -> true
          _ -> false
        end)
      )

    instructions =
      data
      |> Keyword.get(:instructions)
      |> Keyword.values()

    instructions
    |> Enum.reduce(stacks, fn [amount: amount, from: from, to: to], acc ->
      from = from - 1
      to = to - 1

      {transfer, from_stack} =
        Enum.at(acc, from)
        |> Enum.split(amount)

      to_stack =
        transfer
        |> Enum.concat(Enum.at(acc, to))

      List.replace_at(acc, from, from_stack)
      |> List.replace_at(to, to_stack)
    end)
    |> Enum.map(&Enum.take(&1, 1))
    |> Enum.join()
  end
end

ExUnit.start(autorun: false)

defmodule Test do
  use ExUnit.Case, async: true
  @example_input ~s(    [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)
  @input Load.input()

  test "it loads the input" do
    assert String.length(@input) > 0
  end

  test "part 1 example" do
    assert Part1.run(@example_input) === "CMZ"
  end

  test "part 1" do
    assert Part1.run(@input) === "GFTNRBZPF"
  end

  test "part 2 example" do
    assert Part2.run(@example_input) === "MCD"
  end

  test "part 2" do
    assert Part2.run(@input) === "VRQWPDSGP"
  end
end

ExUnit.run()