Sfoglia il codice sorgente

splitting up evaluation code

Brandon Wong 3 settimane fa
parent
commit
9c4e2598c0

+ 14 - 81
frontend/src/cljs/microtables_frontend/utils.cljs

@@ -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."

+ 12 - 11
frontend/src/cljs/microtables_frontend/events.cljs

@@ -1,7 +1,8 @@
 (ns microtables-frontend.events
   (:require
    [microtables-frontend.db :as db]
-   [microtables-frontend.utils :as utils]
+   [microtables-frontend.evaluation :as eval]
+   [microtables-frontend.utils.coordinates :as coords]
    [re-frame.core :as re-frame]))
 
 (re-frame/reg-event-db
@@ -9,15 +10,15 @@
  (fn [_ _]
    (println "initializing db")
    (-> db/default-db
-       (update-in [:table-data] #(utils/walk-modify-data
+       (update-in [:table-data] #(eval/walk-modify-data
                                   %
                                   (fn [_c _r datum]
                                     (if (= (first (:value datum)) "=")
                                       (assoc datum :dirty true)
                                       datum))))
-       (update-in [:table-data] utils/create-all-references)
-       (update-in [:table-data] utils/create-all-back-references)
-       (update-in [:table-data] utils/evaluate-all))))
+       (update-in [:table-data] eval/create-all-references)
+       (update-in [:table-data] eval/create-all-back-references)
+       (update-in [:table-data] eval/evaluate-all))))
 
 (re-frame/reg-event-db
  ::movement-enter-cell
@@ -32,26 +33,26 @@
    (-> db
        (assoc-in [:position :cursor] nil)
        (assoc-in [:position :selection] nil)
-       (update-in [:table-data] #(utils/reset-references % c r))
-       (update-in [:table-data] #(utils/evaluate-from-cell % c r)))))
+       (update-in [:table-data] #(eval/reset-references % c r))
+       (update-in [:table-data] #(eval/evaluate-from-cell % c r)))))
 
 (re-frame/reg-event-db
  ::edit-cell-value
  (fn [db [_ c r value]]
    (println "::edit-cell-value" c r value)
-   (update-in db [:table-data] #(utils/change-datum-value % c r value))))
+   (update-in db [:table-data] #(eval/change-datum-value % c r value))))
 
 ; handle pressing enter (move to the next cell down)
 ; tab is taken care of natively, and is good enough
 (re-frame/reg-event-fx
  ::press-enter-in-cell
  (fn [{:keys [db]} [_ c r]]
-   (let [max-row? (= (utils/highest-row (:table-data db)) r)
-         max-col? (= (utils/highest-col (:table-data db)) c)
+   (let [max-row? (= (coords/highest-row (:table-data db)) r)
+         max-col? (= (coords/highest-col (:table-data db)) c)
          new-col (if max-row?
                    (if max-col?
                      "A"
-                     (utils/next-letter c))
+                     (coords/next-letter c))
                    c)
          new-row (if max-row?
                    1

+ 2 - 2
frontend/src/cljs/microtables_frontend/subs.cljs

@@ -1,6 +1,6 @@
 (ns microtables-frontend.subs
   (:require
-   [microtables-frontend.utils :as utils]
+   [microtables-frontend.utils.coordinates :as coords]
    [re-frame.core :as re-frame]))
 
 (re-frame/reg-sub
@@ -16,7 +16,7 @@
         r1 (get-in selection [:start :row])
         c2 (get-in selection [:end :col])
         r2 (get-in selection [:end :row])
-        cells (utils/range->list c1 r1 c2 r2)]
+        cells (coords/range->list c1 r1 c2 r2)]
     (loop [new-data data
            remaining cells]
       (if (empty? remaining)

+ 69 - 0
frontend/src/cljs/microtables_frontend/utils/coordinates.cljs

@@ -0,0 +1,69 @@
+(ns microtables-frontend.utils.coordinates)
+
+(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)))

+ 4 - 4
frontend/src/cljs/microtables_frontend/views/sheet.cljs

@@ -1,7 +1,7 @@
 (ns microtables-frontend.views.sheet
   (:require
    [microtables-frontend.events :as events]
-   [microtables-frontend.utils :as utils]
+   [microtables-frontend.utils.coordinates :as coords]
    [re-frame.core :as re-frame]))
 
 (defn cell [c r data]
@@ -36,9 +36,9 @@
    [:tbody
     ;TODO: figure out appropriate starting values for maxrow and maxcol (maybe keep them intentionally small)
     ;TODO: figure out movement (maybe allow scroll overflow)
-    (let [maxrow 50;(utils/highest-row data)
-          maxcol "Z";(utils/highest-col data)
-          cols (take-while (partial not= (utils/next-letter maxcol)) utils/col-letters)]
+    (let [maxrow 50;(coords/highest-row data)
+          maxcol "Z";(coords/highest-col data)
+          cols (take-while (partial not= (coords/next-letter maxcol)) coords/col-letters)]
       (cons
        (header-row cols)
        (map #(row % cols data) (range 1 (inc maxrow)))))]])