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

Advent of Code - Day 15

2023_day15.livemd

Advent of Code - Day 15

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

Introduction

–> Content

Puzzle

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

Parser

Code - Parser

defmodule Parser do
  def parse(input) do
    input
    |> String.replace("\n", "")
    |> String.split(",", trim: true)
  end
end

Tests - Parser

ExUnit.start(autorun: false)

defmodule ParserTest do
  use ExUnit.Case, async: true
  import Parser

  @input """
  rn=1,cm-,qp=3,cm=2,\nqp-,pc=\n4,ot=9,ab=5,pc-,pc=6,ot=7
  """

  @expected ["rn=1", "cm-", "qp=3", "cm=2", "qp-", "pc=4", "ot=9", "ab=5", "pc-", "pc=6", "ot=7"]

  describe "parse/1" do
    test "simple example" do
      assert parse(@input) == @expected
    end
  end
end

ExUnit.run()

Part One

Code - Part 1

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

  def run(input_string) when is_bitstring(input_string), do: run(Parser.parse(input_string))

  def run(inputs) do
    inputs
    |> Enum.map(fn input -> hash_alg(input) end)
    |> Enum.sum()
  end

  def hash_alg(input) do
    input
    |> to_charlist()
    |> Enum.reduce(0, fn char, res -> rem((res + char) * 17, 256) end)
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

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

  @raw_input """
  rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7
  """

  @input Parser.parse(@raw_input)

  describe "run/1" do
    test "main example" do
      assert run(@raw_input) == 1320
    end
  end

  describe "hash_alg/1" do
    test "main examples" do
      assert hash_alg("rn=1") == 30
      assert hash_alg("cm-") == 253
      assert hash_alg("qp=3") == 97
      assert hash_alg("cm=2") == 47
      assert hash_alg("qp-") == 14
      assert hash_alg("pc=4") == 180
      assert hash_alg("ot=9") == 9
      assert hash_alg("ab=5") == 197
      assert hash_alg("pc-") == 48
      assert hash_alg("pc=6") == 214
      assert hash_alg("ot=7") == 231
    end
  end
end

ExUnit.run()

Solution - Part 1

PartOne.solve(puzzle_input)

Part Two

Code - Part 2

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

  def run(input_string) when is_bitstring(input_string), do: run(Parser.parse(input_string))

  def run(inputs) do
    inputs
    |> Enum.reduce(%{}, fn input, boxes ->
      input |> parse_step() |> perform_step_on_boxes(boxes)
    end)
    |> focusing_power()
  end

  def hash_alg(input) do
    input
    |> to_charlist()
    |> Enum.reduce(0, fn char, res -> rem((res + char) * 17, 256) end)
  end

  def parse_step(input) do
    String.split(input, ~r/[\=\-]/, trim: true, include_captures: true)
    |> Enum.zip([:label, :operation, :focal_length])
    |> Enum.into(%{}, fn {val, key} -> {key, val} end)
    |> (fn hash ->
          if Map.has_key?(hash, :focal_length) do
            Map.update!(hash, :focal_length, fn val -> String.to_integer(val) end)
          else
            hash
          end
        end).()
  end

  def perform_step_on_boxes(step, boxes) do
    box_number = hash_alg(step[:label])

    lens = %{label: step[:label], focal_length: step[:focal_length]}

    case step[:operation] do
      "-" ->
        Map.update(boxes, box_number, [], fn contents ->
          if index = Enum.find_index(contents, fn content -> content[:label] == step[:label] end) do
            List.delete_at(contents, index)
          else
            contents
          end
        end)
        |> Map.reject(fn {_k, v} -> Enum.empty?(v) end)

      "=" ->
        Map.update(boxes, box_number, [lens], fn contents ->
          if index = Enum.find_index(contents, fn content -> content[:label] == step[:label] end) do
            List.replace_at(contents, index, lens)
          else
            contents ++ [lens]
          end
        end)
    end
  end

  def focusing_power(boxes) do
    boxes
    |> Enum.map(fn {box_number, lenses} ->
      lenses
      |> Enum.with_index(1)
      |> Enum.map(fn {%{label: _label, focal_length: focal_length}, slot_number} ->
        (1 + box_number) * slot_number * focal_length
      end)
    end)
    |> List.flatten()
    |> Enum.sum()
  end
end

Tests - Part 2

ExUnit.start(autorun: false)

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

  @raw_input """
  rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7
  """

  @input Parser.parse(@raw_input)
  @steps Enum.map(@input, fn step_string -> parse_step(step_string) end)

  describe "run/1" do
    test "main example" do
      assert run(@raw_input) == 145
    end
  end

  describe "hash_alg/1" do
    test "main examples" do
      assert hash_alg("rn") == 0
      assert hash_alg("cm") == 0
      assert hash_alg("qp") == 1
      assert hash_alg("cm") == 0
      assert hash_alg("qp") == 1
      assert hash_alg("pc") == 3
      assert hash_alg("ot") == 3
      assert hash_alg("ab") == 3
      assert hash_alg("pc") == 3
      assert hash_alg("pc") == 3
      assert hash_alg("ot") == 3
    end
  end

  describe "parse_step/1" do
    test "main examples" do
      assert parse_step("rn=1") == %{label: "rn", operation: "=", focal_length: 1}
      assert parse_step("cm-") == %{label: "cm", operation: "-"}
      assert parse_step("qp=3") == %{label: "qp", operation: "=", focal_length: 3}
      assert parse_step("cm=2") == %{label: "cm", operation: "=", focal_length: 2}
      assert parse_step("qp-") == %{label: "qp", operation: "-"}
      assert parse_step("pc=4") == %{label: "pc", operation: "=", focal_length: 4}
      assert parse_step("ot=9") == %{label: "ot", operation: "=", focal_length: 9}
      assert parse_step("ab=5") == %{label: "ab", operation: "=", focal_length: 5}
      assert parse_step("pc-") == %{label: "pc", operation: "-"}
      assert parse_step("pc=6") == %{label: "pc", operation: "=", focal_length: 6}
      assert parse_step("ot=7") == %{label: "ot", operation: "=", focal_length: 7}
    end
  end

  describe "perform_step_on_boxes/2" do
    test "main example" do
      boxes = %{}

      assert (boxes = perform_step_on_boxes(parse_step("rn=1"), boxes)) == %{
               0 => [%{label: "rn", focal_length: 1}]
             }

      assert (boxes = perform_step_on_boxes(parse_step("cm-"), boxes)) == %{
               0 => [%{label: "rn", focal_length: 1}]
             }

      assert (boxes = perform_step_on_boxes(parse_step("qp=3"), boxes)) == %{
               0 => [%{label: "rn", focal_length: 1}],
               1 => [%{label: "qp", focal_length: 3}]
             }

      assert (boxes = perform_step_on_boxes(parse_step("cm=2"), boxes)) == %{
               0 => [%{label: "rn", focal_length: 1}, %{label: "cm", focal_length: 2}],
               1 => [%{label: "qp", focal_length: 3}]
             }

      assert (boxes = perform_step_on_boxes(parse_step("qp-"), boxes)) == %{
               0 => [%{label: "rn", focal_length: 1}, %{label: "cm", focal_length: 2}]
             }

      assert (boxes = perform_step_on_boxes(parse_step("pc=4"), boxes)) == %{
               0 => [%{label: "rn", focal_length: 1}, %{label: "cm", focal_length: 2}],
               3 => [%{label: "pc", focal_length: 4}]
             }

      assert (boxes = perform_step_on_boxes(parse_step("ot=9"), boxes)) == %{
               0 => [%{label: "rn", focal_length: 1}, %{label: "cm", focal_length: 2}],
               3 => [%{label: "pc", focal_length: 4}, %{label: "ot", focal_length: 9}]
             }

      assert (boxes = perform_step_on_boxes(parse_step("ab=5"), boxes)) == %{
               0 => [%{label: "rn", focal_length: 1}, %{label: "cm", focal_length: 2}],
               3 => [
                 %{label: "pc", focal_length: 4},
                 %{label: "ot", focal_length: 9},
                 %{label: "ab", focal_length: 5}
               ]
             }

      assert (boxes = perform_step_on_boxes(parse_step("pc-"), boxes)) == %{
               0 => [%{label: "rn", focal_length: 1}, %{label: "cm", focal_length: 2}],
               3 => [%{label: "ot", focal_length: 9}, %{label: "ab", focal_length: 5}]
             }

      assert (boxes = perform_step_on_boxes(parse_step("pc=6"), boxes)) == %{
               0 => [%{label: "rn", focal_length: 1}, %{label: "cm", focal_length: 2}],
               3 => [
                 %{label: "ot", focal_length: 9},
                 %{label: "ab", focal_length: 5},
                 %{label: "pc", focal_length: 6}
               ]
             }

      assert (_boxes = perform_step_on_boxes(parse_step("ot=7"), boxes)) == %{
               0 => [%{label: "rn", focal_length: 1}, %{label: "cm", focal_length: 2}],
               3 => [
                 %{label: "ot", focal_length: 7},
                 %{label: "ab", focal_length: 5},
                 %{label: "pc", focal_length: 6}
               ]
             }
    end
  end

  describe "focusing_power/2" do
    test "main example" do
      boxes = %{
        0 => [%{label: "rn", focal_length: 1}, %{label: "cm", focal_length: 2}],
        3 => [
          %{label: "ot", focal_length: 7},
          %{label: "ab", focal_length: 5},
          %{label: "pc", focal_length: 6}
        ]
      }

      assert focusing_power(boxes) == 145
      assert focusing_power(boxes) == 1 + 4 + 28 + 40 + 72
    end
  end
end

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)