|
@@ -1,85 +1,18 @@
|
|
|
-(ns microtables-frontend.utils
|
|
|
+(ns microtables-frontend.evaluation
|
|
|
(:require
|
|
|
["mathjs" :as mathjs]
|
|
|
[clojure.set :refer [intersection]]
|
|
|
- [clojure.string :as string]))
|
|
|
+ [clojure.string :as string]
|
|
|
+ [microtables-frontend.utils.coordinates :as coords]))
|
|
|
|
|
|
; 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-col
|
|
|
- "Return the highest column (letter) for which there is a non-empty cell"
|
|
|
- [data]
|
|
|
- ; choose the "max" (alphabetical order) value among the longest keys
|
|
|
- (->> data
|
|
|
- keys
|
|
|
- (group-by #(.-length %))
|
|
|
- (apply max-key key)
|
|
|
- val
|
|
|
- (apply max)))
|
|
|
-
|
|
|
-(defn highest-row
|
|
|
- "Return the highest row (number) for which there is a non-empty cell"
|
|
|
- [data]
|
|
|
- ; get all the row keys from all the column objects (and flatten), then pick the max
|
|
|
- (->> data
|
|
|
- vals
|
|
|
- (map keys)
|
|
|
- flatten
|
|
|
- (apply max)))
|
|
|
-
|
|
|
-(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]
|
|
|
- (->> lc
|
|
|
- (mapv #(.charCodeAt % 0))
|
|
|
- increment-letter-code
|
|
|
- (map char)
|
|
|
- (apply str)))
|
|
|
-
|
|
|
-(def col-letters
|
|
|
- (iterate next-letter "A"))
|
|
|
-
|
|
|
-(defn order-two-cols
|
|
|
- "Accepts two column names (letters) and returns them in order."
|
|
|
- [col1 col2]
|
|
|
- (cond
|
|
|
- (> (.-length col1) (.-length col2)) [col2 col1]
|
|
|
- (> (.-length col2) (.-length col1)) [col1 col2]
|
|
|
- (= (max col1 col2) col1) [col2 col1]
|
|
|
- :else [col1 col2]))
|
|
|
-
|
|
|
-(defn range->list
|
|
|
- "Converts two cells (accepting four coordinates) into a list of all the cells in the range between them (inclusively)."
|
|
|
- [col1 row1 col2 row2]
|
|
|
- (let [[start-col end-col] (order-two-cols col1 col2)
|
|
|
- start-row (min row1 row2)
|
|
|
- end-row (max row1 row2)]
|
|
|
- (for [col (take-while #(not= (next-letter end-col) %) (iterate next-letter start-col))
|
|
|
- row (range start-row (inc end-row))]
|
|
|
- {:col col :row row})))
|
|
|
-
|
|
|
-; the order goes top to bottom, then left to right - that makes the most sense to me
|
|
|
-; I don't know why a different order would be important, or even in what situation order is important at all
|
|
|
-(defn parse-range
|
|
|
- "Converts a range in \"A1:B2\" notation to a list of col/row cells: {:col \"A\" :row 1}, etc."
|
|
|
- [range-string]
|
|
|
- (let [col1 (second (re-find #"\(\s*([A-Z]+)" range-string))
|
|
|
- col2 (second (re-find #":\s*([A-Z]+)" range-string))
|
|
|
- row1 (.parseInt js/window (second (re-find #"([0-9]+)\s*:" range-string)))
|
|
|
- row2 (.parseInt js/window (second (re-find #"([0-9]+)\s*\)" range-string)))]
|
|
|
- (range->list col1 row1 col2 row2)))
|
|
|
-
|
|
|
(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 (parse-range range-string)
|
|
|
+ (let [cell-list (coords/parse-range range-string)
|
|
|
strings (map #(str (:col %) (:row %)) cell-list)]
|
|
|
(str "(" (string/join "," strings) ")")))))
|
|
|
|
|
@@ -88,7 +21,7 @@
|
|
|
(memoize (fn [expression]
|
|
|
(string/replace expression #"\(\s*[A-Z]+[0-9]+\s*:\s*[A-Z]+[0-9]+\s*\)" range->commalist))))
|
|
|
|
|
|
-(defn formula?
|
|
|
+(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."
|
|
@@ -110,7 +43,7 @@
|
|
|
{:row r :col c}))))
|
|
|
|
|
|
; leave in the :inbound references, since they probably have not have changed
|
|
|
-(defn add-references
|
|
|
+(defn- add-references
|
|
|
"Parses the expression in the value of a datum, and adds refs as necessary"
|
|
|
[datum]
|
|
|
(let [formula (formula? (:value datum))]
|
|
@@ -127,7 +60,7 @@
|
|
|
|
|
|
; the references in the data are a set of disconnected, doubly-linked trees
|
|
|
;TODO: rather than denotify all, then re-notify all, maybe use a diff? maybe on small scales it's not worth it?
|
|
|
-(defn denotify-references
|
|
|
+(defn- denotify-references
|
|
|
"Remove references in all cells formerly referenced by this cell"
|
|
|
[data origin refs]
|
|
|
(if (empty? refs)
|
|
@@ -136,7 +69,7 @@
|
|
|
de-notified (update-in data [(:col target) (:row target) :inbound] (partial filter #(not= % origin)))]
|
|
|
(recur de-notified origin (rest refs)))))
|
|
|
|
|
|
-(defn notify-references
|
|
|
+(defn- notify-references
|
|
|
"Update references in all cells referenced by this cell"
|
|
|
[data origin refs]
|
|
|
(if (empty? refs)
|
|
@@ -173,7 +106,7 @@
|
|
|
{}
|
|
|
data))
|
|
|
|
|
|
-(defn walk-get-refs
|
|
|
+(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]
|
|
@@ -201,7 +134,7 @@
|
|
|
updated-one (notify-references data origin refs)]
|
|
|
(recur updated-one (rest formulas))))))
|
|
|
|
|
|
-(defn set-dirty-flags
|
|
|
+(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]
|
|
@@ -240,14 +173,14 @@
|
|
|
(denotify-references {:col c :row r} (:refs datum))
|
|
|
(notify-references {:col c :row r} (:refs parsed))))))
|
|
|
|
|
|
-(defn remove-valueless-range-elements
|
|
|
+(defn- remove-valueless-range-elements
|
|
|
"Remove nil values specifically from ranges (to solve issues with some functions like average)."
|
|
|
[variables var-list]
|
|
|
(let [l (string/split (string/replace (first var-list) #"[()]" "") #",")
|
|
|
has-values (filter #(not (nil? (variables %))) l)]
|
|
|
(str "(" (string/join "," has-values) ")")))
|
|
|
|
|
|
-(defn preprocess-expression
|
|
|
+(defn- preprocess-expression
|
|
|
"Handle range cases, rename certain functions (to work with math.js), prepare expression and variables for processing."
|
|
|
[expression variables]
|
|
|
(let [renamed-expression (string/replace expression #"\baverage\(" "mean(")
|
|
@@ -276,7 +209,7 @@
|
|
|
|
|
|
;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
|
|
|
+(defn- find-cycle
|
|
|
"Accepts the data and a datum, and peforms a depth-first search to find reference cycles, following back-references."
|
|
|
([data c r] (find-cycle data c r #{}))
|
|
|
([data c r ancest]
|
|
@@ -289,7 +222,7 @@
|
|
|
:cycle-error
|
|
|
(some #(find-cycle data (:col %) (:row %) this-and-above) inbound)))))
|
|
|
|
|
|
-(defn gather-variables-and-evaluate-cell
|
|
|
+(defn- gather-variables-and-evaluate-cell
|
|
|
"Assumes that all the cell's immediate references have been resolved.
|
|
|
Collects the final values from them, then evaluates the current cell's expression.
|
|
|
Returns the new data map."
|