Skip to content

Commit 5fbde64

Browse files
committed
Day 14 refactor and documentation
1 parent 8d68b88 commit 5fbde64

File tree

3 files changed

+234
-51
lines changed

3 files changed

+234
-51
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,4 @@ _Warning:_ I write long explanations. So... yeah.
2323
| 11 | [source](src/advent_2020_clojure/day11.clj) | [blog](docs/day11.md) |
2424
| 12 | [source](src/advent_2020_clojure/day12.clj) | [blog](docs/day12.md) |
2525
| 13 | [source](src/advent_2020_clojure/day13.clj) | [whiny rant](docs/day13.md) |
26+
| 14 | [source](src/advent_2020_clojure/day14.clj) | [blog](docs/day14.md) |

docs/day14.md

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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!

src/advent_2020_clojure/day14.clj

+41-51
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,58 @@
11
(ns advent-2020-clojure.day14
22
(:require [clojure.string :as str]))
33

4-
; State: {:mask "", :memory {n v}}
5-
4+
; State: {:mask "", :memory {"n" v}}
65
(def empty-mask "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
7-
(def empty-state
8-
{:mask empty-mask :memory {}})
6+
(def empty-state {:mask empty-mask :memory {}})
7+
8+
(defn zero-pad [s]
9+
(let [space-padded (->> s Long/parseLong Long/toBinaryString (format "%36s"))]
10+
(str/replace space-padded \space \0)))
911

10-
; NOTE: This should take in a String, not a number...
11-
(defn pad-n [n]
12-
(let [binary (Long/toBinaryString n)
13-
pad (apply str (take (- 36 (count binary))
14-
(repeat \0)))]
15-
(str pad binary)))
1612
(defn binary-to-long [b]
1713
(Long/parseLong b 2))
1814

19-
(defn mask-n [n mask]
20-
(->> (map (fn [m v] (if (= \X m) v m))
21-
mask
22-
(pad-n n))
15+
(defn mask-value [v mask]
16+
(->> (zero-pad v)
17+
(map (fn [m v] (if (= \X m) v m)) mask)
2318
(apply str)
2419
binary-to-long))
2520

26-
(defn process-line [line state]
27-
(condp re-matches line
28-
#"mask = (\w+)" :>> (fn [[_ mask]] (assoc state :mask mask))
29-
#"mem\[(\d+)\] = (\d+)" :>> (fn [[_ loc v]] (assoc-in state [:memory (Long/parseLong loc)]
30-
(mask-n (Long/parseLong v) (:mask state))))))
21+
(defn update-mask [state mask]
22+
(assoc state :mask mask))
3123

32-
(defn part1 [input]
33-
(loop [[line & next-lines] (str/split-lines input), state empty-state]
34-
(if-not line
35-
(->> (:memory state) vals (apply +))
36-
(recur next-lines (process-line line state)))))
24+
(defn set-bitmasked-value [state addr v]
25+
(assoc-in state [:memory addr] (mask-value v (:mask state))))
3726

38-
; TODO: Change to a char array
3927
(defn floating-addresses [s]
40-
(if-let [idx (str/index-of s \X)]
41-
(into (floating-addresses (apply str (assoc (vec s) idx 0)))
42-
(floating-addresses (apply str (assoc (vec s) idx 1))))
43-
(list s)))
28+
(if-not (str/index-of s \X)
29+
(list s)
30+
(mapcat #(floating-addresses (str/replace-first s \X %))
31+
[\0 \1])))
32+
33+
(defn masked-addresses [address mask]
34+
(->> (zero-pad address)
35+
(map (fn [m v] (if (= m \0) v m)) mask)
36+
(apply str)
37+
floating-addresses))
4438

45-
(defn convert-address-to-masked-addresses [address mask]
46-
(let [binary (pad-n (Long/parseLong address))]
47-
(->> (map (fn [m v] (if (= m \0) v m))
48-
mask
49-
binary)
50-
(apply str)
51-
floating-addresses)))
39+
(defn set-bitmasked-addresses [state addr v]
40+
(->> (:mask state)
41+
(masked-addresses addr)
42+
(map #(vector % (Integer/parseInt v)))
43+
(update state :memory (partial into))))
5244

53-
(defn process-line2 [line state]
45+
(defn process-line [line state mem-command]
5446
(condp re-matches line
55-
#"mask = (\w+)" :>> (fn [[_ mask]] (assoc state :mask mask))
56-
#"mem\[(\d+)\] = (\d+)" :>> (fn [[_ loc v]]
57-
(let [all-addresses (convert-address-to-masked-addresses loc (:mask state))]
58-
(->> all-addresses
59-
(map binary-to-long)
60-
(map #(vector % (Integer/parseInt v)))
61-
(into (:memory state))
62-
(assoc state :memory))))))
63-
64-
(defn part2 [input]
65-
(loop [[line & next-lines] (str/split-lines input), state empty-state]
66-
(if-not line
67-
(->> (:memory state) vals (apply +))
68-
(recur next-lines (process-line2 line state)))))
47+
#"mask = (\w+)" :>> (fn [[_ mask]] (update-mask state mask))
48+
#"mem\[(\d+)\] = (\d+)" :>> (fn [[_ addr v]] (mem-command state addr v))))
49+
50+
(defn solve [input mem-command]
51+
(->> (str/split-lines input)
52+
(reduce #(process-line %2 %1 mem-command) empty-state)
53+
:memory
54+
vals
55+
(apply +)))
56+
57+
(defn part1 [input] (solve input set-bitmasked-value))
58+
(defn part2 [input] (solve input set-bitmasked-addresses))

0 commit comments

Comments
 (0)