Browse Source

restored basic cell evaluation with simple references

Brandon Wong 3 years ago
parent
commit
9085ec55ab

+ 3 - 1
frontend/src/cljs/microtables_frontend/events.cljs

@@ -40,7 +40,9 @@
           (assoc-in [:position :cursor] nil)
           (assoc-in [:position :selection] nil)
           (update-in [:table-data] (partial utils/add-parsed-variables-to-specific-datum c r))
-          (update-in [:alt-table-data c r] utils/add-references)
+          ;(update-in [:alt-table-data c r] utils/add-references)
+          (update-in [:alt-table-data] #(utils/reset-references % c r))
+          (update-in [:alt-table-data] #(utils/evaluate-from-cell % c r))
           (re-evaluate-if-dirty (:dirty datum))))))
 
 

+ 110 - 28
frontend/src/cljs/microtables_frontend/utils.cljs

@@ -66,8 +66,15 @@
   (if (= (first (:value datum)) "=")
     (let [vars (parse-variables (subs (:value datum) 1))
           refs (map str->rc vars)]
-      (-> datum (assoc :vars vars) (assoc :refs refs) (dissoc :error)))
-    (-> datum (dissoc :vars) (dissoc :refs) (dissoc :display) (dissoc :error))))
+      (-> datum
+          (assoc :vars vars)
+          (assoc :refs refs)
+          (dissoc :error)))
+    (-> datum
+        (dissoc :vars)
+        (dissoc :refs)
+        (dissoc :display)
+        (dissoc :error))))
 
 (defn add-parsed-variables-to-specific-datum
   "Parse variables from the value of a datum and add in :vars and :refs (for swap! data-atom).
@@ -142,28 +149,45 @@
             updated-one (notify-references data origin refs)]
         (recur updated-one (rest formulas))))))
 
-;TODO: change to recursive queue-based tree search
-(defn set-dirty-flag
-  "Determines if a datum needs to be marked as \"dirty\", based on value and inbound references. Returns datum with :dirty flag present or absent."
-  [datum]
-  (let [formula? (= (first (:value datum)) "=")
-        back-refs (:inbound datum)
-        back-refs? (not (empty? back-refs))]
-    (if (or formula? back-refs?)
-      (assoc datum :dirty true)
-      (dissoc datum :dirty))))
+(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)))))))
+
+;(walk-get-refs (set-dirty-flags (create-all-back-references (create-all-references (:alt-table-data microtables-frontend.db/default-db))) "C" 5) #(true? (:dirty %3)))
 
 (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)
-        parsed (add-references updated)]
+        updated (assoc datum :value value)]
     (-> data
         (assoc-in [c r :value] value)
-        (update-in [c r] set-dirty-flag)
-        (denotify-references {:col c :row r} (:refs datum))
-        (notify-references {:col c :row r} (:refs parsed)))))
+        (set-dirty-flags c r))))
+
+(defn reset-references
+  "If there has been a change to which cells are referenced by this cell, then change the necessary back-references to this cell."
+  [data c r]
+  (let [datum (get-in data [c r])
+        parsed (add-references datum)]
+    (if (= (:refs datum) (:refs parsed))
+      data
+      (-> data
+          (assoc-in [c r] parsed)
+          (denotify-references {:col c :row r} (:refs datum))
+          (notify-references {:col c :row r} (:refs parsed))))))
 
 
 (def evaluate-expression
@@ -200,34 +224,92 @@
                (find-cycle data (find-ref data cell) this-and-above)) refs)))))
 
 (defn alt-find-cycle
-  "Accepts the data and a datum, and peforms a depth-first search to find reference cycles."
+  "Accepts the data and a datum, and peforms a depth-first search to find reference cycles, following back-references."
   ([data c r] (alt-find-cycle data c r #{}))
   ([data c r ancest]
    (let [datum (get-in data [c r])
          current {:col c :row r}
          this-and-above (conj ancest current)
-         refs (:refs datum)
-         found-repeat (not (empty? (clojure.set/intersection this-and-above (set refs))))]
+         inbound (:inbound datum)
+         found-repeat (not (empty? (clojure.set/intersection this-and-above (set inbound))))]
      (if found-repeat
        :cycle-error
-       (some #(alt-find-cycle data (:col %) (:row %) this-and-above) refs)))))
-
+       (some #(alt-find-cycle data (:col %) (:row %) this-and-above) inbound)))))
+
+
+(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."
+  [data c r]
+  (let [datum (dissoc (dissoc (get-in data [c r]) :dirty) :display) ; get rid of the dirty flag right away (it must be included with the returned data to have effect)
+        refs (:refs datum)
+        value (:value datum)
+        formula? (= (first value) "=")
+        resolved-refs (map #(merge % (get-in data [(:col %) (:row %)])) refs)
+        evaluated-refs (map #(if (= (first (:value %)) "=") (:display %) (:value % "0")) resolved-refs)
+        invalid-refs (some nil? resolved-refs)
+        dirty-refs (some :dirty resolved-refs)
+        error-refs (some #(= (:display %) :error) resolved-refs)
+        unevaluated-refs (some nil? evaluated-refs)
+        cycle-refs (some #(= (:display %) :cycle-error) resolved-refs)
+        disqualified? (or invalid-refs dirty-refs error-refs)]
+    (cond
+      (false? formula?) (assoc-in data [c r] datum) ; if it's not a formula, then return as is (with the dirty flag removed)
+      cycle-refs (-> data                           ; if one of its references has a reference cycle, then this one is "poisoned" as well
+                     (assoc-in [c r] datum)
+                     (assoc-in [c r :display] :cycle-error))
+      unevaluated-refs (assoc-in data [c r :display] :insufficient-data) ; do not un-mark as "dirty", since it has not been evaluated yet
+      disqualified? (-> data                        ; some other error is present
+                        (assoc-in [c r] datum)
+                        (assoc-in [c r :display] :error))
+      (empty? refs) (-> data
+                        (assoc-in [c r] datum)
+                        (assoc-in [c r :display] (evaluate-expression (subs value 1) {})))
+      :else (let [variables (zipmap (map #(str (:col %) (:row %)) refs) evaluated-refs)
+                  evaluated-value (evaluate-expression (subs value 1) variables)
+                  new-datum (assoc datum :display evaluated-value)]
+              (assoc-in data [c r] new-datum)))))
+
+;(time (gather-variables-and-evaluate-cell {"A" {1 {:value "=A2 + 4" :refs '({:col "A" :row 2})} 2 {:value "2"}}} "A" 1))
+;(time (gather-variables-and-evaluate-cell (create-all-back-references (create-all-references (:alt-table-data microtables-frontend.db/default-db))) "B" 7))
+;(time (set-dirty-flags (create-all-back-references (create-all-references (:alt-table-data microtables-frontend.db/default-db))) "B" 7))
+;(zipmap (map #(str (:col %) (:row %)) (list {:col "A" :row 1} {:col "A" :row 2} {:col "A" :row 3} {:col "A" :row 4})) (list 1 3 5 7))
 
 
 ; THE NEW EVALUATE FUNCTION
-;TODO TODO TODO TODO TODO TODO TODO TODO TODO TODO
 ; - check for cycles in the back references, starting from the target cell (if any, use another function to mark it and its back references with :cycle-error and remove :dirty)
-;   - TODO: modify alt-find-cycle to use back-references instead of forward references
 ; - if any of the forward references are dirty, mark the cell (and recurse up) with an error (and set a TODO to think about this further)
 ; - evaluate (using forward references if necessary)
 ; - add all back-references to the queue
 ; - recurse
 ; - TODO: consider initialization case
 ; - TODO: consider multiple cells modified simultaneously
-; - TODO: comment the code well
-#_(defn evaluate-from-cell
-    "Evaluate the final value of a cell, and recursively re-evaluate all the cells that reference it."
-    [data c r])
+(defn evaluate-from-cell
+  "Evaluate the final value of a cell, and recursively re-evaluate all the cells that reference it."
+  [data c r]
+  (let [cycles? (alt-find-cycle data c r)
+        new-data (if cycles?
+                   (-> data                                           ; if there are cycles, mark :cycle-error and remove :dirty (rathan than evaluate) - still need to recurse up the tree to mark dependents with :cycle-error
+                       (update-in [c r] dissoc :dirty)
+                       (assoc-in [c r :display] :cycle-error))
+                   (gather-variables-and-evaluate-cell data c r))]    ; if there are no cycles, evaluate the cell
+    (loop [data new-data
+           queue (get-in new-data [c r :inbound])]
+      (if (empty? queue)
+        data                                                          ; if the queue is empty, we're done
+        (let [current (first queue)
+              cc (:col current)
+              cr (:row current)
+              dirty? (get-in data [cc cr :dirty])
+              re-evaluated-data (if dirty?
+                                  (gather-variables-and-evaluate-cell data cc cr)
+                                  data)
+              sufficient? (not= (get-in re-evaluated-data [cc cr :display]) :insufficient-data)
+              new-queue (if dirty?
+                          (if sufficient?
+                            (concat (rest queue) (get-in re-evaluated-data [cc cr :inbound]))   ; if all is well, then add the back-references onto the queue
+                            (concat (rest queue) (list current)))                               ; if the current cell's dependencies are not satisfied, re-add to the end of the queue
+                          (rest queue))]                                                        ; if the current cell is not marked as dirty, then it has already been processed
+          (recur re-evaluated-data new-queue))))))
 
 
 (defn alt-re-evaluate