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