Browse Source

splitting up evaluation code

Brandon Wong 3 weeks ago
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
   (:require
    ["mathjs" :as mathjs]
    ["mathjs" :as mathjs]
    [clojure.set :refer [intersection]]
    [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:
 ; 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://clojureverse.org/t/guide-on-how-to-use-import-npm-modules-packages-in-clojurescript/2298
 ; https://shadow-cljs.github.io/docs/UsersGuide.html#npm
 ; 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
 (def range->commalist
   "Converts a range in \"A1:B2\" notation to a comma-separated list of cells: \"A1,A2,B1,B2\"."
   "Converts a range in \"A1:B2\" notation to a comma-separated list of cells: \"A1,A2,B1,B2\"."
   (memoize (fn [range-string]
   (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)]
                    strings (map #(str (:col %) (:row %)) cell-list)]
                (str "(" (string/join "," strings) ")")))))
                (str "(" (string/join "," strings) ")")))))
 
 
@@ -88,7 +21,7 @@
   (memoize (fn [expression]
   (memoize (fn [expression]
              (string/replace expression #"\(\s*[A-Z]+[0-9]+\s*:\s*[A-Z]+[0-9]+\s*\)" range->commalist))))
              (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.
   "Determines if a value is a fomula.
   If it is, it returns it (without the leading equals sign).
   If it is, it returns it (without the leading equals sign).
   If not, it returns nil."
   If not, it returns nil."
@@ -110,7 +43,7 @@
                           {:row r :col c}))))
                           {:row r :col c}))))
 
 
 ; leave in the :inbound references, since they probably have not have changed
 ; 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"
   "Parses the expression in the value of a datum, and adds refs as necessary"
   [datum]
   [datum]
   (let [formula (formula? (:value datum))]
   (let [formula (formula? (:value datum))]
@@ -127,7 +60,7 @@
 
 
 ; the references in the data are a set of disconnected, doubly-linked trees
 ; 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?
 ;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"
   "Remove references in all cells formerly referenced by this cell"
   [data origin refs]
   [data origin refs]
   (if (empty? refs)
   (if (empty? refs)
@@ -136,7 +69,7 @@
           de-notified (update-in data [(:col target) (:row target) :inbound] (partial filter #(not= % origin)))]
           de-notified (update-in data [(:col target) (:row target) :inbound] (partial filter #(not= % origin)))]
       (recur de-notified origin (rest refs)))))
       (recur de-notified origin (rest refs)))))
 
 
-(defn notify-references
+(defn- notify-references
   "Update references in all cells referenced by this cell"
   "Update references in all cells referenced by this cell"
   [data origin refs]
   [data origin refs]
   (if (empty? refs)
   (if (empty? refs)
@@ -173,7 +106,7 @@
    {}
    {}
    data))
    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)."
   "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]
   [data pred]
   (reduce-kv (fn [l c column]
   (reduce-kv (fn [l c column]
@@ -201,7 +134,7 @@
             updated-one (notify-references data origin refs)]
             updated-one (notify-references data origin refs)]
         (recur updated-one (rest formulas))))))
         (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.
   "Sets the target cell to \"dirty\" and recursively repeat with its back-references all the way up.
   Returns the new data set."
   Returns the new data set."
   ([data c r]
   ([data c r]
@@ -240,14 +173,14 @@
           (denotify-references {:col c :row r} (:refs datum))
           (denotify-references {:col c :row r} (:refs datum))
           (notify-references {:col c :row r} (:refs parsed))))))
           (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)."
   "Remove nil values specifically from ranges (to solve issues with some functions like average)."
   [variables var-list]
   [variables var-list]
   (let [l (string/split (string/replace (first var-list) #"[()]" "") #",")
   (let [l (string/split (string/replace (first var-list) #"[()]" "") #",")
         has-values (filter #(not (nil? (variables %))) l)]
         has-values (filter #(not (nil? (variables %))) l)]
     (str "(" (string/join "," has-values) ")")))
     (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."
   "Handle range cases, rename certain functions (to work with math.js), prepare expression and variables for processing."
   [expression variables]
   [expression variables]
   (let [renamed-expression (string/replace expression #"\baverage\(" "mean(")
   (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
 ;TODO: memoize dynamically? probably not worth memoizing directly, and could take up too much memory over time
 ;      https://stackoverflow.com/a/13123571/8172807
 ;      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."
   "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] (find-cycle data c r #{}))
   ([data c r ancest]
   ([data c r ancest]
@@ -289,7 +222,7 @@
        :cycle-error
        :cycle-error
        (some #(find-cycle data (:col %) (:row %) this-and-above) inbound)))))
        (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.
   "Assumes that all the cell's immediate references have been resolved.
   Collects the final values from them, then evaluates the current cell's expression.
   Collects the final values from them, then evaluates the current cell's expression.
   Returns the new data map."
   Returns the new data map."

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

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

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

@@ -1,6 +1,6 @@
 (ns microtables-frontend.subs
 (ns microtables-frontend.subs
   (:require
   (:require
-   [microtables-frontend.utils :as utils]
+   [microtables-frontend.utils.coordinates :as coords]
    [re-frame.core :as re-frame]))
    [re-frame.core :as re-frame]))
 
 
 (re-frame/reg-sub
 (re-frame/reg-sub
@@ -16,7 +16,7 @@
         r1 (get-in selection [:start :row])
         r1 (get-in selection [:start :row])
         c2 (get-in selection [:end :col])
         c2 (get-in selection [:end :col])
         r2 (get-in selection [:end :row])
         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
     (loop [new-data data
            remaining cells]
            remaining cells]
       (if (empty? remaining)
       (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
 (ns microtables-frontend.views.sheet
   (:require
   (:require
    [microtables-frontend.events :as events]
    [microtables-frontend.events :as events]
-   [microtables-frontend.utils :as utils]
+   [microtables-frontend.utils.coordinates :as coords]
    [re-frame.core :as re-frame]))
    [re-frame.core :as re-frame]))
 
 
 (defn cell [c r data]
 (defn cell [c r data]
@@ -36,9 +36,9 @@
    [:tbody
    [:tbody
     ;TODO: figure out appropriate starting values for maxrow and maxcol (maybe keep them intentionally small)
     ;TODO: figure out appropriate starting values for maxrow and maxcol (maybe keep them intentionally small)
     ;TODO: figure out movement (maybe allow scroll overflow)
     ;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
       (cons
        (header-row cols)
        (header-row cols)
        (map #(row % cols data) (range 1 (inc maxrow)))))]])
        (map #(row % cols data) (range 1 (inc maxrow)))))]])