Florentin Putz December 2022

Advent of Code 2022, using Clojure

The Advent of Code is an annual programming competition with fun problems for each day in December leading up to Christmas. Same as last year, I participated again together with some colleagues. Last year I used J, but this year I decided to brush up my Clojure.

Clojure

Clojure is a functional programming language (a Lisp dialect) on the Java platform. It鈥檚 syntax is immediately familiar to anyone who has programmed using S-expressions before.

Native speed using GraalVM

Since we also benchmark execution time in our internal leaderboard, I needed some way to avoid the performance overhead of spinning up the JVM and compiling to JVM bytecode on-the-fly. Luckily, we can compile Java applications ahead-of-time into native binaries using GraalVM Native Image. That way, my Clojure code was competitive with the Python, Nim, and C++ code of my colleagues.

Advent of Code

Check out the problem descriptions for each day on the AoC website. Each of my solutions will print two numbers, corresponding to the two parts of the problem. My solutions are also available on GitHub; you can directly execute them using babashka.

Day 1

(def input (slurp "in/01.txt"))

(defn calories
  "Given a string representing one Elf's inventory, calculate the total calories."
  [elf-str]
  (->> elf-str
       str/split-lines
       (map #(Integer/parseInt %))
       (reduce +)))

(def calories-per-elf
  "Calculate the total calories for each Elf."
  (map calories (str/split input #"\n\n")))

;; Part 1: Maximum calories carried by any single Elf.
(println (apply max calories-per-elf))

;; Part 2: Sum of the top three highest calorie counts.
(println (->> calories-per-elf
              (sort >)
              (take 3)
              (apply +)))

Day 2

(def input (slurp "in/02.txt"))

(def strategy-guide (->> input str/split-lines))

(defn points-a
  "Calculate points for part 1, where the strategy guide contains both shapes."
  [line]
  (case line
    "A X" 4  ;; Rock vs Rock: Draw (3) + Rock (1)
    "A Y" 8  ;; Rock vs Paper: Win (6) + Paper (2)
    "A Z" 3  ;; Rock vs Scissors: Loss (0) + Scissors (3)
    "B X" 1  ;; Paper vs Rock: Loss (0) + Rock (1)
    "B Y" 5  ;; Paper vs Paper: Draw (3) + Paper (2)
    "B Z" 9  ;; Paper vs Scissors: Win (6) + Scissors (3)
    "C X" 7  ;; Scissors vs Rock: Win (6) + Rock (1)
    "C Y" 2  ;; Scissors vs Paper: Loss (0) + Paper (2)
    "C Z" 6)) ;; Scissors vs Scissors: Draw (3) + Scissors (3)

(defn points-b
  "Calculate points for part 2, where the strategy guide contains the opponent's shape and the desired outcome."
  [line]
  (case line
    "A X" 3  ;; Rock, Loss: Loss (0) + Scissors (3)
    "A Y" 4  ;; Rock, Draw: Draw (3) + Rock (1)
    "A Z" 8  ;; Rock, Win (6) + Paper (2)
    "B X" 1  ;; Paper, Loss: Loss (0) + Rock (1)
    "B Y" 5  ;; Paper, Draw: Draw (3) + Paper (2)
    "B Z" 9  ;; Paper, Win: Win (6) + Scissors (3)
    "C X" 2  ;; Scissors, Loss: Loss (0) + Paper (2)
    "C Y" 6  ;; Scissors, Draw: Draw (3) + Scissors (3)
    "C Z" 7)) ;; Scissors, Win: Win (6) + Rock (1)

;; Part 1: Calculate total score based on the initial strategy guide.
(println (reduce + (map points-a strategy-guide)))

;; Part 2: Calculate total score based on the updated strategy guide.
(println (reduce + (map points-b strategy-guide)))

Day 3

(def input (str/split-lines (slurp "in/03.txt")))

(def priorities
  (mapv #(cond
           (<= (long \A) % (long \Z)) (- % (long \A) -27)  ;; Uppercase ascii->points: A-Z = 27-52
           (<= (long \a) % (long \z)) (- % (long \a) -1)   ;; Lowercase ascii->points: a-z = 1-26
           :else 0)
        (range 128)))

(defn letter->priority 
  "Convert a character to its priority value."
  [c]
  (priorities (long c)))

(defn duplicate-priority
  "Find the common item type in the compartments and calculate its priority."
  [lst]
  (->> lst
       (map set)
       (reduce set/intersection)
       first
       letter->priority))

;; Part 1: Calculate sum of priorities for items appearing in both compartments of each rucksack.
(println (transduce
          (comp (map #(split-at (/ (count %) 2) %))
                (map duplicate-priority))
          +
          input))

;; Part 2: Calculate sum of priorities for items common to all three-elf groups.
(println (transduce
          (comp (partition-all 3)
                (map duplicate-priority))
          +
          input))

Day 4

(def input (slurp "in/04.txt"))

(defn line->list
  "Convert a line of the form 'a-b,c-d' to a list of integers [a b c d]."
   [line]
  (let [i1 (str/index-of line \-)
        i2 (str/index-of line \,)
        i3 (str/index-of line \- i2)
        a (Integer/parseInt (subs line 0 i1)) ;; first set a-b
        b (Integer/parseInt (subs line (inc i1) i2))
        c (Integer/parseInt (subs line (inc i2) i3)) ;; second set c-d
        d (Integer/parseInt (subs line (inc i3)))]
    [a b c d]))

(def lists (->> input
                str/split-lines
                (map line->list)))

(defn part1
  "Check if one range fully contains the other."
  [[a b c d]]
  (or
   (and (<= a c) (>= b d)) ;; second subset of first
   (and (>= a c) (<= b d)))) ;; first subset of second

(defn part2
  "Check if two ranges overlap."
  [[a b c d]]
  (and (<= c b) (>= d a))) ;; overlap

;; Part 1: Count how many assignment pairs have one range fully containing the other.
(println (count (filter part1 lists)))

;; Part 2: Count how many assignment pairs overlap at all.
(println (count (filter part2 lists)))

Day 5

(def input (str/split (slurp "in/05.txt")  #"\n\n"))

(defn string->ints
  "Convert a string to a vector of integers."
  [s]
  (vec (map #(Integer/parseInt %) (re-seq #"\d+" s))))

(defn rearrange
  "Rearrange crates between stacks according to the specified move.
  If multiple-crates? is true, crates are moved in bulk; otherwise, they are moved one by one."
  [stacks [n from to] multiple-crates?]
  (let [[sel remaining-source-pile] (split-at n (stacks from))
        dest-pile (concat (if multiple-crates? sel (reverse sel)) (stacks to))]
    (-> stacks
        (assoc from remaining-source-pile)
        (assoc to dest-pile))))

(def stacks
  "Initial crate stacks from input."
  (->> input
       first
       (#(str/split % #"\n"))
       drop-last
       (apply mapv vector)
       (drop 1)
       (take-nth 4)
       (map #(remove #{\space} %))
       (cons ())  ;; prepend empty list to get 1-based indexing
       vec))

(def rearrangements
  "List of rearrangement instructions from input."
  (->> input
       last
       (#(str/split % #"\n"))
       (map string->ints)))

(defn part1
  "Rearrange stacks for part 1 (moving crates one by one)."
  [stacks [n from to]]
  (rearrange stacks [n from to] false))

(defn part2
  "Rearrange stacks for part 2 (moving crates in bulk)."
  [stacks [n from to]]
  (rearrange stacks [n from to] true))

;; Part 1: Output the crates on top of each stack after all rearrangements.
(println (->> rearrangements
              (reduce part1 stacks)
              (map first)
              (apply str)))

;; Part 2: Output the crates on top of each stack after all rearrangements (bulk).
(println (->> rearrangements
              (reduce part2 stacks)
              (map first)
              (apply str)))

Day 6

(def input (slurp "in/06.txt"))

(defn marker-pos
  "Find the first position where the last `len` characters are all different."
  [s len]
  (->> s
       (partition len 1)
       (take-while #(not (apply distinct? %)))
       count
       (+ len)))

;; Part 1: Output the position of the first start-of-packet marker (4 distinct characters).
(println (marker-pos input 4))

;; Part 2: Output the position of the first start-of-message marker (14 distinct characters).
(println (marker-pos input 14))

Day 10

(def input (slurp "in/10.txt"))

(defn evall
  "Evaluate the given line and update the value of x accordingly."
  [line x]
  (cond (< (count line) 5) [x] ;; For 'noop' instruction.
        :else [x (+ x (parse-long (subs line 5)))])) ;; For 'addx' instruction.

(def cycles
  "Generate the list of cycle values by evaluating each line of input."
  (->> input
       str/split-lines
       (reductions #(evall %2 (last %1)) [1])
       (apply concat)
       vec))

(defn signal-strength
  "Calculate the signal strength at the given cycle."
  [cycle]
  (* cycle (nth cycles (dec cycle))))

;; Part 1: Calculate the sum of signal strengths at the specified cycles.
(println (reduce + (map signal-strength [20 60 100 140 180 220])))

(defn render-pixel
  "Render the pixel at the given index based on the value of x."
  [index x]
  (if (<= -1 (- x (mod index 40)) 1) \# \.))

;; Part 2: Render the image given by the program.
(println
 (->> cycles
      drop-last
      (map-indexed render-pixel)
      (partition 40)
      (map (partial apply str))
      (str/join "\n")))

For part 2, this prints a rendering of the CRT display. There you can read the eight letters for the puzzle answer.

Day 11

(def input (slurp "in/11.txt"))

(defn numbers
  "Extract all numbers from a string and parse them as longs."
  [s]
  (map parse-long (re-seq #"\d+" s)))

(defn parse-monkey
  "Parse a block of text representing a monkey's attributes."
  [s]
  (let [lines (->> s str/split-lines vec)
        items (->> (nth lines 1) numbers)
        [_ a1 op a2] (->> (nth lines 2) (re-matches #".*= ([\d\w]+) ([\+\*]) ([\d\w]+)$"))
        func #((case op "*" * "+" +)
               (if (= a1 "old") % (parse-long a1))
               (if (= a2 "old") % (parse-long a2)))
        divisor (->> (nth lines 3) numbers first)
        divtrue (->> (nth lines 4) numbers first)
        divfalse (->> (nth lines 5) numbers first)
        next-fn #(if (zero? (mod % divisor)) divtrue divfalse)]
    {:items items, :f func, :next next-fn, :divisor divisor, :divtrue divtrue, :divfalse divfalse, :a1 a1, :op op, :a2 a2, :inspected 0}))

(def monkeys (->> (str/split input #"\n\n") (mapv parse-monkey)))

(defn next-round
  "Simulate the next round of item inspections and throws.
   ms_ - vector of monkeys with their current state.
   fworry - function to compute the new worry level."
  [ms_ fworry]
  (loop [ms ms_ ;; Initialize the state of monkeys.
         i 0]   ;; Start with the first monkey.
    (cond
       ;; If all monkeys have been processed, return the updated state.
      (>= i (count ms)) ms
      ;; If the current monkey has no items, move to the next monkey.
      (empty? (get-in ms [i :items])) (recur ms (inc i))
      ;; Otherwise, process the current monkey's first item.
      :else
      (let [m (nth ms i)            ;; Get the current monkey's state.
            it (first (:items m))   ;; Get the first item from the monkey's items.
            newit (fworry m it)     ;; Compute the new worry level using the fworry function.
            next ((:next m) newit)] ;; Determine the next monkey to receive the item. 
        (recur (-> ms
                   ;; Remove the processed item from the current monkey's items. 
                   (update-in [i :items] rest)
                   ;; Increment the inspection count for the current monkey.
                   (update-in [i :inspected] inc)
                   ;; Add the new item to the next monkey's items.
                   (update-in [next :items] concat (list newit)))
               i)))))  ;; Continue processing the current monkey (i) until all items are processed.

(defn monkey-business
  "Calculate the level of monkey business by multiplying the inspections of the two most active monkeys."
  [ms]
  (->> ms
       (map :inspected)
       (sort >)
       (take 2)
       (reduce *)))

(def modulo (->> monkeys (map :divisor) (reduce *)))

(def fworry1 (fn [m old] (int (/ ((:f m) old) 3))))
(def fworry2 (fn [m old] (mod ((:f m) old) modulo)))

 ;; Part 1: Calculate the level of monkey business after 20 rounds with initial worry function.
(println
 (->> monkeys
      (iterate #(next-round % fworry1))
      (#(nth % 20))
      monkey-business))

 ;; Part 2: Calculate the level of monkey business after 10000 rounds with modified worry function.
(println
 (->> monkeys
      (iterate #(next-round % fworry2))
      (#(nth % 10000))
      monkey-business))