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