Browse Source

detection and handling of circular references; cleaning up of code

Brandon Wong 4 years ago
parent
commit
4a4cf5daa3
1 changed files with 55 additions and 50 deletions
  1. 55 50
      frontend/src/microtables_frontend/core.cljs

+ 55 - 50
frontend/src/microtables_frontend/core.cljs

@@ -19,7 +19,8 @@
                   {:row 15 :col "E" :value "8968" :view :display}
                   {:row 7 :col "B" :value "=C5 + D6" :view :display}
                   {:row 8 :col "B" :value "=B7 * 2" :view :display}
-                  {:row 7 :col "C" :value "=D1" :view :display}])
+                  {:row 7 :col "C" :value "=D1" :view :display}
+                  {:row 12 :col "B" :value "=C12" :view :display}])
 
 (defonce data-atom (r/atom sample-data))
 
@@ -38,13 +39,15 @@
 
 
 ; CHANGE VALUE FUNCTIONS
-(defn update-value [c r datum value]
-  (if (nil? datum)
-    (swap! data-atom conj {:row r :col c :value value})
-    (swap! data-atom (fn [d] (map #(if (and (= r (:row %)) (= c (:col %))) (assoc % :value value) %) d)))))
-(defn toggle-display [c r view-mode]
+(defn update-value [c r existing-datum value]
+  (if (nil? existing-datum)
+    (swap! data-atom conj {:row r :col c :value value :dirty true})
+    (swap! data-atom (partial map #(if (and (= r (:row %)) (= c (:col %))) (assoc (assoc % :dirty true) :value value) %)))))
+
+;TODO: consider changing this for a single "view" atom which points to zero or one cells, which will determine whether to show the formula or evaluation
+(defn toggle-display [data c r view-mode]
   (println (str "  toggling " c r " to " view-mode))
-  (swap! data-atom (fn [d] (map #(if (and (= r (:row %)) (= c (:col %))) (assoc % :view view-mode) %) d))))
+  (map #(if (and (= r (:row %)) (= c (:col %))) (assoc % :view view-mode) %) data))
 
 ; CALCULATION / FORMULA EVALUATION FUNCTIONS
 
@@ -64,44 +67,31 @@
 (defn find-ref [data cell-ref]
   (some (fn [{:keys [row col] :as datum}] (if (and (= row (:row cell-ref)) (= col (:col cell-ref))) datum)) data))
 (defn copy-display-values [data display-values]
-  (let [removed (map #(-> % (dissoc :vars) (dissoc :refs) (dissoc :found) (dissoc :inputs)) display-values)]
-    (into data removed)))
-
-;TODO: TEST THIS
-;TODO: memoize dynamically?
-(defn find-cycle [data datum ances]
-  (let [cur {:row (:row datum) :col (:col datum)}
-        this-and-above (conj ances cur)
-        refs (:refs datum)
-        found (not (empty? (clojure.set/intersection this-and-above (set refs))))
-        ]
-  (println "searching for cycle" datum ances found)
-    (if found
-      :cycle-error
-      (some (fn [cell]
-              (find-cycle data (find-ref data cell) this-and-above)
-              ) refs)
-      )
-    )
-  )
-
-(defn find-formula-problems [data l]
-  ;(println "searching for potential problems" l (some #(= % {:row (:row l) :col (:col l)}) (:refs l)))
-  (println "searching for potential problems" (find-cycle data l #{}))
-  ;(loop []
-    
-    ;)
-  )
+  (let [original (map #(dissoc % :dirty) data)
+        removed (map #(-> % (dissoc :found) (dissoc :inputs) (dissoc :dirty)) display-values)]
+    (into original removed)))
+
+;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
+  ([data datum] (find-cycle data datum #{}))
+  ([data datum ances]
+   (let [cur {:row (:row datum) :col (:col datum)}
+         this-and-above (conj ances cur)
+         refs (:refs datum)
+         found (not (empty? (clojure.set/intersection this-and-above (set refs))))]
+     (if found
+       :cycle-error
+       (some (fn [cell]
+               (find-cycle data (find-ref data cell) this-and-above)) refs)))))
 
 (defn find-val [data c r]
   (let [l (find-cell data c r)
         v (get l :display (get l :value))
-        formula? (and (string? v) (= (first v) "="))
-        formula-value (if formula? (find-formula-problems data l))
-        ]
-    (println "found?" c r v l (get l :value) (get l :display))
+        formula? (and (string? v) (= (first v) "="))]
     (cond
       (nil? v) 0
+      ;(contains? l :error) :ref-error
       formula? :not-yet
       :else v)))
 
@@ -109,12 +99,10 @@
 
 ;TODO: figure out how to re-evaluate only when the cell modified affects other cells
 (defn re-evaluate [data]
-  (let [remove-old-displays (map #(dissoc % :display) data)
-        {has-formula true original-values false} (group-by #(= (first (:value %)) "=") remove-old-displays)
-        ;TODO: detect circular references
-        variables (map #(assoc % :vars (parse-variables (subs (:value %) 1))) has-formula)
-        first-mapped-cell-keys (map (fn [datum] (assoc datum :refs (map str->rc (:vars datum)))) variables)]
-    (loop [values original-values mapped-cell-keys first-mapped-cell-keys]
+  (let [{has-formula true original-values false} (group-by #(= (first (:value %)) "=") data)
+        found-cycles (map #(let [found (find-cycle data %)] (if found (assoc % :error found) %)) has-formula)
+        {eligible true ineligible false} (group-by #(not (contains? %  :error)) found-cycles)]
+    (loop [values (into original-values ineligible) mapped-cell-keys eligible]
       (let [search-values (map (fn [datum] (assoc datum :found (map #(find-val (concat values mapped-cell-keys) (:col %) (:row %)) (:refs datum)))) mapped-cell-keys)
             {not-ready true ready nil} (group-by (fn [datum] (some #(= :not-yet %) (:found datum))) search-values)
             prepped-for-eval (map (fn [datum] (assoc datum :inputs (apply hash-map (interleave (:vars datum) (:found datum))))) ready)
@@ -124,14 +112,29 @@
           updated-values
           (recur updated-values not-ready))))))
 
+(defn add-parsed-variables [datum]
+  (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))))
+
+(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).
+  If the value does not contain a fomula, remove any :vars and :refs that may have been there."
+  [c r data] (map #(if (and (= (:col %) c) (= (:row %) r))
+                     (add-parsed-variables %)
+                     %) data))
 
 (defn on-enter-cell [c r e]
   (println (str "entering cell " c r))
-  (toggle-display c r :value))
+  (swap! data-atom #(toggle-display % c r :value)))
 (defn on-leave-cell [c r e]
   (println (str "leaving cell " c r))
-  (toggle-display c r :display)
-  (swap! data-atom re-evaluate))
+  (swap! data-atom #(as-> % data
+                      (toggle-display data c r :display)
+                      (add-parsed-variables-to-specific-datum c r data)
+                      (re-evaluate data))))
 
 
 ;; -------------------------
@@ -139,7 +142,9 @@
 
 (defn cell [c r data]
   (let [datum (some #(if (and (= c (:col %)) (= r (:row %))) %) data)]
-    ^{:key (str c r)} [:td [:input {:value (if (= (get datum :view nil) :value) (get datum :value "") (get datum :display (get datum :value "")))
+    ^{:key (str c r)} [:td [:input {:value (if (= (get datum :view nil) :value)
+                                             (get datum :value "")
+                                             (get datum :error (get datum :display (get datum :value ""))))
                                     :on-change #(update-value c r datum (.. % -target -value))
                                     :on-blur (partial on-leave-cell c r)
                                     :on-focus (partial on-enter-cell c r)}]]))
@@ -170,7 +175,7 @@
 ;; -------------------------
 ;; Initialize app
 
-(swap! data-atom re-evaluate) ; evalutate any formulas the first time
+(swap! data-atom #(->> % (map add-parsed-variables) (re-evaluate))) ; evalutate any formulas the first time
 
 (defn mount-root []
   (r/render [app] (.getElementById js/document "app")))