Advent Of Code 2023 - Day 3
After an easyish day 2, the difficulty level bumped up a bit again for day 3.
Day 3 - Gear Ratios
See Day 3 for a detailed description of the problem.
To run the Jactl shown here save the code into a file (e.g. advent03.jactl
) and run it like this (where advent03.txt
is your input file from the Advent of Code site for Day 3):
$ cat advent03.txt | java -jar jactl-2.0.0.jar advent03.jactl
Part 1
In part 1 we are given a grid consisting of digits, dots, and symbols (such as ‘+’, ‘#’, ‘*’). The idea is to find in the grid sequences of digits and treat these sequences as a number and sum all numbers in the grid that match the criteria that one of the digits in the number must be adjacent to a symbol. Adjacent, for this puzzle, means any neighbour in the 8 surrounding squares of the grid.
For example:
..123..44.
..+3..*...
.32..677..
..111.....
.......$9.
As usual, for puzzles involving grids, I added a border of dots around the grid to avoid having to check for out-of-bounds conditions when looking for neighbours at the edge of the grid.
Here is the code:
def (rows, D) = [stream(nextLine), [-1,0,1]]
def g = ['.' * (rows[0].size()+2)] + rows.map{ '.'+it+'.' } + ['.' * (rows[0].size()+2)]
def numNearSym(x,y) { g[y][x] =~ /\d/ && D.flatMap{ dx -> D.map{ dy -> [x+dx,y+dy] } }.anyMatch{ x1,y1 -> g[y1][x1] !~ /[.\d]/ } }
g.mapWithIndex{ r,y -> r.size().map{ x -> g[y][x] + (numNearSym(x, y) ? 'S' : '') }.join() }
.flatMap{ it.split(/[^\dS]+/).filter{ 'S' in it }.map{ s/[^\d]+//g }
.map{ it as int } }.sum()
The first line assigns all input lines to rows
and assigns [-1,0,1]
to D
for use when calculating deltas
to x,y
coordinates later.
Then we create our grid of lines in the variable g
, adding a row at the beginning and end and adding a dot to the
beginning and end of each row.
The function numNearSym()
checks if at x,y
we have a digit and then looks for a neighbour that is not a digit and
is not a .
and returns true if we are a digit with such a neighbour.
Then we iterate over the rows using mapWithIndex()
and append S
to each digit in the row that has a neighbouring
symbol.
We take the resulting lines with these S
characters and use the regex split to split the line into a list of substrings
that were separated by anything that was not a digit or S
.
This gives us our candidate numbers.
We filter for only numbers with an embeded S
(since these are the ones with symbol neighbours), remove the S
characters and convert into numbers before summing them.
Part 2
For part 2 we are only interested in numbers that have a neighbour that is a *
and only if that *
has exactly
two numbers for which it is a neighbour.
Then we multiply the two numbers together and sum the resulting products.
01 def lines = stream(nextLine)
02 def g = ['.' * (lines[0].size()+2)] + lines.map{ '.' + it + '.' } + ['.' * (lines[0].size()+2)]
03
04 def nearest2Nums(x,y) {
05 def nums = [[-1,0,1].flatMap{ dx -> [-1,0,1].map{dy -> [x+dx, y+dy] } }
06 .filter{ x1, y1 -> g[y1][x1] =~ /\d/ }
07 .map{ x1,y1 -> [x1 - (x1+1).filter{ g[y1][x1-it] !~ /\d/ }.limit(1)[0] + 1, y1] }
08 .sort().unique()].filter{ it.size() == 2 }[0]
09 nums ? nums.map{ x1,y1 -> g[y1].substring(x1,(g[y1].size()-x1).map{x1+it+1}.filter{ g[y1][it] !~ /\d/ }.limit(1)[0]) as int }
10 .grouped(2).map{ it[0] * it[1] }[0]
11 : null
12 }
13
14 g.size().flatMap{ y -> g[y].size().filter{ g[y][it] == '*' }.flatMap{ nearest2Nums(it, y) } }.sum()
As in part 1, we read the lines and add a border of dots on all sides.
We define a function nearest2Nums(x,y)
that for a given location check if any neighbours are digits (line 6).
Then at line 7 we find the start location in the current row for the number that contains that digit and return the
[x,y]
location for that number.
At line 8 we sort these coordinates and eliminate duplicates (since one number might have multiple digits neighbouring
that ‘*’) and then filter solutions where there are exactly 2 numbers.
Since we have wrapped the result in [
and ]
we actually have a list of a list of coordinate pairs and so the filter
will return either this list (if it has 2 elements) or null if the list is filtered out.
The [0]
unwraps the resulting list back into a simple list of coordinate pairs.
At line 9, if the nums
result was non-null, we map the coordinates into the numbers at those locations by finding
the ending digit, grabbing the substring and converting to an int
.
Line 10 turns the 2 element list into a list of a list of 2 elements so we can multiply the two numbers together.
Line 11 returns null
if the location did not have exactly two neighbouring numbers.
Line 14 searches all grid locations for ‘*’ and uses the nearest2Nums()
function to return null or the product of
the 2 numbers (if they exist).
By using flatMap()
we ignore null
values and can then invoke sum()
to sum everything together.