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 && 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.