|
| 1 | +(ns day-19-1 |
| 2 | + (:require |
| 3 | + [clojure.java.io :as io] |
| 4 | + [clojure.string :as str] |
| 5 | + [clojure.test :as t :refer [deftest is]] |
| 6 | + [instaparse.core :as ic] |
| 7 | + [instaparse.transform :as it] |
| 8 | + [taoensso.tufte :as tufte :refer [defnp p]])) |
| 9 | + |
| 10 | +;; DRAMATIS PERSONAE |
| 11 | +;; xn .... an fsm transition |
| 12 | +;; xf .... a transform |
| 13 | +;; m ..... a part rating |
| 14 | + |
| 15 | +(defn make-guard-predicate |
| 16 | + [k op int] |
| 17 | + ;; do a code-as-data thing so we can examine in the pipeline; otherwise |
| 18 | + ;; anonymous fns are opaque and poor for debugging |
| 19 | + (let [sexp (list 'fn '[m] (list 'p ::guard |
| 20 | + (if (= op '<) |
| 21 | + ;; swap so always < |
| 22 | + (list '< (list k 'm) int) |
| 23 | + (list '< int (list k 'm))))) |
| 24 | + f (eval sexp)] |
| 25 | + ;; stash source in meta so it's examinable |
| 26 | + (with-meta f {::guard-source sexp}))) |
| 27 | + |
| 28 | +(def parse-transitions |
| 29 | + (let [grammar "workflow = state transitions |
| 30 | + transitions = <'{'> transition (<','> transition)* <'}'> |
| 31 | + <transition> = guarded-xn | unguarded-xn |
| 32 | + guarded-xn = (guard <':'> state) |
| 33 | + unguarded-xn = state |
| 34 | + guard = k op int |
| 35 | + state = #'[a-z]+' | 'A' | 'R' |
| 36 | + k = #'[a-z]+' |
| 37 | + op = '<' | '>' |
| 38 | + int = #'\\d+'" |
| 39 | + xfs {:int parse-long |
| 40 | + :op symbol |
| 41 | + :k keyword |
| 42 | + :state keyword |
| 43 | + :unguarded-xn (fn [k] {:to k}) |
| 44 | + :guard make-guard-predicate |
| 45 | + :guarded-xn (fn [guard k] {:guard guard :to k}) |
| 46 | + :transitions vector |
| 47 | + :workflow vector}] |
| 48 | + (comp (partial it/transform xfs) |
| 49 | + (ic/parser grammar)))) |
| 50 | + |
| 51 | +(def parse-rating |
| 52 | + (let [grammar "rating = <'{'> entry (<','> entry)* <'}'> |
| 53 | + entry = k <'='> n |
| 54 | + k = #'[a-z]+' |
| 55 | + n = #'\\d+'" |
| 56 | + xfs {:n parse-long |
| 57 | + :k keyword |
| 58 | + :entry vector |
| 59 | + :rating (fn [& kns] (into {} kns))}] |
| 60 | + (comp (partial it/transform xfs) |
| 61 | + (ic/parser grammar)))) |
| 62 | + |
| 63 | +(defnp read-input [path] |
| 64 | + (with-open [r (io/reader path)] |
| 65 | + (let [[raw-xns _ raw-ratings] (doall (partition-by str/blank? (line-seq r)))] |
| 66 | + {:xns (into {} (map parse-transitions) raw-xns) |
| 67 | + :ms (map parse-rating raw-ratings)}))) |
| 68 | + |
| 69 | +(defnp pick-transition |
| 70 | + "Find first transition in ordered transitions xns where the guard on m |
| 71 | + passes." |
| 72 | + [xns m] |
| 73 | + (reduce (fn [_ {guard :guard :or {guard (constantly true)} :as xn}] |
| 74 | + (let [use? (guard m)] |
| 75 | + #_(prn (-> guard meta ::guard-source) use? to) |
| 76 | + (when use? (reduced xn)))) |
| 77 | + nil |
| 78 | + xns)) |
| 79 | + |
| 80 | +(defnp step-fsm [fsm] |
| 81 | + (let [m (:m fsm) |
| 82 | + state (:state fsm) |
| 83 | + xns (get-in fsm [:xns state]) |
| 84 | + xn (pick-transition xns m) |
| 85 | + next-state (:to xn)] |
| 86 | + (assoc fsm :state next-state))) |
| 87 | + |
| 88 | +(defn run-fsm |
| 89 | + "Keep following transitions state until we hit an :A or an :R." |
| 90 | + [terminal? fsm] |
| 91 | + (let [fsm' (step-fsm fsm)] |
| 92 | + (if (terminal? (:state fsm')) fsm' (recur terminal? fsm')))) |
| 93 | + |
| 94 | +(defnp part-1 [{:keys [xns ms]}] |
| 95 | + (let [fsm {:state :in :xns xns} |
| 96 | + xf (comp |
| 97 | + (map (fn [m] (assoc fsm :m m))) |
| 98 | + (map (fn [fsm] (run-fsm #{:A :R} fsm))) |
| 99 | + (filter (fn [fsm] (= :A (:state fsm)))) |
| 100 | + (map :m) |
| 101 | + (mapcat vals))] |
| 102 | + (transduce xf + ms))) |
| 103 | + |
| 104 | +(deftest test-part-1 (is (= 19114 (part-1 (read-input "test.txt"))))) |
| 105 | + |
| 106 | +(comment |
| 107 | + (tufte/add-basic-println-handler! {}) |
| 108 | + (tufte/profile {} (time (part-1 (read-input "test.txt")))) |
| 109 | + (tufte/profile {} (time (part-1 (read-input "input.txt"))))) ; 330820 |
0 commit comments