|
| 1 | +# Day Eighteen: Operation Order |
| 2 | + |
| 3 | +* [Problem statement](https://adventofcode.com/2020/day/18) |
| 4 | +* [Solution code](https://github.com/abyala/advent-2020-clojure/blob/master/src/advent_2020_clojure/day18.clj) |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +I'm going to declare right now that I had a ton of fun with this problem. As is becoming the case, it's |
| 9 | +a little hard to talk about Part 1 without also looking at Part 2, since I tend to refactor the code so |
| 10 | +much to be reusable between both parts. And since this is my blog post, I make the rules! |
| 11 | + |
| 12 | +The problem states that we are given several lines of text, each representing an arithmetic equation we need |
| 13 | +to solve. The operations are `+`, `*`, and parentheses, but the order of operations is all wacky. While |
| 14 | +parentheses always take priority, in part one we apply addition or multiplication based strictly on the |
| 15 | +left-to-right ordering, and in part two we prioritize addition over multiplication. My Dear Aunt Sally feels |
| 16 | +an excuse is quite in order! |
| 17 | + |
| 18 | +## Planning ahead |
| 19 | + |
| 20 | +Let's start with the end in mind. We know that parts 1 and 2 differ only in how we prioritize which operation |
| 21 | +to perform at a time, so let's figure that out. I know I'm going to handle the parentheses especially, so I |
| 22 | +think at some point I'm going to have an expression without parentheses, and I'll make a pass through the |
| 23 | +data to decide whether or not to apply any given operation. So that means I need to look at each mathematical |
| 24 | +line as a sequence of tokens, and I need to decide whether to take action on any single token. |
| 25 | + |
| 26 | +Let's parse the data. I want to take a String like `1 + (2 * (3 * 4) + 5)` and see it as a simple vector of |
| 27 | +values. The parentheses are always next to a number, but everything else is space delimited. For the numeric |
| 28 | +strings, let's turn them into `BigInteger`s right away, since we'll get overflow if we use `Long`s. The regex |
| 29 | +is a little ugly, but it's not bad. If I put in artifical spaces, you can see it's essentially one big `or`, |
| 30 | +and `re-seq` returns a sequence of tokens that match the regex. The `mapv` only attempts to parse values |
| 31 | +that aren't symbols; in theory, I could have done a try-catch parse for each token instead. |
| 32 | + |
| 33 | +I won't spend any time going over [Clojure-Java interop](https://clojure.org/reference/java_interop), but |
| 34 | +suffice it to say that `(BigInteger. "123")`, with that extra period, calls the constructor of `BigInteger` |
| 35 | +with the parameter `"123"`. |
| 36 | + |
| 37 | +```clojure |
| 38 | +(defn parse-mathematical-string [line] |
| 39 | + (->> (re-seq #"\d+|\(|\)|\+|\*" line) |
| 40 | + (mapv #(if (#{"+" "*" "(" ")"} %) % (BigInteger. %))))) |
| 41 | +``` |
| 42 | + |
| 43 | +Now let's zip to the very end. I'm going to have a `solve` function that takes in my data file and the |
| 44 | +ordered operations for non-parenthetical vectors. It's going to parse each line, run some calculation |
| 45 | +with those operations, and then add the results together. Since we're dealing with `BigIntegers`, we |
| 46 | +can't use `(apply + values)` and instead have to `reduce` the result using the Java method `.add`. |
| 47 | + |
| 48 | +```clojure |
| 49 | +(defn solve [input operations] |
| 50 | + (->> (str/split-lines input) |
| 51 | + (map parse-mathematical-string) |
| 52 | + (map #(calculate % operations)) |
| 53 | + (reduce #(.add %1 %2)))) |
| 54 | + |
| 55 | +; Helper functions for order-of-operations |
| 56 | +(def only-add (partial = "+")) |
| 57 | +(def only-mul (partial = "*")) |
| 58 | +(def add-or-mul (partial #{"+" "*"})) |
| 59 | + |
| 60 | +(defn part1 [input] (solve input [add-or-mul])) |
| 61 | +(defn part2 [input] (solve input [only-add only-mul])) |
| 62 | +``` |
| 63 | + |
| 64 | +Now that we have an idea of the overall structure, we can fill in the details. |
| 65 | + |
| 66 | +## Calculation functions |
| 67 | + |
| 68 | +Now that I'm getting more comfortable with Clojure, I'm trying to focus more on business-looking |
| 69 | +functions. The `calculate` function should take in the vector of tokens and the sequence of |
| 70 | +operations, and return the `BigInteger` result. When looking at a list of tokens, we should apply |
| 71 | +a simple priority. If there's only one value, return it. If there are parentheses, handle them. |
| 72 | +Otherwise, do "ordered-arithmetic" with the operation filters that were passed in. |
| 73 | + |
| 74 | +```clojure |
| 75 | +(defn calculate [tokens operations] |
| 76 | + (or (unpack-simple-expression tokens) |
| 77 | + (apply-parentheses tokens operations) |
| 78 | + (ordered-arithmatic tokens operations))) |
| 79 | +``` |
| 80 | + |
| 81 | +The `unpack-simple-expression` function is, well, simple. If there's only one token, return it. |
| 82 | +Remember that `when` will return `nil` if its predicate is false, and the `or` in `calculate` |
| 83 | +will return the first non-`nil` value. |
| 84 | + |
| 85 | +```clojure |
| 86 | +(defn unpack-simple-expression [tokens] |
| 87 | + (when (= 1 (count tokens)) (first tokens))) |
| 88 | +``` |
| 89 | + |
| 90 | +Next I want to get to `apply-parentheses`, but first we need to helper functions. We are going |
| 91 | +to need Java's `indexOf` and `lastIndexOf` functions, but they return `-1` if the value is not found, |
| 92 | +and magic constants are evil. So I'll start with an `index-of-or-nil` to make this easier to read |
| 93 | +and use in Clojure. Second, I need a function that takes in a vector of tokens, removes several |
| 94 | +of them, and replaces them with a new value, so `replace-subvec` will handle that for us. |
| 95 | + |
| 96 | +```clojure |
| 97 | +(defn index-of-or-nil [coll v] |
| 98 | + (let [idx (.indexOf coll v)] |
| 99 | + (when (>= idx 0) idx))) |
| 100 | + |
| 101 | +(defn replace-subvec [v low high new-value] |
| 102 | + (apply conj (subvec v 0 low) new-value (subvec v high))) |
| 103 | +``` |
| 104 | + |
| 105 | +Ok, let's handle `apply-parentheses`. The easiest way to handle the nesting is to find the |
| 106 | +_first_ closed paren, and the _last_ open paren before it, because that will represent the |
| 107 | +range of an expression without additional parentheses. `when-let` will only process if we |
| 108 | +find an index for `close-paren`, and if it's there, we can assume we'll find one for |
| 109 | +`open-paren`. Then we extract the subvector between these two indices and call `calculate` |
| 110 | +on it. Armed with that simplification, we call `replace-subvec` to create our new vector |
| 111 | +of tokens, and we call back to `calculate` with the new simplified vector. |
| 112 | + |
| 113 | +One thing to note is that I've set up a mutual dependency between the functions `calculate` |
| 114 | +and `apply-parentheses`. My initial one-function-to-rule-them-all solution didn't have this, |
| 115 | +but I'm making little functions. Clojure requires function definitions to appear in order, |
| 116 | +so we need to use `declare` to create a definition for `calculate` that `apply-parentheses` |
| 117 | +can hook into, before we define it later in the file. It's a little ugly, but at least it |
| 118 | +brings to light something that generally we don't want, so maybe that's a hidden benefit |
| 119 | +of this limitation! |
| 120 | + |
| 121 | +```clojure |
| 122 | +; Forward declaration, needed for mutual dependent functions. |
| 123 | +(declare calculate) |
| 124 | + |
| 125 | +(defn apply-parentheses [tokens operations] |
| 126 | + (when-let [close-paren (index-of-or-nil tokens ")")] |
| 127 | + (let [open-paren (.lastIndexOf (subvec tokens 0 close-paren) "(") |
| 128 | + new-value (-> (subvec tokens (inc open-paren) close-paren) |
| 129 | + (calculate operations))] |
| 130 | + (-> (replace-subvec tokens open-paren (inc close-paren) new-value) |
| 131 | + (calculate operations))))) |
| 132 | +``` |
| 133 | + |
| 134 | +At this point, we've handled parentheses and identity, so it's time to handle addition and |
| 135 | +subtraction. Let's make a little function called `simple-math` that takes in a vector and |
| 136 | +the index of the operation, and it returns a reduced vector having applied the operation |
| 137 | +to the values on either side. Nothing fancy here. |
| 138 | + |
| 139 | +```clojure |
| 140 | +(defn simple-math [tokens idx] |
| 141 | + (let [[op tok-a tok-b] (map #(tokens (+ idx %)) [0 -1 1]) |
| 142 | + new-val (case op |
| 143 | + "*" (.multiply tok-a tok-b) |
| 144 | + "+" (.add tok-a tok-b))] |
| 145 | + (replace-subvec tokens (dec idx) (+ idx 2) new-val))) |
| 146 | +``` |
| 147 | + |
| 148 | +Then the last piece to handle is the so-called "ordered arithmetic." Remember that the |
| 149 | +operations from part 1 will be `[add-or-mul]` while part 2 will be `[only-add only-mul]`, |
| 150 | +so we've got a vector of unary predicates. I'll implement this as a recursive function, |
| 151 | +since we need to look at all of the operations, and all of the tokens. Each time through, |
| 152 | +we'll short-circuit if this is a simple expression with only one value. If not, see if we |
| 153 | +can find the index of a token that passes the current predicate. If so, call `simple-math` |
| 154 | +and recursively call this function with the new value. Otherwise, drop the current operation |
| 155 | +and recursively call back in with the next one. Note that we assume the expressions are all |
| 156 | +valid, so we don't have to worry about running out of operations. |
| 157 | + |
| 158 | +```clojure |
| 159 | +(defn ordered-arithmetic [tokens [op & other-ops :as operations]] |
| 160 | + (or (unpack-simple-expression tokens) |
| 161 | + (when-let [idx (->> tokens |
| 162 | + (keep-indexed (fn [idx tok] (when (op tok) idx))) |
| 163 | + first)] |
| 164 | + (-> (simple-math tokens idx) |
| 165 | + (ordered-arithmetic operations))) |
| 166 | + (ordered-arithmetic tokens other-ops))) |
| 167 | +``` |
| 168 | + |
| 169 | +So yeah, I had a ton of fun with this problem! |
0 commit comments