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

Day 3

day03.livemd

Day 3

Mix.install([
  {:kino, "~> 0.11.3"}
])

Data

input = Kino.Input.textarea("Please paste your input file:")

Solutions

Day 3: Gear Ratios

Here is an example engine schematic:

467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598..

Every other number is adjacent to a symbol and so is a part number; their sum is 4361.

Part Two

Adding up all of the gear ratios produces 467835.

input = Kino.Input.read(input)

defmodule Y2023.D3 do
  @moduledoc """
  https://adventofcode.com/2023/day/3
  """

  def p1(input) do
    input
    |> parse_input()
    |> search_part_lines()
    |> Enum.sum()
  end

  def parse_input(input) do
    input
    |> String.split("\n")
    |> Enum.map(&String.trim/1)
  end

  def search_part_lines(lines), do: search_part_line(lines, nil)

  def search_part_line([line, next | rest], previous) do
    get_part_numbers_in_line(line, previous, next) ++
      search_part_line([next | rest], line)
  end

  def search_part_line([line], previous) do
    get_part_numbers_in_line(line, previous, nil)
  end

  def get_part_numbers_in_line(line, previous, next) do
    line
    |> get_number_indices()
    |> filter_part_numbers(line, previous, next)
    |> indices_to_numbers(line)
  end

  def get_number_indices(line) do
    Regex.scan(~r/\d+/, line, return: :index)
  end

  def filter_part_numbers(indices, line, previous, next) do
    indices
    |> Enum.filter(&is_part_number?(&1, line, previous, next))
  end

  def is_part_number?([index], line, previous, next) do
    symbol_before?(index, line) ||
      symbol_after?(index, line) ||
      symbol_above?(index, previous) ||
      symbol_below?(index, next)
  end

  def symbol_before?({0, _length}, _line), do: false

  def symbol_before?({start, _length}, line) do
    line
    |> String.at(start - 1)
    |> is_symbol?()
  end

  def symbol_after?({start, length}, line) when start + length >= length(line), do: false

  def symbol_after?({start, length}, line) do
    line
    |> String.at(start + length)
    |> is_symbol?()
  end

  def symbol_above?(_, nil), do: false

  def symbol_above?({start, length}, previous) do
    symbol_in_range?(previous, start - 1, length + 2)
  end

  def symbol_below?(_, nil), do: false

  def symbol_below?({start, length}, next) do
    symbol_in_range?(next, start - 1, length + 2)
  end

  def symbol_in_range?(line, -1, length), do: symbol_in_range?(line, 0, length - 1)

  def symbol_in_range?(line, start, length) when start + length >= length(line),
    do: symbol_in_range?(line, start, length - 1)

  def symbol_in_range?(line, start, length) do
    line
    |> String.slice(start, length)
    |> String.split("")
    |> Enum.any?(&is_symbol?/1)
  end

  def is_symbol?("."), do: false
  def is_symbol?(""), do: false
  def is_symbol?(nil), do: false

  def is_symbol?(character) do
    case Integer.parse(character) do
      :error -> true
      _ -> false
    end
  end

  def indices_to_numbers(indices, line) do
    Enum.map(indices, fn [{start, length}] ->
      line
      |> String.slice(start, length)
      |> String.to_integer()
    end)
  end

  def p2(input) do
    input
    |> parse_input()
    |> search_gear_lines()
    |> Enum.filter(fn {_index, numbers} ->
      length(numbers) === 2
    end)
    |> Enum.map(fn {_index, numbers} ->
      Enum.product(numbers)
    end)
    |> Enum.sum()
  end

  def search_gear_lines(lines), do: search_gear_line(lines, nil)

  def search_gear_line([line, next | rest], previous) do
    get_gear_numbers_in_line(line, previous, next) ++
      search_gear_line([next | rest], line)
  end

  def search_gear_line([line], previous) do
    get_gear_numbers_in_line(line, previous, nil)
  end

  def get_gear_numbers_in_line(line, previous, next) do
    line
    |> get_star_indices()
    |> find_adjacent_numbers(line, previous, next)
  end

  def get_star_indices(line) do
    Regex.scan(~r/\*/, line, return: :index)
  end

  def find_adjacent_numbers(indices, line, previous, next) do
    indices
    |> Enum.map(&index_with_adjacent_numbers(&1, line, previous, next))
  end

  def index_with_adjacent_numbers([{index, _}], line, previous, next) do
    {index, adjacent_numbers(index, line, previous, next)}
  end

  def adjacent_numbers(index, line, previous, next) do
    number_before(index, line) ++
      number_after(index, line) ++
      numbers_on_line(index, previous) ++
      numbers_on_line(index, next)
  end

  def number_before(0, _line), do: []

  def number_before(index, line) do
    line
    |> String.slice(0, index)
    |> number_at_end()
    |> Enum.map(&String.to_integer/1)
  end

  def number_at_end(substring) do
    Regex.run(~r/\d+$/, substring) || []
  end

  def number_after(index, line) when index + 1 >= length(line), do: []

  def number_after(index, line) do
    line
    |> String.slice(index + 1, String.length(line))
    |> number_at_start()
    |> Enum.map(&String.to_integer/1)
  end

  def number_at_start(substring) do
    Regex.run(~r/^\d+/, substring) || []
  end

  def numbers_on_line(_index, nil), do: []

  def numbers_on_line(index, previous) do
    numbers_on_line =
      previous
      |> get_number_indices()
      |> Enum.map(fn [{start, length}] ->
        {start, length}
      end)

    spanning_number =
      numbers_on_line
      |> numbers_spanning_index(index)
      |> Enum.map(fn {start, length} ->
        previous
        |> String.slice(start, length)
        |> String.to_integer()
      end)

    if Enum.empty?(spanning_number) do
      number_before(index, previous) ++
        number_after(index, previous)
    else
      spanning_number
    end
  end

  def numbers_spanning_index(numbers_on_line, index) do
    Enum.filter(numbers_on_line, fn {start, length} ->
      start <= index &amp;&amp; start + length > index
    end)
  end
end

Y2023.D3.p1(input) |> IO.inspect()
Y2023.D3.p2(input) |> IO.inspect()

Notes

Someone on Reddit posted the following example test data that captures more of the edge cases:

12.......*..
+.........34
.......-12..
..78........
..*....60...
78.........9
.5.....23..$
8...90*12...
............
2.2......12.
.*.........*
1.1..503+.56

This helped a lot.

Observations

My solution to part 1 was not very helpful for part 2. I think I only reused one function. By the end, I was just trying to get finished, so the code is not very clean or optimized.