Powered by AppSignal & Oban Pro

AOC 2023 - Day 3

2023/day3.livemd

AOC 2023 - Day 3

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

Input

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

Day 3 Code

defmodule Day3 do
  def build_matrix_entry(""), do: []
  def build_matrix_entry(<> <> rest), do: [<> | build_matrix_entry(rest)]

  def is_next_to_symbol(number, matrix) do
    same_row = [{number.row_idx, number.start_pos - 1}, {number.row_idx, number.end_pos + 1}]
    range = max(number.start_pos - 1, 0)..(number.end_pos + 1)

    row_above =
      if number.row_idx > 0,
        do: Enum.map(range, fn pos -> {number.row_idx - 1, pos} end),
        else: []

    row_below = Enum.map(range, fn pos -> {number.row_idx + 1, pos} end)

    all_adjacent_coords = same_row ++ row_above ++ row_below

    Enum.any?(all_adjacent_coords, fn {row_idx, pos} ->
      row = Enum.at(matrix, row_idx)

      if row do
        Enum.at(row, pos) not in [nil, ".", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
      else
        false
      end
    end)
  end

  def get_adjacent_numbers(row_idx, pos, all_numbers) do
    relevant_row_indices = [row_idx - 1, row_idx, row_idx + 1]

    Enum.filter(all_numbers, fn number ->
      number_pos_range = (number.start_pos - 1)..(number.end_pos + 1)
      number.row_idx in relevant_row_indices and pos in number_pos_range
    end)
  end
end

matrix =
  puzzle_input
  |> String.split("\n")
  |> Enum.map(&amp;Day3.build_matrix_entry/1)

# Find all the numbers present in the matrix
{_row_idx, all_numbers} =
  Enum.reduce(matrix, {0, []}, fn row, {row_idx, numbers} ->
    accumulator = %{
      current_number: %{
        start_pos: nil,
        end_pos: nil,
        digits: "",
        row_idx: row_idx
      },
      numbers: [],
      current_pos: 0
    }

    row_data =
      Enum.reduce(row, accumulator, fn cell, acc ->
        cond do
          cell in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] ->
            if acc.current_number.start_pos != nil do
              # We are adding to an existing number
              Map.merge(acc, %{
                current_number: %{acc.current_number | digits: acc.current_number.digits <> cell},
                current_pos: acc.current_pos + 1
              })
            else
              # We encountered a new number
              Map.merge(acc, %{
                current_number: %{acc.current_number | digits: cell, start_pos: acc.current_pos},
                current_pos: acc.current_pos + 1
              })
            end

          true ->
            if acc.current_number.start_pos != nil do
              # We found the end of a number
              Map.merge(acc, %{
                current_number: %{start_pos: nil, end_pos: nil, digits: "", row_idx: row_idx},
                numbers: [%{acc.current_number | end_pos: acc.current_pos - 1} | acc.numbers],
                current_pos: acc.current_pos + 1
              })
            else
              # We're not building a number, move on
              Map.merge(acc, %{
                current_pos: acc.current_pos + 1
              })
            end
        end
      end)

    row_numbers =
      if row_data.current_number.start_pos != nil do
        # Add left-over number if the row ended on a digit.
        [%{row_data.current_number | end_pos: row_data.current_pos} | row_data.numbers]
      else
        row_data.numbers
      end

    {row_idx + 1, [row_numbers | numbers]}
  end)

numbers = List.flatten(all_numbers)

Part 1 - Solution

numbers
|> Enum.filter(fn number ->
  Day3.is_next_to_symbol(number, matrix)
end)
|> Enum.reduce(0, fn number, acc ->
  acc + String.to_integer(number.digits)
end)

Part 2 - Solution

# Loop over every cell, if the cell is a "*", check if we have two adjacent numbers,
# and add them if so
{_idx, adjacent_numbers_pr_row} =
  Enum.reduce(matrix, {0, []}, fn row, {row_idx, all_adjacent_numbers} ->
    {_pos, adjacent_row_numbers} =
      Enum.reduce(row, {0, []}, fn cell, {pos, adjacent_row_numbers} ->
        if cell == "*" do
          res = Day3.get_adjacent_numbers(row_idx, pos, numbers)

          if length(res) == 2 do
            {pos + 1, [res | adjacent_row_numbers]}
          else
            {pos + 1, adjacent_row_numbers}
          end
        else
          {pos + 1, adjacent_row_numbers}
        end
      end)

    {row_idx + 1, [adjacent_row_numbers | all_adjacent_numbers]}
  end)

Enum.reduce(adjacent_numbers_pr_row, 0, fn row_numbers, acc ->
  res =
    Enum.reduce(row_numbers, 0, fn numbers, acc ->
      if length(numbers) == 2 do
        digit_1 = Enum.at(numbers, 0).digits |> String.to_integer()
        digit_2 = Enum.at(numbers, 1).digits |> String.to_integer()

        acc + digit_1 * digit_2
      else
        acc
      end
    end)

  acc + res
end)