| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121 |
- (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))))
|