The problem starts with us receiving an input string of one integer per line, which we need to turn into a sequence
of integers. We solve this with a simple parsing function, parse-measurements
, which splits the input by each line,
and then maps each individual String using the Java function Integer.parseInt
within a lambda call.
(defn parse-measurements [input]
(->> (str/split-lines input)
(map #(Integer/parseInt %))))
The problem needs us to count up the number of times a measurement is greater than the previous measurement. To do this,
we use the partition
function to create sequences of length 2 with a step of 1, such that we get every adjacent pair.
This means that (partition 2 1 (range 4))
would convert (0 1 2 3)
to ((0 1) (1 2) (2 3))
. After that, we apply a
filter for the appropriate pairs, and count them up.
I've shown two ways to implement the filter. In the first instance, I destructured the pair into [a b]
and then
simply checked the predicate (< a b)
. However, even though that syntax works, I thought a partial function would look
nicer, since all we need to check is that "each" value in the pair is less than the next one. Thus the function
(partial apply <)
does the same thing without an explicit lambda. The partial
function says "run this against all
subsequent values you provide as arguments," which again in this case is a single sequence with two values. The
apply
function says to call the <
function on each value within the sequence.
; Original implementation
(defn part1 [input]
(->> (parse-measurements input)
(partition 2 1)
(filter (fn [[a b]] (< a b)))
count))
; Cleaner implementation
(defn part1 [input]
(->> (parse-measurements input)
(partition 2 1)
(filter (partial apply <))
count))
Easy enough. Let's see what part 2 brings us!
This part of the puzzle is very similar to the first, except that instead of checking for increasing values in the measurements themselves, we need to look for increasing values in the rolling sum of three values. There's really nothing to this, if we copy-paste part 1 and make a tiny change.
After we parse the measurements, we need to make a new rolling partition of size 3 and step 1, and then add those
values up. Once we're done and we have a new sequence of integers, we again partition our pairs and do our filter.
I use another partial function, this time mapping each three-tuple to the sum of its values, using
(map (partial apply +) sequence-of-three-tuples)
. Put it together, and we have a working solution.
(defn part2 [input]
(->> (parse-measurements input)
(partition 3 1)
(map (partial apply +))
(partition 2 1)
(filter (partial apply <))
count))
Ok, that worked just fine, but let's do a little cleanup to make things nicer. First of all, while we wait for the
next GA version of Clojure that introduces CLJ-2667, I really hate
having to call (map #(Integer/parseInt %) sequence)
to get to the Java static method
Integer.parseInt
. It's just ugly. So I created a utils namespace to define a nice parse-int
function to use.
(defn parse-int [s] (Integer/parseInt s))
And then, even though it's sort of bad form, I refer
to it in my day01
namespace. It should be in the
clojure.core
namespace and we all know it, so this is me sticking it to the man. Take that, nobody caring at all!
(defn parse-measurements [input]
(->> (str/split-lines input)
(map parse-int)))
Next, let's address the "issue" where parts 1 and 2 are so very similar. In theory, here's what the algorithm should look like:
- Parse the input string into a sequence of integers for each measurement.
- Transform that numeric sequence into a different numeric sequence, based on what each part requires.
- For part 1, no transformation is necessary.
- For part 2, transform that sequence into the sum of the three-measurement sliding window.
- Count the number of adjancent pairs with increasing values.
Piece of cake. Starting with part 1, we'll make our unified solve
function, and have part1
send it the input
string and the identity
function, so the transformation does nothing. For part 2, we'll make a simple function
three-measurement-sliding-window
to create the sliding window, and then send it along.
(defn solve [input mapping]
(->> (parse-measurements input)
(mapping)
(partition 2 1)
(filter (partial apply <))
count))
(defn three-measurement-sliding-window [measurements]
(->> (partition 3 1 measurements)
(map (partial apply +))))
(defn part1 [input] (solve input identity))
(defn part2 [input] (solve input three-measurement-sliding-window))
There we go, all clean. On to day 2!
A coworker of mine correctly observed that for part 2, we don't need to sum the values at all. If the first four
values of the sequence are (a b c d)
, then (< (+ a b c) (+ b c d))
is equivalent to (< a d)
. As a result,
all we need to do is determine how many numbers apart we need to compare values. For part 1, we compare each
adjacent pair, and for part 2 we compare numbers that are 3 spaces apart.
So let's make this happen with a few small revisions to the solve
function. Instead of taking in a mapping function,
we'll instead take in the window-size
we need; part 1 has a window size of two for the adjacent values, and part 2
has a window size of four (one more than the triple). The solve
function parses the measurements, creates
partitions of the correct size, and then calls everyone's favorite function juxt
. The juxt
function applies a
number of functions to an input, returning a vector of the responses. In this case, we'll map (juxt first last)
to
each windowed sequence, such that each sequence returns a two-element vector. From there, we apply the same old
filter, and count up the results.
(defn solve [input window-size]
(->> (parse-measurements input)
(partition window-size 1)
(map (juxt first last))
(filter (partial apply <))
count))
(defn part1 [input] (solve input 2))
(defn part2 [input] (solve input 4))