utils.cljs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. (ns microtables-frontend.utils
  2. (:require
  3. ["mathjs" :as mathjs]))
  4. ; to add an npm package to shadow-cljs:
  5. ; https://clojureverse.org/t/guide-on-how-to-use-import-npm-modules-packages-in-clojurescript/2298
  6. ; https://shadow-cljs.github.io/docs/UsersGuide.html#npm
  7. (defn highest [dir data] (apply max (map dir data)))
  8. (defn highest-col
  9. "Return the highest column (letter) for which there is a non-empty cell"
  10. [data]
  11. ; choose the "max" (alphabetical order) value among the longest keys
  12. (apply max (val (apply max-key key (group-by #(.-length %) (keys data))))))
  13. (defn highest-row
  14. "Return the highest row (number) for which there is a non-empty cell"
  15. [data]
  16. ; get all the row keys from all the column objects (and flatten), then pick the max
  17. (apply max (flatten (map keys (vals data)))))
  18. (defn increment-letter-code [s]
  19. (let [l (last s)]
  20. (cond
  21. (empty? s) [65]
  22. (= l 90) (conj (increment-letter-code (subvec s 0 (dec (count s)))) 65)
  23. :else (conj (subvec s 0 (dec (count s))) (inc l)))))
  24. (defn next-letter [lc]
  25. (apply str (map char (increment-letter-code (mapv #(.charCodeAt % 0) lc)))))
  26. (def col-letters (iterate next-letter "A"))
  27. (defn get-datum [data c r]
  28. (some #(if (and (= c (:col %)) (= r (:row %))) %) data))
  29. (def parse-variables (memoize (fn [expression]
  30. (as-> (js->clj (.parse mathjs expression)) $
  31. (.filter $ #(true? (.-isSymbolNode %)))
  32. (map #(.-name %) $)
  33. (map #(.toUpperCase %) $)
  34. (filter #(re-matches #"[A-Z]+[0-9]+" %) $)))))
  35. (def str->rc (memoize (fn [s]
  36. (let [c (re-find #"^[A-Z]+" s)
  37. r (.parseInt js/window (re-find #"[0-9]+$" s))]
  38. {:row r :col c}))))
  39. (defn add-parsed-variables [datum]
  40. (if (= (first (:value datum)) "=")
  41. (let [vars (parse-variables (subs (:value datum) 1))
  42. refs (map str->rc vars)]
  43. (-> datum (assoc :vars vars) (assoc :refs refs) (dissoc :error)))
  44. (-> datum (dissoc :vars) (dissoc :refs) (dissoc :display) (dissoc :error))))
  45. ; leave in the :inbound references, since they probably have not have changed
  46. (defn add-references
  47. "Parses the expression in the value of a datum, and adds vars and refs as necessary"
  48. [datum]
  49. (if (= (first (:value datum)) "=")
  50. (let [vars (parse-variables (subs (:value datum) 1))
  51. refs (map str->rc vars)]
  52. (-> datum (assoc :vars vars) (assoc :refs refs) (dissoc :error)))
  53. (-> datum (dissoc :vars) (dissoc :refs) (dissoc :display) (dissoc :error))))
  54. (defn add-parsed-variables-to-specific-datum
  55. "Parse variables from the value of a datum and add in :vars and :refs (for swap! data-atom).
  56. If the value does not contain a fomula, remove any :vars and :refs that may have been there."
  57. [c r data] (map #(if (and (= (:col %) c) (= (:row %) r))
  58. (add-parsed-variables %)
  59. %) data))
  60. ; the references in the data are a set of disconnected, doubly-linked trees
  61. ;TODO: rather than denotify all, then re-notify all, maybe use a diff? maybe on small scales it's not worth it?
  62. (defn denotify-references
  63. "Remove references in all cells formerly referenced by this cell"
  64. [data origin refs]
  65. (if (empty? refs)
  66. data
  67. (let [target (first refs)
  68. de-notified (update-in data [(:col target) (:row target) :inbound] (partial filter #(not= % origin)))]
  69. (recur de-notified origin (rest refs)))))
  70. (defn notify-references
  71. "Update references in all cells referenced by this cell"
  72. [data origin refs]
  73. (if (empty? refs)
  74. data
  75. (let [target (first refs)
  76. notified (update-in data [(:col target) (:row target) :inbound] conj origin)]
  77. (recur notified origin (rest refs)))))
  78. (defn create-all-references
  79. "Starting from a clean slate, add in all references. This wipes any references that may have been present."
  80. [data]
  81. (reduce-kv
  82. (fn [columns c curr-column]
  83. (assoc columns c (reduce-kv
  84. (fn [rows r datum]
  85. (assoc rows r (add-references (dissoc (dissoc datum :refs) :inbound))))
  86. {}
  87. curr-column)))
  88. {}
  89. data))
  90. ;TODO: re-write create-all-references to use walk-modify-data instead
  91. (defn walk-modify-data
  92. "Walks through the data map and updates each datum by applying f (a function accepting col, row, datum)."
  93. [data f]
  94. (reduce-kv
  95. (fn [columns c curr-column]
  96. (assoc columns c (reduce-kv
  97. (fn [rows r datum]
  98. (assoc rows r (f c r datum)))
  99. {}
  100. curr-column)))
  101. {}
  102. data))
  103. ;(create-all-back-references (create-all-references {"A" {1 {:value "=B2"}} "B" {2 {:value "=B3"} 3 {:value "=A1"}}}))
  104. ;(= (walk-modify-data (:alt-table-data microtables-frontend.db/default-db) #(-> %3 (dissoc :refs) (dissoc :inbound) (add-references))) (walk-modify-data (:alt-table-data microtables-frontend.db/default-db) #(add-references (dissoc (dissoc %3 :refs) :inbound))) (create-all-references (:alt-table-data microtables-frontend.db/default-db)))
  105. (defn walk-get-refs
  106. "Walks through the data map and returns a list of :col/:row maps for each cell which satisfies the predicate (a function accepting col, row, datum)."
  107. [data pred]
  108. (reduce-kv (fn [l c column] (concat l (map (fn [[r _]] {:col c :row r}) (filter (fn [[r datum]] (pred c r datum)) column)))) '() data))
  109. (defn create-all-back-references
  110. "Assuming all references have been added, insert all back references."
  111. [data]
  112. (loop [data data
  113. formulas (walk-get-refs data #(= (first (:value %3)) "="))]
  114. (if (empty? formulas)
  115. data
  116. (let [origin (first formulas)
  117. refs (get-in data [(:col origin) (:row origin) :refs])
  118. updated-one (notify-references data origin refs)]
  119. (recur updated-one (rest formulas))))))
  120. ;TODO: change to recursive queue-based tree search
  121. (defn set-dirty-flag
  122. "Determines if a datum needs to be marked as \"dirty\", based on value and inbound references. Returns datum with :dirty flag present or absent."
  123. [datum]
  124. (let [formula? (= (first (:value datum)) "=")
  125. back-refs (:inbound datum)
  126. back-refs? (not (empty? back-refs))]
  127. (if (or formula? back-refs?)
  128. (assoc datum :dirty true)
  129. (dissoc datum :dirty))))
  130. (defn change-datum-value
  131. "Modify the value of a datum in the table, and update all applicable references"
  132. [data c r value]
  133. (let [datum (get-in data [c r])
  134. updated (assoc datum :value value)
  135. parsed (add-references updated)]
  136. (-> data
  137. (assoc-in [c r :value] value)
  138. (update-in [c r] set-dirty-flag)
  139. (denotify-references {:col c :row r} (:refs datum))
  140. (notify-references {:col c :row r} (:refs parsed)))))
  141. (def evaluate-expression
  142. "Convert (via mathjs) an expression string to a final answer (also a string). A map of variables must also be provided. If there is an error, it will return :calc-error."
  143. (memoize (fn [expression variables]
  144. (try
  145. (.evaluate mathjs expression (clj->js variables))
  146. (catch js/Error e
  147. (println "mathjs evaluation error" (.-message e) e)
  148. :calc-error)))))
  149. ;TODO: deal with lowercase cell references
  150. (defn find-cell [data c r]
  151. (some #(if (and (= (:col %) c) (= (:row %) r)) %) data))
  152. (defn find-ref [data cell-ref]
  153. (some (fn [{:keys [row col] :as datum}] (if (and (= row (:row cell-ref)) (= col (:col cell-ref))) datum)) data))
  154. (defn copy-display-values [data display-values]
  155. (let [original (map #(dissoc % :dirty) data)
  156. removed (map #(-> % (dissoc :found) (dissoc :inputs) (dissoc :dirty)) display-values)]
  157. (into original removed)))
  158. ;TODO: memoize dynamically? probably not worth memoizing directly, and could take up too much memory over time
  159. ; https://stackoverflow.com/a/13123571/8172807
  160. (defn find-cycle
  161. ([data datum] (find-cycle data datum #{}))
  162. ([data datum ances]
  163. (let [cur {:row (:row datum) :col (:col datum)}
  164. this-and-above (conj ances cur)
  165. refs (:refs datum)
  166. found (not (empty? (clojure.set/intersection this-and-above (set refs))))]
  167. (if found
  168. :cycle-error
  169. (some (fn [cell]
  170. (find-cycle data (find-ref data cell) this-and-above)) refs)))))
  171. (defn alt-find-cycle
  172. "Accepts the data and a datum, and peforms a depth-first search to find reference cycles."
  173. ([data c r] (alt-find-cycle data c r #{}))
  174. ([data c r ancest]
  175. (let [datum (get-in data [c r])
  176. current {:col c :row r}
  177. this-and-above (conj ancest current)
  178. refs (:refs datum)
  179. found-repeat (not (empty? (clojure.set/intersection this-and-above (set refs))))]
  180. (if found-repeat
  181. :cycle-error
  182. (some #(alt-find-cycle data (:col %) (:row %) this-and-above) refs)))))
  183. ; THE NEW EVALUATE FUNCTION
  184. ;TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO
  185. ; - check for cycles in the back references, starting from the target cell (if any, use another function to mark it and its back references with :cycle-error and remove :dirty)
  186. ; - TODO: modify alt-find-cycle to use back-references instead of forward references
  187. ; - if any of the forward references are dirty, mark the cell (and recurse up) with an error (and set a TODO to think about this further)
  188. ; - evaluate (using forward references if necessary)
  189. ; - add all back-references to the queue
  190. ; - recurse
  191. ; - TODO: consider initialization case
  192. ; - TODO: consider multiple cells modified simultaneously
  193. ; - TODO: comment the code well
  194. #_(defn evaluate-from-cell
  195. "Evaluate the final value of a cell, and recursively re-evaluate all the cells that reference it."
  196. [data c r])
  197. (defn alt-re-evaluate
  198. "Evaluate the values of cells that contain formulae, following reference chains if applicable."
  199. [data]
  200. (let [non-empty-cells (flatten (map (fn [[c v]] (map (fn [[r _]] {:col c :row r}) v)) data))
  201. {has-formula true original-values false} (group-by #(= (first (get-in data [(:col %) (:row %) :value])) "=") non-empty-cells)
  202. found-cycles (map #(let [found (alt-find-cycle data (:col %) (:row %))]
  203. (if found (assoc % :error found) %)) has-formula)]
  204. non-empty-cells))
  205. (defn find-val [data c r]
  206. (let [l (find-cell data c r)
  207. v (get l :display (get l :value))
  208. formula? (and (string? v) (= (first v) "="))]
  209. (cond
  210. (nil? v) 0
  211. ;(contains? l :error) :ref-error
  212. formula? :not-yet
  213. :else v)))
  214. ;TODO: ADD DOCSTRINGS TO ALL CONNECTED FUNCTIONS AND RENAME VARIABLES WHERE NEEDED
  215. ;TODO: figure out how to re-evaluate only when the cell modified affects other cells
  216. (defn re-evaluate [data]
  217. (println "re-evaluating" data)
  218. (let [{has-formula true original-values false} (group-by #(= (first (:value %)) "=") data)
  219. found-cycles (map #(let [found (find-cycle data %)] (if found (assoc % :error found) %)) has-formula)
  220. {eligible true ineligible false} (group-by #(not (contains? % :error)) found-cycles)]
  221. ;
  222. (loop [values (into original-values ineligible)
  223. mapped-cell-keys eligible]
  224. (let [search-values (map (fn [datum]
  225. (assoc datum :found (map #(find-val
  226. (concat values mapped-cell-keys)
  227. (:col %)
  228. (:row %))
  229. (:refs datum))))
  230. mapped-cell-keys)
  231. {not-ready true ready nil} (group-by (fn [datum]
  232. (some #(= :not-yet %) (:found datum)))
  233. search-values)
  234. prepped-for-eval (map (fn [datum]
  235. (let [hash-map-of-vars-to-vals (apply hash-map (interleave (:vars datum) (:found datum)))]
  236. (assoc datum :inputs hash-map-of-vars-to-vals)))
  237. ready)
  238. evaluated (map (fn [datum]
  239. (assoc datum :display (evaluate-expression
  240. (subs (:value datum) 1)
  241. (:inputs datum))))
  242. prepped-for-eval)
  243. updated-values (copy-display-values values evaluated)]
  244. (println "EVALUATED" evaluated)
  245. (if (nil? not-ready)
  246. updated-values
  247. (recur updated-values not-ready))))))