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