(ns microtables-frontend.utils (:require ["mathjs" :as mathjs])) ; to add an npm package to shadow-cljs: ; https://clojureverse.org/t/guide-on-how-to-use-import-npm-modules-packages-in-clojurescript/2298 ; https://shadow-cljs.github.io/docs/UsersGuide.html#npm (defn highest [dir data] (apply max (map dir data))) (defn increment-letter-code [s] (let [l (last s)] (cond (empty? s) [65] (= l 90) (conj (increment-letter-code (subvec s 0 (dec (count s)))) 65) :else (conj (subvec s 0 (dec (count s))) (inc l))))) (defn next-letter [lc] (apply str (map char (increment-letter-code (mapv #(.charCodeAt % 0) lc))))) (def col-letters (iterate next-letter "A")) (defn get-datum [data c r] (some #(if (and (= c (:col %)) (= r (:row %))) %) data)) (def parse-variables (memoize (fn [expression] (as-> (js->clj (.parse mathjs expression)) $ (.filter $ #(true? (.-isSymbolNode %))) (map #(.-name %) $) (map #(.toUpperCase %) $) (filter #(re-matches #"[A-Z]+[0-9]+" %) $))))) (def str->rc (memoize (fn [s] (let [c (re-find #"^[A-Z]+" s) r (.parseInt js/window (re-find #"[0-9]+$" s))] {:row r :col c})))) (defn add-parsed-variables [datum] (if (= (first (:value datum)) "=") (let [vars (parse-variables (subs (:value datum) 1)) refs (map str->rc vars)] (-> datum (assoc :vars vars) (assoc :refs refs) (dissoc :error))) (-> datum (dissoc :vars) (dissoc :refs) (dissoc :display) (dissoc :error)))) (defn add-parsed-variables-to-specific-datum "Parse variables from the value of a datum and add in :vars and :refs (for swap! data-atom). If the value does not contain a fomula, remove any :vars and :refs that may have been there." [c r data] (map #(if (and (= (:col %) c) (= (:row %) r)) (add-parsed-variables %) %) data)) (def evaluate-expression (memoize (fn [expression variables] (try (.evaluate mathjs expression (clj->js variables)) (catch js/Error e (println "mathjs evaluation error" (.-message e) e) :calc-error))))) ;TODO: deal with lowercase cell references (defn find-cell [data c r] (some #(if (and (= (:col %) c) (= (:row %) r)) %) data)) (defn find-ref [data cell-ref] (some (fn [{:keys [row col] :as datum}] (if (and (= row (:row cell-ref)) (= col (:col cell-ref))) datum)) data)) (defn copy-display-values [data display-values] (let [original (map #(dissoc % :dirty) data) removed (map #(-> % (dissoc :found) (dissoc :inputs) (dissoc :dirty)) display-values)] (into original removed))) ;TODO: memoize dynamically? probably not worth memoizing directly, and could take up too much memory over time ; https://stackoverflow.com/a/13123571/8172807 (defn find-cycle ([data datum] (find-cycle data datum #{})) ([data datum ances] (let [cur {:row (:row datum) :col (:col datum)} this-and-above (conj ances cur) refs (:refs datum) found (not (empty? (clojure.set/intersection this-and-above (set refs))))] (if found :cycle-error (some (fn [cell] (find-cycle data (find-ref data cell) this-and-above)) refs))))) (defn find-val [data c r] (let [l (find-cell data c r) v (get l :display (get l :value)) formula? (and (string? v) (= (first v) "="))] (cond (nil? v) 0 ;(contains? l :error) :ref-error formula? :not-yet :else v))) ;TODO: ADD DOCSTRINGS TO ALL CONNECTED FUNCTIONS AND RENAME VARIABLES WHERE NEEDED ;TODO: figure out how to re-evaluate only when the cell modified affects other cells (defn re-evaluate [data] (println "re-evaluating" data) (let [{has-formula true original-values false} (group-by #(= (first (:value %)) "=") data) found-cycles (map #(let [found (find-cycle data %)] (if found (assoc % :error found) %)) has-formula) {eligible true ineligible false} (group-by #(not (contains? % :error)) found-cycles)] ; (loop [values (into original-values ineligible) mapped-cell-keys eligible] (let [search-values (map (fn [datum] (assoc datum :found (map #(find-val (concat values mapped-cell-keys) (:col %) (:row %)) (:refs datum)))) mapped-cell-keys) {not-ready true ready nil} (group-by (fn [datum] (some #(= :not-yet %) (:found datum))) search-values) prepped-for-eval (map (fn [datum] (let [hash-map-of-vars-to-vals (apply hash-map (interleave (:vars datum) (:found datum)))] (assoc datum :inputs hash-map-of-vars-to-vals))) ready) evaluated (map (fn [datum] (assoc datum :display (evaluate-expression (subs (:value datum) 1) (:inputs datum)))) prepped-for-eval) updated-values (copy-display-values values evaluated)] (println "EVALUATED" evaluated) (if (nil? not-ready) updated-values (recur updated-values not-ready))))))