Don’t know why the regex ~r/[\W && [^\.]]/x
does not work in Elixir. It works pretty well in Ruby.
Anyway, here is my solution:
Don’t know why the regex ~r/[\W && [^\.]]/x
does not work in Elixir. It works pretty well in Ruby.
Anyway, here is my solution:
I like it, it’s very concise.
Mine is quite long, and this does not even contains the code for the Grid helper
defmodule AdventOfCode.Y23.Day3 do
alias AoC.Input, warn: false
def read_file(file, _part) do
Input.stream!(file, trim: true)
end
def parse_input(input, _part) do
AoC.Grid.parse_stream(input, &parse_char/1)
end
defp parse_char(<<n>>) when n in ?0..?9, do: {:ok, n - ?0}
defp parse_char(<<?.>>), do: :ignore
defp parse_char(<<?*>>), do: {:ok, :gear}
defp parse_char(<<_>>), do: {:ok, :sym}
def part_one(grid) do
grid
|> Enum.filter(fn {xy, val} -> is_integer(val) and sym_neighbour?(grid, xy) end)
|> Enum.map(fn {xy, _} -> first_digit(grid, xy) end)
|> Enum.uniq()
|> Enum.map(&collect_number(grid, &1))
|> Enum.sum()
end
defp sym_neighbour?(grid, xy) do
xy |> AoC.Grid.cardinal8() |> Enum.any?(fn txy -> Map.get(grid, txy) in [:sym, :gear] end)
end
defp first_digit(grid, xy) do
west_xy = AoC.Grid.translate(xy, :w)
case Map.get(grid, west_xy) do
n when is_integer(n) -> first_digit(grid, west_xy)
_ -> xy
end
end
defp collect_number(grid, xy) do
collect_number(grid, xy, [Map.fetch!(grid, xy)])
end
defp collect_number(grid, xy, acc) do
east_xy = AoC.Grid.translate(xy, :e)
case Map.get(grid, east_xy) do
n when is_integer(n) -> collect_number(grid, east_xy, [n | acc])
_ -> acc |> :lists.reverse() |> Integer.undigits()
end
end
# -- Part 2 -----------------------------------------------------------------
def part_two(grid) do
grid
# For each digit in the grid, associate the digit XY with the neighbouring
# gear XY, keeping only digits with such neighbour.
|> Enum.flat_map(fn
{xy, val} when is_integer(val) ->
case gear_neighbour(grid, xy) do
nil -> []
gear_xy -> [{gear_xy, xy}]
end
_ ->
[]
end)
# Group those digits XY by their neighbouring gear XY.
|> Enum.group_by(&elem(&1, 0), &elem(&1, 1))
# Transform the digits XY into the first digit XY of their number
|> Enum.flat_map(fn {_gear_xy, digits_xys} ->
first_digits = digits_xys |> Enum.map(&first_digit(grid, &1)) |> Enum.uniq()
# Keep only the gear groups with exactly two numbers. Collect the actual
# numbers and compute the product.
case first_digits do
[digit_a_xy, digit_b_xy] ->
num_a = collect_number(grid, digit_a_xy)
num_b = collect_number(grid, digit_b_xy)
[num_a * num_b]
_ ->
[]
end
end)
|> Enum.reduce(&Kernel.+/2)
end
defp gear_neighbour(grid, xy) do
xy |> AoC.Grid.cardinal8() |> Enum.find(fn txy -> Map.get(grid, txy) == :gear end)
end
end
But hey, it works Part 2 is solved in 5ms which I am not very satisfied with.
I’m not satisfied with my Part 2, either. It costs 300ms to run on my computer
Wow this one. Not a dataset I’d like to traverse at midnight. But I love LiveBook. I really do.
defmodule AdventOfCode.Y2023.Day03 do
alias AdventOfCode.Helpers.{InputReader, Transformers}
def input, do: InputReader.read_from_file(2023, 3)
def run(input \\ input()) do
input = parse(input)
{run_1(input), run_2(input)}
end
defp run_1(input) do
input
|> Enum.map(fn {v, _, _} -> String.to_integer(v) end)
|> Enum.sum()
end
defp run_2(input) do
input
|> Enum.map(fn {v, _, gear} -> {String.to_integer(v), Enum.uniq(gear)} end)
|> Enum.flat_map(fn {num, gears} ->
gears
|> Enum.map(fn gear ->
{gear, num}
end)
end)
|> Enum.group_by(fn {a, _} -> a end, fn {_, b} -> b end)
|> Map.filter(fn {_, v} -> length(v) == 2 end)
|> Map.values()
|> Enum.map(fn [a, b] -> a * b end)
|> Enum.sum()
end
def parse(data \\ input()) do
grid = data |> Transformers.lines() |> Enum.map(&String.graphemes/1) |> Transformers.grid2d()
0..(grid |> Map.keys() |> Enum.max() |> elem(0))
|> Enum.flat_map(&collect_all(grid, &1))
|> Enum.filter(fn {_, n, _} -> n == true end)
end
defp dirs(x, y) do
[
{x + 1, y},
{x - 1, y},
{x, y + 1},
{x, y - 1},
{x + 1, y + 1},
{x - 1, y - 1},
{x + 1, y - 1},
{x - 1, y + 1}
]
end
@reject MapSet.new(~w/. 1 2 3 4 5 6 7 8 9 0/)
defp is_part(grid, {x, y}) do
dirs(x, y)
|> Enum.map(&grid[&1])
|> Enum.reject(&(is_nil(&1) || MapSet.member?(@reject, &1)))
|> Enum.empty?()
|> Kernel.not()
end
defp get_gears(grid, {x, y}), do: Enum.filter(dirs(x, y), &(grid[&1] == "*"))
defp collect_one(grid, pos), do: collect_one(grid[pos], grid, pos, "", false, [])
defp collect_one(cur, grid, {x, y}, digits, part?, gears) when cur in ~w/1 2 3 4 5 6 7 8 9 0/ do
collect_one(
grid[{x, y + 1}],
grid,
{x, y + 1},
digits <> cur,
part? || is_part(grid, {x, y}),
get_gears(grid, {x, y}) ++ gears
)
end
defp collect_one(nil, _, {_, _}, digits, part?, gears), do: {:halt, digits, part?, gears}
defp collect_one(_, _, {_, y}, digits, part?, gears), do: {{:cont, y}, digits, part?, gears}
defp collect_all(grid, row), do: collect_all(grid, row, 0, [])
defp collect_all(grid, row, col, numbers) do
case collect_one(grid, {row, col}) do
{:halt, "", _, _} ->
numbers
{_, "", _, _} ->
collect_all(grid, row, col + 1, numbers)
{:halt, number, part?, gears} ->
[{number, part?, gears} | numbers]
{{:cont, next}, number, part?, gears} ->
collect_all(grid, row, next, [{number, part?, gears} | numbers])
end
end
end
This one took me two hours continuously to get working.
contains_symbol = fn s ->
match = Regex.match?(~r/[^\d.]/, s)
match
end
grab_numbers = fn line ->
Regex.scan(~r/\d+/, line, return: :index)
|> Enum.map(fn [{index, len}] ->
{index, len, line |> String.slice(index, len) |> Integer.parse() |> elem(0)}
end)
end
part1 = fn s ->
for [pre, curr, post] <-
s
|> Enum.reject(&(String.length(&1) == 0))
|> then(fn x -> Enum.concat([""], x) end)
|> Enum.chunk(3, 1)
|> Enum.map(fn u -> Enum.map(u, &String.trim/1) end),
[{index, length}] <- Regex.scan(~r/\d+/, curr, return: :index),
contains_symbol.(String.slice(curr, max(index - 1, 0), length + 2)) ||
contains_symbol.(String.slice(pre, max(index - 1, 0), length + 2)) ||
contains_symbol.(String.slice(post, max(index - 1, 0), length + 2)),
{number, _} = String.slice(curr, index, length) |> Integer.parse() do
number
end
|> Enum.sum()
end
part2 = fn s ->
for [pre, curr, post] <-
s
|> Enum.reject(&(String.length(&1) == 0))
|> Enum.chunk(3, 1)
|> Enum.map(fn u -> Enum.map(u, &String.trim/1) end),
curr |> String.contains?("*"),
[{gear_idx, _}] <- Regex.scan(~r/\*/, curr, return: :index),
matches =
grab_numbers.(pre) |> Enum.concat(grab_numbers.(post)) |> Enum.concat(grab_numbers.(curr)),
valid_matches =
matches
|> Enum.filter(fn {m_idx, m_len, _} ->
gear_idx in max(0, m_idx - 1)..(m_idx + m_len)
end),
valid_matches |> Enum.count() == 2,
[{_, _, a}, {_, _, b}] = valid_matches do
a * b
end
|> Enum.sum()
end
part1.(IO.stream()) |> IO.puts()
I’m not proud of this code, but hey, it works and it’s fast.
I got tripped up on an off-by-one error with my ranges in part 1. That’s what I get for staying up late to do this one.
Not super happy about that one, I guess the parser emission could’ve been more elegant
Performance of list_adjecent_numbers
is bad but it works…
My beginner’s solution, Day 3 part 1.
My focus was on readability. How I did?
defmodule Day03 do
def part1(input) do
# " 467..
# ...*.
# ..35 "
schematic = input
|> String.split("\n")
|> Enum.map(&String.graphemes/1)
# [ ["4", "6", "7", ".", "."],
# [".", ".", ".", "*", "."],
# [".", ".", "3", "5", "."] ]
matrix_of_positions_adjecent_to_symbols(schematic)
# [ [false, false, true, true, true],
# [false, false, true, false, true],
# [false, false, true, true, true] ]
|> Enum.zip(schematic)
|> Enum.map(fn {m, s} -> Enum.zip(s, m) end)
# [ [{"4", false}, {"6", false}, {"7", true}, {".", true}, {".", true}],
# [{".", false}, {".", false}, {".", true}, {"*", false}, {".", true}],
# [{".", false}, {".", false}, {".", true}, {"3", true}, {"5", true}] ]
|> Enum.map(&extract_valid_numbers_from_zipped_row/1)
|> List.flatten
|> Enum.sum
end
def matrix_of_positions_adjecent_to_symbols(schematic) do
blank_matrix = List.duplicate(false, 140)
|> List.duplicate(140)
# [ [false, false, false, false, false],
# [false, false, false, false, false],
# [false, false, false, false, false] ]
schematic
|> Enum.map(&Enum.with_index/1)
|> Enum.with_index
# [ {[{"4", 0}, {"6", 1}, {"7", 2}, {".", 3}, {".", 4}], 0},
# {[{".", 0}, {".", 1}, {".", 2}, {"*", 3}, {".", 4}], 1},
# {[{".", 0}, {".", 1}, {".", 2}, {"3", 3}, {"5", 4}], 2} ]
|> Enum.reduce(blank_matrix,
fn {row, row_index}, acc ->
Enum.reduce(row, acc,
fn {p, index}, acc ->
cond do
is_symbol?(p) -> encrircle_with_true_at(acc, row_index, index)
true -> acc
end
end)
end)
# [ [false, false, true, true, true],
# [false, false, true, false, true],
# [false, false, true, true, true] ]
end
def is_symbol?(grapheme) do
["*", "&", "@", "/", "+", "-", "%", "$", "=", "#"]
|> Enum.member?(grapheme)
end
def is_digit?(grapheme) do
["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|> Enum.member?(grapheme)
end
def encrircle_with_true_at(matrix, row, pos) do
matrix
|> replace_with_true_at(row-1, pos-1)
|> replace_with_true_at(row-1, pos)
|> replace_with_true_at(row-1, pos+1)
|> replace_with_true_at(row, pos-1)
|> replace_with_true_at(row, pos+1)
|> replace_with_true_at(row+1, pos-1)
|> replace_with_true_at(row+1, pos)
|> replace_with_true_at(row+1, pos+1)
end
def replace_with_true_at(matrix, row, position) do
matrix
|> List.pop_at(row)
|> elem(0)
|> List.replace_at(position, true)
|> then(&List.replace_at(matrix, row, &1))
end
def extract_valid_numbers_from_zipped_row(row) do
# [{"4", false}, {"6", false}, {"7", true}, {".", true}, {".", true}]
row
|> Enum.chunk_by(fn {n, _} -> is_digit?(n) end)
|> Enum.filter(fn x -> x |> List.first |> elem(0) |> is_digit? end)
|> Enum.map(&Enum.unzip/1)
# [{["4", "6", "7"], [false, false, true]}, {[...],[...]}, ...]
|> Enum.filter(fn {_, truth_list} -> Enum.any?(truth_list) end)
|> Enum.map(fn x -> x
|> elem(0)
|> Enum.join
|> String.to_integer
end)
end
end
Preprocessing:
defmodule Day03 do
def parse_line("", {_, _, acc}), do: acc
def parse_line("." <> rest, {x, y, acc}), do: parse_line(rest, {x + 1, y, acc})
def parse_line(<<c>> <> _ = str, {x, y, items}) when c in ?0..?9 do
{num, rest} = Integer.parse(str)
len = floor(:math.log10(num) + 1)
ref = make_ref()
points =
for i <- 0..(len - 1) do
{{x + i, y}, {ref, num}}
end
parse_line(rest, {x + len, y, points ++ items})
end
def parse_line(<<c>> <> rest, {x, y, items}) do
parse_line(rest, {x + 1, y, [{{x, y}, <<c>>} | items]})
end
def around({x, y}) do
for dx <- -1..1, dy <- -1..1, do: {x + dx, y + dy}
end
end
grid =
puzzle_input
|> String.split()
|> Enum.with_index()
|> Enum.flat_map(fn {line, y} ->
Day03.parse_line(line, {0, y, []})
end)
{parts, ids} =
grid
|> Enum.split_with(fn {_, value} ->
is_binary(value)
end)
parts = Map.new(parts)
ids = Map.new(ids)
Part 1:
used_parts =
for {xy, _} <- parts,
dxy <- Day03.around(xy),
{:ok, val} <- [Map.fetch(ids, dxy)],
into: %{},
do: val
Enum.sum(Map.values(used_parts))
Part 2:
cogs =
for {xy, _} <- parts,
values =
Map.take(ids, Day03.around(xy)) |> Map.values() |> Enum.uniq() |> Enum.map(&elem(&1, 1)),
match?([_, _], values),
do: Enum.product(values)
Enum.sum(cogs)
My solution:
I have to admit, my favorite part of these puzzles is
Essentially, seeing if I can implement good abstractions upfront. Today was a good day for that!
The datastructure I chose to use here was {symbols, numbers}
, each as maps, with their indexed location in the grid as the keys and the parsed data from the grid as the values. In the case of numbers, their column index is a range representing their span in the grid.
@type index :: non_neg_integer()
@type symbol_index :: {row_num :: index(), col_num :: index()}
@type symbols :: %{
symbol_index() => symbol :: String.t()
}
@type number_index :: {row_num :: index(), col_num :: Range.t()}
@type numbers :: %{
number_index() => number :: non_neg_integer()
}
@type input :: {symbols(), numbers()}
{symbols, numbers} = {%{
{1, 3} => "*",
{3, 6} => "#",
{4, 3} => "*",
{5, 5} => "+",
{8, 3} => "$",
{8, 5} => "*"
},
%{
{0, 0..2} => 467,
{0, 5..7} => 114,
{2, 2..3} => 35,
{2, 6..8} => 633,
{4, 0..2} => 617,
{5, 7..8} => 58,
{6, 2..4} => 592,
{7, 6..8} => 755,
{9, 1..3} => 664,
{9, 5..7} => 598
}}
Regex.scan(regex, string, return: :index)
carried the day. I never really use it for anything but programming puzzles where I am indexing into a string grid, but boy is it handy here.
Source code available here.
defmodule AoC.Day.Three.Input do
def parse(input_file \\ System.fetch_env!("INPUT_FILE")) do
input_file
|> File.read!()
|> String.split("\n")
|> Enum.with_index()
|> Enum.reduce({%{}, %{}}, fn {row, y}, {symbols, numbers} ->
symbols = scan_symbols({row, y}, symbols)
numbers = scan_numbers({row, y}, numbers)
{symbols, numbers}
end)
end
def scan_symbols({row, y}, symbols) do
Regex.scan(~r{[^\d\.]}, row, return: :index)
|> List.flatten()
|> Enum.reduce(symbols, fn {x, 1}, symbols ->
Map.put(symbols, {y, x}, String.at(row, x))
end)
end
def scan_numbers({row, y}, numbers) do
Regex.scan(~r{\d+}, row, return: :index)
|> List.flatten()
|> Enum.reduce(numbers, fn {x, length}, numbers ->
x_start = x
x_end = x + length - 1
number =
row
|> String.slice(x_start..x_end)
|> String.to_integer()
Map.put(numbers, {y, x_start..x_end}, number)
end)
end
end
My adjacency check here is not as inefficient as scanning the whole grid ever time, but still suboptimal. It was easy and fun to implement, thought. It checks every symbol against every number.
A more optimal approach would probably be to generate the indices of every possible symbol location around each number, and do a lookup to see if there was an entry (or vice-versa).
I don’t love the variable names I came up with in the adjacency logic, but readability is less at a premium in these sorts of application as being able to see where the different - 1
s and + 1
s are, and terseness helps there.
Source code available here.
defmodule AoC.Day.Three.Part.One do
def solve({symbols, numbers}) do
symbols
|> Enum.map(&adjacent_numbers_indices(&1, numbers))
|> List.flatten()
|> Enum.uniq()
|> Enum.map(&Map.get(numbers, &1))
|> Enum.sum()
end
def adjacent_numbers_indices({symbol_index, _symbol}, numbers) do
numbers
|> Map.keys()
|> Enum.filter(&adjacent?(symbol_index, &1))
end
# The geometry of considers 8 neighbours to be adjacent, cardinals and diagonals
# We hardcode these in function heads, going clockwise, starting north
def adjacent?(symbol_index, number_index)
# North
def adjacent?({s_y, s_x}, {n_y, n_xs..n_xe})
when s_y + 1 == n_y and s_x in n_xs..n_xe,
do: true
# North-East
def adjacent?({s_y, s_x}, {n_y, n_xs..n_xe})
when s_y + 1 == n_y and (s_x + 1) in n_xs..n_xe,
do: true
# East
def adjacent?({s_y, s_x}, {n_y, n_xs..n_xe})
when s_y == n_y and (s_x + 1) in n_xs..n_xe,
do: true
# South-East
def adjacent?({s_y, s_x}, {n_y, n_xs..n_xe})
when s_y - 1 == n_y and (s_x + 1) in n_xs..n_xe,
do: true
# South
def adjacent?({s_y, s_x}, {n_y, n_xs..n_xe})
when s_y - 1 == n_y and s_x in n_xs..n_xe,
do: true
# South-West
def adjacent?({s_y, s_x}, {n_y, n_xs..n_xe})
when s_y - 1 == n_y and (s_x - 1) in n_xs..n_xe,
do: true
# West
def adjacent?({s_y, s_x}, {n_y, n_xs..n_xe})
when s_y == n_y and (s_x - 1) in n_xs..n_xe,
do: true
# North-West
def adjacent?({s_y, s_x}, {n_y, n_xs..n_xe})
when s_y + 1 == n_y and (s_x - 1) in n_xs..n_xe,
do: true
def adjacent?(_symbol_index, _number_index), do: false
end
Exactly what you want to see: use the same datastructure you developed for part one, use functions from part one to solve! Source code available here.
defmodule AoC.Day.Three.Part.Two do
@gear "*"
import AoC.Day.Three.Part.One
def solve({symbols, numbers}) do
symbols
|> Enum.filter(fn {_index, symbol} -> symbol == @gear end)
|> Enum.map(&adjacent_numbers_indices(&1, numbers))
|> Enum.filter(&(length(&1) == 2))
|> Enum.reduce(0, fn [gear1, gear2], acc ->
acc + numbers[gear1] * numbers[gear2]
end)
end
end
I did something I didn’t see in other solutions. I created an index of positions for all the numbers, but also added a unique id to each number. That way, the number 467 can be indexed as [{{0,0}, {id_1, 467}}, {{0,1}, {id_1, 467}}, {{0,2}, {id_1, 467}}]
That way, adjacency is trivial, and then deduplicating numbers is also trivial (as the same number might be adjacent to a symbol multiple times)
Solution: https://github.com/juanperi/advent_of_code/blob/master/2023/day_03.ex
I have not yet looked at the solutions in this thread but I just wanted to state that for day 3 this was a very difficult challenge relative to previous years. I think there may have been an effort to handicap the AI coders with the difficult input parsing this year.
It could also be because it’s Sunday, and they’re typically more involved.
I skipped part 2 today, not a fan of these grid challenges in Elixir. I did get part 1 running in 3ms using :ets for constant time lookups though. And I wrote a crazy single-pass binary pattern-matching recursive function to parse the input that I’ll spare you all from seeing
Finally managed to get part 2 correct after completely redoing my parser approach. I still don’t understand the bug my original approach had as it worked for part 1 and the example input for part 2. Still, the second attempt is much cleaner (not CLEAN, but not the mess of spaghetti my first attempt was).
Full code here. Just the parser:
defmodule Parser do
alias AocToolbox.Input
defstruct [
:max_ndx,
line: "",
row: 0,
ndx: 0,
graph: %{},
num_map: %{},
symbol_neighbors_map: %{}
]
def parse_raw_graph(text) do
text
|> Input.lines()
|> Enum.with_index()
|> Enum.map(fn {line, row} ->
parse_lines(%__MODULE__{line: line, row: row, max_ndx: String.length(line)})
end)
|> Enum.reduce(fn row, acc ->
row
|> Enum.map(fn {key, val} ->
{key, Map.merge(val, acc[key], fn _, v1, v2 -> MapSet.union(v1, v2) end)}
end)
end)
end
def parse_lines(%__MODULE__{
ndx: ndx,
max_ndx: max_ndx,
graph: graph,
num_map: num_map,
symbol_neighbors_map: symbol_neighbors_map
})
when ndx > max_ndx,
do: [graph: graph, num_map: num_map, symbol_neighbors_map: symbol_neighbors_map]
def parse_lines(%__MODULE__{
line: <<".", rest::binary>>,
row: row,
ndx: ndx,
max_ndx: max_ndx,
graph: graph,
num_map: num_map,
symbol_neighbors_map: symbol_neighbors_map
}),
do:
parse_lines(%__MODULE__{
line: rest,
row: row,
ndx: ndx + 1,
max_ndx: max_ndx,
graph: Map.put(graph, {row, ndx}, nil),
num_map: num_map,
symbol_neighbors_map: symbol_neighbors_map
})
def parse_lines(%__MODULE__{
line: <<?*, rest::binary>>,
row: row,
ndx: ndx,
max_ndx: max_ndx,
graph: graph,
num_map: num_map,
symbol_neighbors_map: symbol_neighbors_map
}) do
parse_lines(%__MODULE__{
line: rest,
row: row,
ndx: ndx + 1,
max_ndx: max_ndx,
graph: Map.put(graph, {row, ndx}, :gear),
num_map: num_map,
symbol_neighbors_map:
update_symbol_neighbors(symbol_neighbors_map, row, ndx, max_ndx, :gear)
})
end
def parse_lines(%__MODULE__{
line: <<d, rest::binary>>,
row: row,
ndx: ndx,
max_ndx: max_ndx,
graph: graph,
num_map: num_map,
symbol_neighbors_map: symbol_neighbors_map
})
when d > 47 and d < 58 do
{num, rem_line, last_ndx} = parse_nums(d - 48, rest, ndx)
graph =
ndx..last_ndx
|> Enum.reduce(graph, fn n, g -> Map.update(g, {row, n}, num, fn _ -> num end) end)
num_map =
all_associations(ndx, last_ndx, row)
|> Map.merge(num_map, fn _, v1, v2 -> MapSet.union(v1, v2) end)
parse_lines(%__MODULE__{
line: rem_line,
row: row,
ndx: last_ndx + 1,
max_ndx: max_ndx,
graph: graph,
num_map: num_map,
symbol_neighbors_map: symbol_neighbors_map
})
end
def parse_lines(%__MODULE__{
line: "",
graph: graph,
num_map: num_map,
symbol_neighbors_map: symbol_neighbors_map
}),
do: [graph: graph, num_map: num_map, symbol_neighbors_map: symbol_neighbors_map]
def parse_lines(%__MODULE__{
line: <<s, rest::binary>>,
row: row,
ndx: ndx,
max_ndx: max_ndx,
graph: graph,
num_map: num_map,
symbol_neighbors_map: symbol_neighbors_map
}),
do:
parse_lines(%__MODULE__{
line: rest,
row: row,
ndx: ndx + 1,
max_ndx: max_ndx,
graph: Map.put(graph, {row, ndx}, :symbol),
num_map: num_map,
symbol_neighbors_map:
update_symbol_neighbors(symbol_neighbors_map, row, ndx, max_ndx, :symbol)
})
defp parse_nums(acc, <<d, rest::binary>>, ndx) when d > 47 and d < 58 do
parse_nums(acc * 10 + d - 48, rest, ndx + 1)
end
defp parse_nums(acc, bin, ndx), do: {acc, bin, ndx}
defp all_associations(start, stop, row) do
start..stop
|> Enum.to_list()
|> AocToolbox.Math.permutations()
|> Enum.map(fn [a | rest] ->
{{row, a}, MapSet.new(Enum.map(rest, fn b -> {row, b} end))}
end)
|> Enum.into(%{})
end
defp update_symbol_neighbors(symbol_neighbors_map, row, ndx, max_ndx, symbol) do
for i <- -1..1,
j <- -1..1,
not (i == 0 and j == 0),
row + i >= 0,
ndx + j >= 0,
ndx + j <= max_ndx,
reduce: symbol_neighbors_map do
acc ->
Map.update(acc, {symbol, {row, ndx}}, MapSet.new([{row + i, ndx + j}]), fn curr ->
MapSet.put(curr, {row + i, ndx + j})
end)
end
end
For part 1 I decided to try a “sliding window” approach. After splitting into lines, the code “slides” a window over three adjacent lines simultaneously looking for a number on the second line that is has a symbol somewhere around it. This limits the amount of parsing done and yes, it’s not very reusable for the second part (which I may not finish today).
It’s not super pretty, but it does run in about 3.5ms on my MacBook Pro (M1 Max). And I do think its quite easy to comprehend.
def part_1 do
input = parse_input()
find_parts(input, 0, tl(input))
end
def parse_input do
list = String.split(@input, "\n", trim: true)
length = String.length(hd(list))
header = String.duplicate(".", length)
[header | list] ++ [header]
|> Enum.map(&("." <> &1 <> "."))
end
# Guards used for surrounding symbol detection
defguard are_digits(c1, c2, c3) when
(c1 in ?0..?9 and c2 in ?0..?9 and c3 in ?0..?9)
defguard are_digits(c1, c2) when
(c1 in ?0..?9 and c2 in ?0..?9)
defguard are_digits(c1) when
(c1 in ?0..?9)
defguard is_surrounded_by_symbol(l1c1, l1c2, l1c3, l1c4, l1c5, l3c1, l3c2, l3c3, l3c4, l3c5, l2c1, l2c5) when
(l1c1 != ?. or l1c2 != ?. or l1c3 != ?. or l1c4 != ?. or l1c5 != ?. or
l3c1 != ?. or l3c2 != ?. or l3c3 != ?. or l3c4 != ?. or l3c5 != ?. or
l2c1 != ?. or l2c5 != ?.)
defguard is_surrounded_by_symbol(l1c1, l1c2, l1c3, l1c4, l3c1, l3c2, l3c3, l3c4, l2c1, l2c4) when
(l1c1 != ?. or l1c2 != ?. or l1c3 != ?. or l1c4 != ?. or
l3c1 != ?. or l3c2 != ?. or l3c3 != ?. or l3c4 != ?. or
l2c1 != ?. or l2c4 != ?.)
defguard is_surrounded_by_symbol(l1c1, l1c2, l1c3, l3c1, l3c2, l3c3, l2c1, l2c3) when
(l1c1 != ?. or l1c2 != ?. or l1c3 != ?. or
l3c1 != ?. or l3c2 != ?. or l3c3 != ?. or
l2c1 != ?. or l2c3 != ?.)
# Three digit number
def find_parts([<<l1c1, l1c2, l1c3, l1c4, l1c5, l1_rest::binary>>, <<l2c1, l2c2, l2c3, l2c4, l2c5, l2_rest::binary>>, <<l3c1, l3c2, l3c3, l3c4, l3c5, l3_rest::binary>> | rest], acc, tail)
when are_digits(l2c2, l2c3, l2c4) and
is_surrounded_by_symbol(l1c1, l1c2, l1c3, l1c4, l1c5, l3c1, l3c2, l3c3, l3c4, l3c5, l2c1, l2c5) do
acc = ((l2c2 - ?0) * 100) + ((l2c3 - ?0) * 10) + (l2c4 - ?0) + acc
find_parts([<<l1c5, l1_rest::binary>>, <<l2c5, l2_rest::binary>>, <<l3c5, l3_rest::binary>> | rest], acc, tail)
end
# Three digit number but no symbol, skip over the number
def find_parts([<<_, _, _, _, l1c5, l1_rest::binary>>, <<_, l2c2, l2c3, l2c4, l2c5, l2_rest::binary>>, <<_, _, _, _, l3c5, l3_rest::binary>> | rest], acc, tail)
when are_digits(l2c2, l2c3, l2c4) do
find_parts([<<l1c5, l1_rest::binary>>, <<l2c5, l2_rest::binary>>, <<l3c5, l3_rest::binary>> | rest], acc, tail)
end
# Two digit number surrounded by a symbol
def find_parts([<<l1c1, l1c2, l1c3, l1c4, l1_rest::binary>>, <<l2c1, l2c2, l2c3, l2c4, l2_rest::binary>>, <<l3c1, l3c2, l3c3, l3c4, l3_rest::binary>> | rest], acc, tail)
when are_digits(l2c2, l2c3) and
is_surrounded_by_symbol(l1c1, l1c2, l1c3, l1c4, l3c1, l3c2, l3c3, l3c4, l2c1, l2c4) do
acc = ((l2c2 - ?0) * 10) + (l2c3 - ?0) + acc
find_parts([<<l1c4, l1_rest::binary>>, <<l2c4, l2_rest::binary>>, <<l3c4, l3_rest::binary>> | rest], acc, tail)
end
# Two digit number but no symbol, skip over the number
def find_parts([<<_, _, _, l1c4, l1_rest::binary>>, <<_, l2c2, l2c3, l2c4, l2_rest::binary>>, <<_, _, _, l3c4, l3_rest::binary>> | rest], acc, tail)
when are_digits(l2c2, l2c3) do
find_parts([<<l1c4, l1_rest::binary>>, <<l2c4, l2_rest::binary>>, <<l3c4, l3_rest::binary>> | rest], acc, tail)
end
# One digit number surrounded with a symbol
def find_parts([<<l1c1, l1c2, l1c3, l1_rest::binary>>, <<l2c1, l2c2, l2c3, l2_rest::binary>>, <<l3c1, l3c2, l3c3, l3_rest::binary>> | rest], acc, tail)
when are_digits(l2c2) and
is_surrounded_by_symbol(l1c1, l1c2, l1c3, l3c1, l3c2, l3c3, l2c1, l2c3) do
acc = (l2c2 - ?0) + acc
find_parts([<<l1c3, l1_rest::binary>>, <<l2c3, l2_rest::binary>>, <<l3c3, l3_rest::binary>> | rest], acc, tail)
end
# Move the window across one character
def find_parts([<<_, rest_1::binary>>, <<_, rest_2::binary>>, <<_, rest_3::binary>> | rest], acc, tail) do
find_parts([rest_1, rest_2, rest_3 | rest], acc, tail)
end
# Move the window down one line
def find_parts(["", "", "" | _rest], acc, [line_1, line_2, line_3 | tail]) do
find_parts([line_1, line_2, line_3], acc, [line_2, line_3 | tail])
end
# End of input, return the accumulator
def find_parts([_, _, _], acc, _tail) do
acc
end
I did the same, just with make_ref()
instead of number.