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