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

AOC 2023 - Day 03

aoc2023/day03.livemd

AOC 2023 - Day 03

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

AOC Helper

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

Part 1

Code

defmodule PartOne do
  def extract_grid(input) do
    input
    |> String.split("\n")
    |> Enum.map(&extract_line/1)
  end

  def extract_line(line) do
    line
    |> String.graphemes()
    |> Enum.map(fn char ->
      with :error <- Integer.parse(char) do
        case char do
          "." -> nil
          _ -> {:symbol, char}
        end
      else
        _ -> {:number, char}
      end
    end)
  end

  def find_numbers(grid) do
    grid
    |> Enum.with_index()
    |> Enum.reduce([], fn {line, y}, numbers ->
      {numbers, current} =
        line
        |> Enum.with_index()
        |> Enum.reduce({numbers, []}, fn {value, x}, {numbers, current} ->
          with {:number, _} <- value do
            {numbers, current ++ [{x, y}]}
          else
            _ ->
              if current == [], do: {numbers, current}, else: {numbers ++ [current], []}
          end
        end)

      if current == [], do: numbers, else: numbers ++ [current]
    end)
  end

  def valid_numbers(grid, numbers) do
    Enum.reduce(numbers, [], fn positions, valid_numbers ->
      if adjacent_symbol?(grid, positions) do
        valid_numbers ++ [build_number(grid, positions)]
      else
        valid_numbers
      end
    end)
  end

  def adjacent_symbol?(grid, {x, y}) do
    [
      {x - 1, y},
      {x - 1, y - 1},
      {x - 1, y + 1},
      {x, y - 1},
      {x, y + 1},
      {x + 1, y},
      {x + 1, y - 1},
      {x + 1, y + 1}
    ]
    |> Enum.find_value(fn {x, y} ->
      case grid |> Enum.at(y) |> Kernel.||([]) |> Enum.at(x) do
        {:symbol, _} -> true
        _ -> false
      end
    end)
  end

  def adjacent_symbol?(grid, positions) when is_list(positions) do
    Enum.find(positions, fn position -> adjacent_symbol?(grid, position) end)
  end

  def build_number(grid, positions) do
    positions
    |> Enum.map(fn {x, y} -> grid |> Enum.at(y) |> Enum.at(x) |> elem(1) end)
    |> Enum.join("")
    |> String.to_integer()
  end

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

  def run(input) do
    grid = extract_grid(input)
    numbers = find_numbers(grid)
    valid_numbers(grid, numbers) |> Enum.sum()
  end
end

Test

ExUnit.start(autorun: false)

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

  @input "467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598.."
  @expected 4361

  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 extract_grid(input) do
    input
    |> String.split("\n")
    |> Enum.map(&amp;extract_line/1)
  end

  def extract_line(line) do
    line
    |> String.graphemes()
    |> Enum.map(fn char ->
      with :error <- Integer.parse(char) do
        case char do
          "*" -> :gear
          _ -> nil
        end
      else
        _ -> {:number, char}
      end
    end)
  end

  def find_numbers(grid) do
    grid
    |> Enum.with_index()
    |> Enum.reduce([], fn {line, y}, numbers ->
      {numbers, current} =
        line
        |> Enum.with_index()
        |> Enum.reduce({numbers, []}, fn {value, x}, {numbers, current} ->
          with {:number, _} <- value do
            {numbers, current ++ [{x, y}]}
          else
            _ ->
              if current == [],
                do: {numbers, current},
                else: {numbers ++ [MapSet.new(current)], []}
          end
        end)

      if current == [], do: numbers, else: numbers ++ [MapSet.new(current)]
    end)
  end

  def find_gears(grid, numbers) do
    grid
    |> Enum.with_index()
    |> Enum.reduce([], fn {line, y}, gears ->
      line
      |> Enum.with_index()
      |> Enum.reduce(gears, fn {value, x}, gears ->
        if value == :gear do
          case adjacent_numbers(numbers, {x, y}) do
            [v1, v2] -> gears ++ [build_number(grid, v1) * build_number(grid, v2)]
            _ -> gears
          end
        else
          gears
        end
      end)
    end)
  end

  def adjacent_numbers(numbers, {x, y}) do
    [
      {x - 1, y},
      {x - 1, y - 1},
      {x - 1, y + 1},
      {x, y - 1},
      {x, y + 1},
      {x + 1, y},
      {x + 1, y - 1},
      {x + 1, y + 1}
    ]
    |> Enum.flat_map(fn position ->
      case Enum.find(numbers, &amp;MapSet.member?(&amp;1, position)) do
        nil -> []
        number -> [number]
      end
    end)
    |> Enum.uniq()
  end

  def build_number(grid, positions) do
    positions
    |> MapSet.to_list()
    |> Enum.map(fn {x, y} -> grid |> Enum.at(y) |> Enum.at(x) |> elem(1) end)
    |> Enum.join("")
    |> String.to_integer()
  end

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

  def run(input) do
    grid = extract_grid(input)
    numbers = find_numbers(grid)
    find_gears(grid, numbers) |> Enum.sum()
  end
end

Test

ExUnit.start(autorun: false)

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

  @input "467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598.."
  @expected 467_835

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

ExUnit.run()

Solution

PartTwo.solve(puzzle_input)