|
| 1 | +# Day Fourteen: Docking Data |
| 2 | + |
| 3 | +* [Problem statement](https://adventofcode.com/2020/day/14) |
| 4 | +* [Solution code](https://github.com/abyala/advent-2020-clojure/blob/master/src/advent_2020_clojure/day14.clj) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +Ok, I'm back to liking Advent Of Code again, after the Day That Must Not Be Named. Today we're writing |
| 9 | +another program that acts as a program, and while not always the most challenging problems, I find |
| 10 | +them relaxing and fun. |
| 11 | + |
| 12 | +I gave a hint in Day 12 that whenever we see a program with an instruction set, that means the |
| 13 | +instructions are probably going to change between parts 1 and 2, or in a future day's puzzles. Today |
| 14 | +was no exception, so the code was structured to prepare for it. |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## Part 1 |
| 19 | + |
| 20 | +We're introduced to a bitmask system, wherein our instruction set contains lines that set a 36-bit |
| 21 | +bitmask, and a set of instructions on how to update memory registers. The goal is to mask the data |
| 22 | +against the mask before setting it into the register. When we're done, we add up the values of |
| 23 | +registers. |
| 24 | + |
| 25 | +First, let's get the state to pass around from instruction to instruction, which will be a map of |
| 26 | +`:mask` as a String, and `:memory` as a map of memory address to its numeric value. Even though the |
| 27 | +addresses look like numbers, we're not manipulating them as such, so the keys of `:memory` will be |
| 28 | +strings. So let's get our initializers ready. |
| 29 | + |
| 30 | +```clojure |
| 31 | +; State: {:mask "", :memory {"n" v}} |
| 32 | +(def empty-mask "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX") |
| 33 | +(def empty-state {:mask empty-mask :memory {}}) |
| 34 | +``` |
| 35 | + |
| 36 | +Next, let's get some helper functions to convert numeric Strings into 36-character, zero-padded |
| 37 | +binary representations. We're mostly going to use Java's `Long` class here, first parsing the |
| 38 | +String into a `Long`, converting it its its binary form with `toBinaryString`, and finally applying |
| 39 | +a String formatter. Java _really_ puts up a fight with formatting numeric Strings, so I had to |
| 40 | +pad with spaces, and then use `str/replace` to turn them into zeros. A little yucky, but that's why |
| 41 | +we make small functions! |
| 42 | + |
| 43 | +After that, just to keep things clear, `binary-to-long` just turns a binary string back into a `Long`. |
| 44 | + |
| 45 | +```clojure |
| 46 | +(defn zero-pad [s] |
| 47 | + (let [space-padded (->> s Long/parseLong Long/toBinaryString (format "%36s"))] |
| 48 | + (str/replace space-padded \space \0))) |
| 49 | + |
| 50 | +(defn binary-to-long [b] |
| 51 | + (Long/parseLong b 2)) |
| 52 | +``` |
| 53 | + |
| 54 | +I then need one more function to move this along - `mask-value` takes in a numeric String value `v` |
| 55 | +and the current `mask`, and returns the `Long` value after applying the mask. Nothing fancy here - |
| 56 | +turn the decimal String into a binary String, overwrite all values with non-`X` mask characters, |
| 57 | +and turn that back into a Long. |
| 58 | + |
| 59 | +```clojure |
| 60 | +(defn mask-value [v mask] |
| 61 | + (->> (zero-pad v) |
| 62 | + (map (fn [m v] (if (= \X m) v m)) mask) |
| 63 | + (apply str) |
| 64 | + binary-to-long)) |
| 65 | +``` |
| 66 | + |
| 67 | +I've got a cool function coming up, but for now, let's think about the two operations our input can take - |
| 68 | +update the mask and set the bitmasked value into a register. `update-mask` and `set-bitmasked-value` |
| 69 | +hit the spot, simply associating the correct values into the state. |
| 70 | + |
| 71 | +```clojure |
| 72 | +(defn update-mask [state mask] |
| 73 | + (assoc state :mask mask)) |
| 74 | + |
| 75 | +(defn set-bitmasked-value [state addr v] |
| 76 | + (assoc-in state [:memory addr] (mask-value v (:mask state)))) |
| 77 | +``` |
| 78 | + |
| 79 | +I want to add a little suspense, so let's get to the `solve` function first. Looking ahead a tiny bit |
| 80 | +to Part 2, we learn that the `mem` command is going to be different between parts 1 and 2, so let's |
| 81 | +figure out the overall algorithm. We're going to parse the input data and reduce the state through |
| 82 | +each line, calling `process-line` each time. At the end, we'll grab the `:memory` of the final state, |
| 83 | +strip away the values, and add them all together. |
| 84 | + |
| 85 | +```clojure |
| 86 | +(defn solve [input mem-command] |
| 87 | + (->> (str/split-lines input) |
| 88 | + (reduce #(process-line %2 %1 mem-command) empty-state) |
| 89 | + :memory |
| 90 | + vals |
| 91 | + (apply +))) |
| 92 | +``` |
| 93 | + |
| 94 | +Ok, now for the fun part -- the `process-line` function. Given the current `line` of text, the `state` of |
| 95 | +the application, and the `mem-command` (function to apply to a `mem` operation), process the line and |
| 96 | +return the new state. For this, we want to use `re-matches` to find which regular expression matches |
| 97 | +the line of text. There are only two possible regexes to use, but let's pretend there could be more. |
| 98 | +In my `advent_2016_clojure` day 21 solution, I would have used a bunch of `when-let` blocks, thrown |
| 99 | +together with an `or` to return the first non-`nil` value, as such: |
| 100 | + |
| 101 | +```clojure |
| 102 | +(or (when-let [args (re-matches #"regex1" line)] |
| 103 | + (first-function args)) |
| 104 | + (when-let [args (re-matches #"regex2" line)] |
| 105 | + (second-function args))) |
| 106 | +``` |
| 107 | + |
| 108 | +But I recently learned about the `condp` function, which is a perfect match. It takes the form |
| 109 | +`(condp f arg2 arg1-a fun-a arg1-b fun-b ... arg1-n fun-n)`. It applies the function `f` to |
| 110 | +`arg1-a` and `arg2`, and if the result is non-`nil`, then it runs the next clause. If it's `nil`, |
| 111 | +then try again with `arg1-b`. It's like a normal `condp`, but it defines the structure to apply |
| 112 | +each time for each condition. In this case, we want to apply a different regular expression to |
| 113 | +the `line` argument, and whichever one matches, we should apply a different function. Then `:>>`, |
| 114 | +which I think I'll call the "double-chin," just feeds the result of the left-hand side to the |
| 115 | +right-hand expression. So all together, `process-line` takes in the `line`, runs `condp` to |
| 116 | +identify the matching regular expression, and then calls either `update-mask` or `mem-command` |
| 117 | +on the arguments. |
| 118 | + |
| 119 | +```clojure |
| 120 | +(defn process-line [line state mem-command] |
| 121 | + (condp re-matches line |
| 122 | + #"mask = (\w+)" :>> (fn [[_ mask]] (update-mask state mask)) |
| 123 | + #"mem\[(\d+)\] = (\d+)" :>> (fn [[_ addr v]] (mem-command state addr v)))) |
| 124 | +``` |
| 125 | + |
| 126 | +Last step -- `part1` calls `solve`, using `set-bitmasked-value` as the function to call when |
| 127 | +the `line` matches the `mem` operation. |
| 128 | + |
| 129 | +```clojure |
| 130 | +(defn part1 [input] (solve input set-bitmasked-value)) |
| 131 | +``` |
| 132 | + |
| 133 | +--- |
| 134 | + |
| 135 | +## Part 2 |
| 136 | + |
| 137 | +As already stated earlier, part 2 is just like part 1, except that the `mem` operation now masks |
| 138 | +the `mem` address into 1 or more addresses, and sets them all to the value on the `mem` line. |
| 139 | +I won't go into the details of the rules, but suffice it to say we're going to define a function |
| 140 | +called `set-bitmasked-addresses`, in contrast with part 1's `set-bitmasked-value`, to feed into |
| 141 | +the `solve` function. Let's do this backwards, and start from the higher-order function and work |
| 142 | +our way down. |
| 143 | + |
| 144 | +```clojure |
| 145 | +(defn part2 [input] (solve input set-bitmasked-addresses)) |
| 146 | +``` |
| 147 | + |
| 148 | +`set-bitmasked-addresses` needs to apply some masking function, `masked-addresses`, based on the |
| 149 | +incoming address and the current state's mask, to get the list of "real" addresses. Then for |
| 150 | +each one, we map the new address to the value `v` from the line. Then we update the current |
| 151 | +state's memory by applying the `into` function to merge the old map with the new. |
| 152 | + |
| 153 | +```clojure |
| 154 | +(defn set-bitmasked-addresses [state addr v] |
| 155 | + (->> (:mask state) |
| 156 | + (masked-addresses addr) |
| 157 | + (map #(vector % (Integer/parseInt v))) |
| 158 | + (update state :memory (partial into)))) |
| 159 | +``` |
| 160 | + |
| 161 | +On to the `masked-addresses` function. This actually works in two steps. First, we apply the mask |
| 162 | +to the incoming address, to come up with a new 36-character address that's made of `0`, `1`, and |
| 163 | +`x` characters. Once we have that, we will need to explode out all of the so-called "floating" |
| 164 | +addresses. `masked-addresses` is nothing special - pad the input, apply the mask, reassemble |
| 165 | +into a new String, and then delegate to the upcoming `floating-addresses` function. |
| 166 | + |
| 167 | +```clojure |
| 168 | +(defn masked-addresses [address mask] |
| 169 | + (->> (zero-pad address) |
| 170 | + (map (fn [m v] (if (= m \0) v m)) mask) |
| 171 | + (apply str) |
| 172 | + floating-addresses)) |
| 173 | +``` |
| 174 | + |
| 175 | +Last function coming up - `floating-addresses`. Given a String that might contain any number of `X` |
| 176 | +characters, return all possible Strings where each `X` is replaced with a `0` or a `1`. There are |
| 177 | +a bunch of ways to do this, but ultimately it's a choice between an iterative `loop-recur` approach |
| 178 | +or a recursive approach. I wrote both but like using recursion in Clojure. If the function finds a |
| 179 | +String with no `X` characters, then we return the single-element list containing that String. If |
| 180 | +there are any `X` characters, replace the first one with a `0` and then a `1`, recursively grabbing |
| 181 | +the `floating-addresses` with each. Then we combine the `0` list with the `1` list using `mapcat`, |
| 182 | +which again is Clojure's flat map function. |
| 183 | + |
| 184 | +```clojure |
| 185 | +(defn floating-addresses [s] |
| 186 | + (if-not (str/index-of s \X) |
| 187 | + (list s) |
| 188 | + (mapcat #(floating-addresses (str/replace-first s \X %)) |
| 189 | + [\0 \1]))) |
| 190 | +``` |
| 191 | + |
| 192 | +All in all, this felt like a very organized and structured little program. I dig it! |
0 commit comments