19 October 2021

Map Merging in Clojure

How to combine various kinds of data into maps.

Advent of Code 2018 Day 4 presents a few interesting challenges.

First of all, this is the first puzzle (of 2018) with input whose records/lines aren't fully self-contained. For instance, the following three lines all pertained to a single 'shift' by Guard #419:

[1518-02-09 23:59] Guard #419 begins shift
[1518-02-10 00:16] falls asleep
[1518-02-10 00:47] wakes up

The following seven lines all pertain to a shift for Guard #61:

[1518-02-12 00:00] Guard #61 begins shift
[1518-02-12 00:32] falls asleep
[1518-02-12 00:39] wakes up
[1518-02-12 00:52] falls asleep
[1518-02-12 00:53] wakes up
[1518-02-12 00:57] falls asleep
[1518-02-12 00:58] wakes up

I won't bore you with the loop/recur required to parse those records here (source). But the goal of parsing these records was a data structure showing when each guard was asleep. When parsed, the sample data provided with the puzzle description looks like this:

[{10 (range 5, 25)}
 {10 (range 30 55)}
 {99 (range 40 50)}
 {10 (range 24 29)}
 {99 (range 36 46)}
 {99 (range 45 55)}]

Each map has a single key (guard-id) and a sequence of minutes when the corresponding guard was asleep. The next task is to merge that data structure into a single map, with all minutes spent sleeping keyed by guard-id:

{10 (concat (range 5, 25)
            (range 30 55)
            (range 24 29))
 99 (concat (range 40 50)
            (range 36 46)
            (range 45 55))}

As is typical of solving problems in Clojure, there's a function for doing just that:

(defn merge-naps [parsed]
  (apply (partial merge-with concat) parsed))

In a previous post I described my usage of merge-with to achieve what the frequencies functional already does. Now I'm using merge-with in conjunction with concat to build the map needed for this solution. Having a merged map for each guard allows for analysis of their sleep patterns (source) into a more helpful data structure:

; Guard 10
{:checksum                 (* 10 24)
 :total-minutes-slept      50
 :naps-on-sleepiest-minute 2}

; Guard 99
{:checksum                 (* 99 45)
 :total-minutes-slept      30
 :naps-on-sleepiest-minute 3}

From there, we're just a sort-function away, a phrase which here refers to the fact that map keys are also map functions:

(defn sleepiest-guard [sort-fn input]
  (->> (parse-naps input)
       merge-naps
       (map analyze-sleep)
       (sort-by sort-fn)
       last))

(defn part1 [input]
  (:checksum (sleepiest-guard :total-minutes-slept input)))

(defn part2 [input]
  (:checksum (sleepiest-guard :naps-on-sleepiest-minute input)))