Advent of Clojure — Day 2
This is part of my series on the Advent of Clojure.
The puzzle from Day 2 read:
As you walk through the door, a glowing humanoid shape yells in your direction. “You there! Your state appears to be idle. Come help us repair the corruption in this spreadsheet — if we take another millisecond, we’ll have to display an hourglass cursor!”
The spreadsheet consists of rows of apparently-random numbers. To make sure the recovery process is on the right track, they need you to calculate the spreadsheet’s checksum. For each row, determine the difference between the largest value and the smallest value; the checksum is the sum of all of these differences.
For example, given the following spreadsheet:
5 1 9 5
7 5 3
2 4 6 8- The first row’s largest and smallest values are
9
and1
, and their difference is8
.
- The second row’s largest and smallest values are7
and3
, and their difference is4
.
- The third row’s difference is6
.
- In this example, the spreadsheet’s checksum would be8 + 4 + 6 = 18
.What is the checksum for the spreadsheet in your puzzle input?
Each row is independent, so the first step is to take a single row and generate a checksum. What does a row look like? They’re presented as text, but what we really want is a sequence of numbers. So let’s work with that for now, and work on converting the text to numbers later.
Given a seq of numbers, we want the maximum value, the minimum value, and to find the difference. The max
and min
functions take multiple parameters rather than a seq, but a seq can be expanded into the position of parameters with apply
.
If a var called row
contains the numbers in a row, the expression for the maximum would be (apply max row). The entire checksum will then be:
(- (apply max row) (apply min row))
This can be mapped across every row of input, meaning that it needs to be represented as a function. The simplest way is to use the anonymous function syntax, replacing row
with %
. This creates a seq of checksums, which then needs to be summed together. Just like max
and min
, this seq can be passed to the +
function using apply
. Again, we’ll use the threading macro, ->>
so that these operations can be done one step at a time.
Trying this across the example data looks like:
That’s great, so now we need to convert the textual input into these number sequences. First of all, the input needs to be split into lines, which can be done using clojure.string/split
and a regular expression that matches on a newline. I like to refer to the clojure.string
namespace using str
, so this function becomes str/split
. Once the lines have been created, the numbers on each line can be split using the split
function again, using a regular expression that matches on spaces. This second split
needs to be applied via map
on the lines created by the first split
. The result is a seq of lines, where each line is a seq of strings. Finally, this will need a number parser mapped across the line to create the required numbers.
Since the Integer/parseInt
function is a Java interop function, it isn’t represented as a function value that can be passed to map
. A simple wrapper function allows it to be passed in that way, which is defined on line 1 below.
The lines are created with the split
on line 4. Each line is broken up into short strings by mapping the split across all lines on line 5. Finally, the partial
creates a function that will map read-int
over a sequence of these short strings, and this gets mapped across all input lines on line 6.
The data file can be read using slurp
, and then passed to the as-arrays
function to get the required input, when we can then give it to the day2
function.
When applied to the data file I was given, this gave me a result of 44670
, thus leading to the second part of the puzzle:
“Great work; looks like we’re on the right track after all. Here’s a star for your effort.” However, the program seems a little worried. Can programs be worried?
“Based on what we’re seeing, it looks like all the User wanted is some information about the evenly divisible values in the spreadsheet. Unfortunately, none of us are equipped for that kind of calculation — most of us specialize in bitwise operations.”
It sounds like the goal is to find the only two numbers in each row where one evenly divides the other — that is, where the result of the division operation is a whole number. They would like you to find those numbers on each line, divide them, and add up each line’s result.
For example, given the following spreadsheet:
5 9 2 8
9 4 7 3
3 8 6 5In the first row, the only two numbers that evenly divide are
8
and2
; the result of this division is4
.In the second row, the two numbers are
9
and3
; the result is3
.In the third row, the result is
2
.In this example, the sum of the results would be
4 + 3 + 2 = 9
.What is the sum of each row’s result in your puzzle input?
This is exactly the same as the previous question, only using a different checksum calculation. Last time, the checksum was a simple anonymous function, but this time the checksum needs more work, so it will need to be pulled out into its own function.
The first step is to find larger numbers and try to divide the smaller numbers into it. To do this, let’s start by sorting the line from largest to smallest. Then the first (largest) number can be divided by all the numbers after it, and if there are any matches then that identifies the pair where one divides evenly into the other. If not, then the first number can be dropped off, and we can repeat this process, starting now with the second largest number.
Line 2 does the sorting, using the greater-than function >
to provide comparisons, thereby sorting from largest to smallest. Line 3 starts the loop that searches through the numbers in the row, and uses destructuring to get the first and largest number, f
, and all the remaining numbers, r
.
Line 4 is merely a guard that checks if there is anything to look for. This would only fail if nothing were found in the line, which is not supposed to happen, but code should always check for data that wasn’t expected.
Line 5 does the work of trying to divide the remaining numbers into f
. The some
function applies its anonymous function to each of the numbers in r
, returning the first non-nil result. Let’s have a quick look at that anonymous function:
#(when (zero? (mod f %)) (/ f %))
This checks if the number being tested (represented by %
) divides into f
by looking for a mod
of 0. If it fits evenly, then return that number divided into f
. If it doesn’t fit, then the when expression returns nil
. So the call to some
on line 5 will either return the number we’re looking for, or else execution will fall to the recur
, which then loops back to check the sequence starting at the next largest number.
The second day2 function will be identical to the first one, only using this checksum instead of anonymous function used in the first part of the puzzle.
If this were production code, I’d have a single day2
function which would be passed the checksum function to use, but that’s just overengineering when I already have the answer.
My answer was 285.
The final code is here:
Next comes Day 3.