|  | @@ -1,225 +0,0 @@
 | 
	
		
			
				|  |  | -(ns microtables-frontend.core
 | 
	
		
			
				|  |  | -    (:require
 | 
	
		
			
				|  |  | -      [reagent.core :as r]
 | 
	
		
			
				|  |  | -      ["mathjs" :as mathjs]))
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -; to generate random values
 | 
	
		
			
				|  |  | -;for(let i = 0, s = new Set(); i < 10; i++){ let r = Math.floor(Math.random() * 15)+1, c = a[Math.floor(Math.random() * a.length)], k = `${c}${r}`; if(s.has(k)){ i--; continue; } s.add(k); v.push(`{:row ${r} :col "${c}" :value "${Math.floor(Math.random() * 10000)}"}`); }
 | 
	
		
			
				|  |  | -(def sample-data [{:row 1 :col "A" :value "59" :view :display}
 | 
	
		
			
				|  |  | -                  {:row 5 :col "C" :value "269" :view :display}
 | 
	
		
			
				|  |  | -                  {:row 4 :col "B" :value "7893" :view :display}
 | 
	
		
			
				|  |  | -                  {:row 2 :col "F" :value "8650" :view :display}
 | 
	
		
			
				|  |  | -                  {:row 6 :col "D" :value "4065" :view :display}
 | 
	
		
			
				|  |  | -                  {:row 7 :col "F" :value "5316" :view :display}
 | 
	
		
			
				|  |  | -                  {:row 12 :col "A" :value "2405" :view :display}
 | 
	
		
			
				|  |  | -                  {:row 5 :col "B" :value "7863" :view :display}
 | 
	
		
			
				|  |  | -                  {:row 9 :col "E" :value "3144" :view :display}
 | 
	
		
			
				|  |  | -                  {:row 10 :col "D" :value "8272" :view :display}
 | 
	
		
			
				|  |  | -                  {:row 11 :col "D" :value "2495" :view :display}
 | 
	
		
			
				|  |  | -                  {: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 12 :col "B" :value "=C12" :view :display}])
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -(defonce data-atom (r/atom sample-data))
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -(defn highest [dir data] (apply max (map dir data)))
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -; COLUMN NAMES
 | 
	
		
			
				|  |  | -(defn increment-letter-code [s]
 | 
	
		
			
				|  |  | -  (let [l (last s)]
 | 
	
		
			
				|  |  | -    (cond
 | 
	
		
			
				|  |  | -      (empty? s) [65]
 | 
	
		
			
				|  |  | -      (= l 90) (conj (increment-letter-code (subvec s 0 (dec (count s)))) 65)
 | 
	
		
			
				|  |  | -      :else (conj (subvec s 0 (dec (count s))) (inc l)))))
 | 
	
		
			
				|  |  | -(defn next-letter [lc]
 | 
	
		
			
				|  |  | -  (apply str (map char (increment-letter-code (mapv #(.charCodeAt % 0) lc)))))
 | 
	
		
			
				|  |  | -(def col-letters (iterate next-letter "A"))
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -; CHANGE VALUE FUNCTIONS
 | 
	
		
			
				|  |  | -(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))
 | 
	
		
			
				|  |  | -  (map #(if (and (= r (:row %)) (= c (:col %))) (assoc % :view view-mode) %) data))
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -; CALCULATION / FORMULA EVALUATION FUNCTIONS
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -(def parse-variables (memoize (fn [expression]
 | 
	
		
			
				|  |  | -                                (js->clj (.getVariables mathjs expression)))))
 | 
	
		
			
				|  |  | -(def evaluate-expression (memoize (fn [expression variables]
 | 
	
		
			
				|  |  | -                                    (let [answer (.evaluate mathjs expression (clj->js variables))]
 | 
	
		
			
				|  |  | -                                      (if (nil? (.-error answer))
 | 
	
		
			
				|  |  | -                                        answer
 | 
	
		
			
				|  |  | -                                        :calc-error)))))
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -(def str->rc (memoize (fn [s]
 | 
	
		
			
				|  |  | -                        (let [c (re-find #"^[A-Z]+" s)
 | 
	
		
			
				|  |  | -                              r (.parseInt js/window (re-find #"[0-9]+$" s))]
 | 
	
		
			
				|  |  | -                          {:row r :col c}))))
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -;TODO: deal with lowercase cell references
 | 
	
		
			
				|  |  | -(defn find-cell [data c r]
 | 
	
		
			
				|  |  | -  (some #(if (and (= (:col %) c) (= (:row %) r)) %) data))
 | 
	
		
			
				|  |  | -(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 [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) "="))]
 | 
	
		
			
				|  |  | -    (cond
 | 
	
		
			
				|  |  | -      (nil? v) 0
 | 
	
		
			
				|  |  | -      ;(contains? l :error) :ref-error
 | 
	
		
			
				|  |  | -      formula? :not-yet
 | 
	
		
			
				|  |  | -      :else v)))
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -;TODO: figure out how to re-evaluate only when the cell modified affects other cells
 | 
	
		
			
				|  |  | -(defn re-evaluate [data]
 | 
	
		
			
				|  |  | -  (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)
 | 
	
		
			
				|  |  | -            evaluated (map (fn [datum] (assoc datum :display (evaluate-expression (subs (:value datum) 1) (:inputs datum)))) prepped-for-eval)
 | 
	
		
			
				|  |  | -            updated-values (copy-display-values values evaluated)]
 | 
	
		
			
				|  |  | -        (println "EVALUATED" evaluated)
 | 
	
		
			
				|  |  | -        (if (nil? not-ready)
 | 
	
		
			
				|  |  | -          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))
 | 
	
		
			
				|  |  | -  (swap! data-atom #(toggle-display % c r :value)))
 | 
	
		
			
				|  |  | -(defn on-change [c r datum e]
 | 
	
		
			
				|  |  | -  (update-value c r datum (.. e -target -value)))
 | 
	
		
			
				|  |  | -(defn on-leave-cell [c r e]
 | 
	
		
			
				|  |  | -  (println (str "leaving cell " c r))
 | 
	
		
			
				|  |  | -  (swap! data-atom #(as-> % data
 | 
	
		
			
				|  |  | -                      (toggle-display data c r :display)
 | 
	
		
			
				|  |  | -                      (add-parsed-variables-to-specific-datum c r data)
 | 
	
		
			
				|  |  | -                      (re-evaluate data))))
 | 
	
		
			
				|  |  | -(defn on-keyPress [c r e]
 | 
	
		
			
				|  |  | -  (println "key press" c r (.. e -which) (.keys js/Object e))
 | 
	
		
			
				|  |  | -  (if (= 13 (.. e -which)) (.focus (.getElementById js/document (str c (inc r))))))
 | 
	
		
			
				|  |  | -  ;TODO TODO TODO TODO TODO TODO TODO TODO TODO
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -;; -------------------------
 | 
	
		
			
				|  |  | -;; Views
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -(defn smart-border [c r]
 | 
	
		
			
				|  |  | -  [:div {:class "smartborder"}
 | 
	
		
			
				|  |  | -   [:div {:class "extendrange button"
 | 
	
		
			
				|  |  | -          :title "extend range"
 | 
	
		
			
				|  |  | -          :on-click #(println "extend range")}]
 | 
	
		
			
				|  |  | -   [:div {:class "fillrange button"
 | 
	
		
			
				|  |  | -          :title "fill range"
 | 
	
		
			
				|  |  | -          :on-click #(println "fill range")}]
 | 
	
		
			
				|  |  | -   [:div {:class "moverange button"
 | 
	
		
			
				|  |  | -          :title "move range"
 | 
	
		
			
				|  |  | -          :on-click #(println "move range")}]
 | 
	
		
			
				|  |  | -   [:div {:class "emptyrange button"
 | 
	
		
			
				|  |  | -          :title "empty range"
 | 
	
		
			
				|  |  | -          :on-click #(println "empty range")}]
 | 
	
		
			
				|  |  | -   [:div {:class "deleterange button"
 | 
	
		
			
				|  |  | -          :title "delete range"
 | 
	
		
			
				|  |  | -          :on-click #(println "delete range")}]
 | 
	
		
			
				|  |  | -   [:div {:class "copyrange button"
 | 
	
		
			
				|  |  | -          :title "copy range"
 | 
	
		
			
				|  |  | -          :on-click #(println "copy range")}]])
 | 
	
		
			
				|  |  | -   
 | 
	
		
			
				|  |  | -  
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -(defn cell [c r data]
 | 
	
		
			
				|  |  | -  (let [datum (some #(if (and (= c (:col %)) (= r (:row %))) %) data)]
 | 
	
		
			
				|  |  | -    ^{:key (str c r)} [:td
 | 
	
		
			
				|  |  | -                       [:input {:id (str c r)
 | 
	
		
			
				|  |  | -                                    :value (if (= (get datum :view nil) :value)
 | 
	
		
			
				|  |  | -                                             (get datum :value "")
 | 
	
		
			
				|  |  | -                                             (get datum :error (get datum :display (get datum :value ""))))
 | 
	
		
			
				|  |  | -                                    :on-change (partial on-change c r datum)
 | 
	
		
			
				|  |  | -                                    :on-blur (partial on-leave-cell c r)
 | 
	
		
			
				|  |  | -                                    :on-focus (partial on-enter-cell c r)
 | 
	
		
			
				|  |  | -                                    :on-paste #(println "paste" %);TODO
 | 
	
		
			
				|  |  | -                                    :on-drop #(println "drop" %);TODO
 | 
	
		
			
				|  |  | -                                    :on-keyPress (partial on-keyPress c r)}]
 | 
	
		
			
				|  |  | -                       (smart-border c r)]))
 | 
	
		
			
				|  |  | -(defn row [r cols data]
 | 
	
		
			
				|  |  | -  ^{:key (str "row-" r)} [:tr
 | 
	
		
			
				|  |  | -                          (cons
 | 
	
		
			
				|  |  | -                            ^{:key (str "row-head-" r)} [:th (str r)]
 | 
	
		
			
				|  |  | -                            (map #(cell % r data) cols))])
 | 
	
		
			
				|  |  | -(defn header-row [cols]
 | 
	
		
			
				|  |  | -  ^{:key "header"} [:tr
 | 
	
		
			
				|  |  | -                    (cons
 | 
	
		
			
				|  |  | -                      ^{:key "corner"} [:th]
 | 
	
		
			
				|  |  | -                      (map (fn [c] ^{:key (str "col-head-" c)} [:th c]) cols))])
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -(defn sheet [data]
 | 
	
		
			
				|  |  | -  [:table [:tbody
 | 
	
		
			
				|  |  | -           (let [maxrow (highest :row data)
 | 
	
		
			
				|  |  | -                 cols (take-while (partial not= (next-letter (highest :col data))) col-letters)]
 | 
	
		
			
				|  |  | -             (cons
 | 
	
		
			
				|  |  | -               (header-row cols)
 | 
	
		
			
				|  |  | -               (map #(row % cols data) (range 1 (inc maxrow)))))]])
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -(defn app []
 | 
	
		
			
				|  |  | -  [:div
 | 
	
		
			
				|  |  | -   ;[:h3 "Microtables"]
 | 
	
		
			
				|  |  | -   [sheet @data-atom]])
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -;; -------------------------
 | 
	
		
			
				|  |  | -;; Initialize app
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -(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")))
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -(defn init! []
 | 
	
		
			
				|  |  | -  (mount-root))
 | 
	
		
			
				|  |  | -
 |