(ns microtables-frontend.utils.data (:require ["mathjs" :as mathjs] [clojure.string :as string] [microtables-frontend.utils.coordinates :as coords])) (defn formula? "Determines if a value is a fomula. If it is, it returns it (without the leading equals sign). If not, it returns nil." [value] (if (= (first value) "=") (subs value 1) nil)) (def range->commalist "Converts a range in \"A1:B2\" notation to a comma-separated list of cells: \"A1,A2,B1,B2\"." (memoize (fn [range-string] (let [cell-list (coords/parse-range range-string) strings (map #(str (:col %) (:row %)) cell-list)] (str "(" (string/join "," strings) ")"))))) (def replace-ranges-in-expression "Receives an expression string, and replaces all ranges in colon notation (\"A1:B2\") into a comma-separated list of cells (\"A1,A2,B1,B2\")." (memoize (fn [expression] (string/replace expression #"\(\s*[A-Z]+[0-9]+\s*:\s*[A-Z]+[0-9]+\s*\)" range->commalist)))) (def parse-variables (memoize (fn [expression] (as-> (js->clj (.parse mathjs (replace-ranges-in-expression expression))) $ (.filter $ #(true? (.-isSymbolNode ^js %))) (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 (js/parseInt (re-find #"[0-9]+$" s))] {:row r :col c})))) ; leave in the :inbound references, since they probably have not have changed (defn add-references "Parses the expression in the value of a datum, and adds refs as necessary" [datum] (let [formula (formula? (:value datum))] (if formula (let [vars (parse-variables formula) refs (map str->rc vars)] (-> datum (assoc :refs refs) (dissoc :error))) (-> datum (dissoc :refs) (dissoc :display) (dissoc :error))))) (defn create-all-references "Starting from a clean slate, add in all references. This wipes any references that may have been present." [data] (reduce-kv (fn [columns c curr-column] (assoc columns c (reduce-kv (fn [rows r datum] (assoc rows r (add-references (dissoc (dissoc datum :refs) :inbound)))) {} curr-column))) {} data)) ;TODO: re-write create-all-references to use walk-modify-data instead (defn walk-modify-data "Walks through the data map and updates each datum by applying f (a function accepting col, row, datum)." [data f] (reduce-kv (fn [columns c curr-column] (assoc columns c (reduce-kv (fn [rows r datum] (assoc rows r (f c r datum))) {} curr-column))) {} data)) (defn walk-get-refs "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)." [data pred] (reduce-kv (fn [l c column] (->> column (filter (fn [[r datum]] (pred c r datum))) (map (fn [[r _]] {:col c :row r})) (concat l))) '() data)) (defn- set-dirty-flags "Sets the target cell to \"dirty\" and recursively repeat with its back-references all the way up. Returns the new data set." ([data c r] (set-dirty-flags data (list {:col c :row r}))) ([data queue] (if (empty? queue) data (let [cur (first queue) c (:col cur) r (:row cur) datum (get-in data [c r])] (if (true? (:dirty datum)) (recur data (rest queue)) (let [new-data (assoc-in data [c r :dirty] true) new-queue (concat (rest queue) (:inbound datum))] (recur new-data new-queue))))))) (defn change-datum-value "Modify the value of a datum in the table, and update all applicable references" [data c r value] (let [datum (get-in data [c r]) updated (assoc datum :value value)] (-> data (assoc-in [c r :value] value) (set-dirty-flags c r))))