123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248 |
- (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}])
- (defonce data-atom (r/atom sample-data))
- (defn highest [dir data] (apply max (map dir data)))
- ; COLUMN NAMES
- (defn upgrade-letter-code [s]
- (let [l (last s)]
- (cond
- (empty? s) [65]
- (= l 90) (conj (upgrade-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 (upgrade-letter-code (mapv #(.charCodeAt % 0) lc)))))
- (def col-letters (iterate next-letter "A"))
- ; 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 update-display [c r display]
- (println (str " trying to update " c r " to " display))
- (swap! data-atom (fn [d] (map #(if (and (= r (:row %)) (= c (:col %)) (= (first (:value %)) "=")) (assoc % :display display) %) d))))
- (defn toggle-display [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))))
- ; CALCULATION / FORMULA EVALUATION FUNCTIONS
- (defn temp1 [data]
- (let [first-order (group-by #(not= (first (:value %)) "=") data)
- evaluated-cells (map #(keyword (str (:col %) (:row %))) (first-order true))
- non-evaluated-cells (map #(keyword (str (:col %) (:row %))) (first-order false))
- evaluated-values (map :value (first-order true))
- non-evaluated-values (map #(subs (:value %) 1) (first-order false))]
- {:remaining (zipmap non-evaluated-cells non-evaluated-values)
- :evaluated (zipmap evaluated-cells evaluated-values)}))
- (def parse-variables (memoize (fn [expression]
- (js->clj (.getVariables mathjs expression)))))
- (def evaluate-expression (memoize (fn [expression variables]
- (.evaluate mathjs expression (clj->js variables)))))
- (defn temp3 [[k v]]
- (let [w (name k)
- c (re-find #"^[A-Z]+" w)
- r (.parseInt js/window (re-find #"[0-9]+$" w))]
- (do
- (println "looking at cell " c r)
- (update-display c r v)
- )
- )
- )
- (defn temp2 [evaluated remaining]
- (if (empty? remaining)
- evaluated
- (let [vars (map #(parse-variables (val %)) remaining)
- remaining-vars (zipmap (keys remaining) vars)
- evaluated-vars (map (fn [x] (map #(let [item (keyword %)]
- (cond
- (contains? evaluated item) [% (evaluated item)]
- (contains? remaining item) [% :not-yet]
- :else [% 0]
- )) x)) vars)
- parsed (zipmap (keys remaining) evaluated-vars)
- not-ready (group-by (fn [[ k v ]] (some #(= :not-yet (last %)) v)) parsed)
- ;TODO: detect circular references
- prepared (map (fn [[k v]] [k (evaluate-expression (remaining k) (apply hash-map (flatten v)))]) (not-ready nil))
- new-evaluated (reduce conj evaluated prepared)
- still-remaining (apply hash-map (flatten (map #(list (key %) (remaining (key %))) (not-ready true))))]
- ;remaining ; {:B8 B7 * 2, :C7 D1, :B7 C5 + D6}
- ;vars ; ([B7] [D1] [C5 D6])
- ;remaining-vars ; {:B8 [B7], :C7 [D1], :B7 [C5 D6]}
- ;parsed ; {:B8 ([B7 :not-yet]), :C7 ([D1 0]), :B7 ([C5 269] [D6 4065])}
- ;not-ready ; {true [[:B8 ([B7 :not-yet])]], nil [[:C7 ([D1 0])] [:B7 ([C5 269] [D6 4065])]]}
- ;prepared ; ([:C7 0] [:B7 4334])
- ;still-remaining ; {:B8 B7 * 2}
- ;evaluated; TODO: change name, consider putting "data" directly in here
- (recur new-evaluated still-remaining)
- ;new-evaluated
- )
- ))
- (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}))))
- (defn find-cell [data c r]
- (some #(if (and (= (:col %) c) (= (:row %) r)) %) data))
- (defn find-val [data c r]
- (let [l (find-cell data c r)
- v (get l :display (get l :value))]
- (println "found?" c r v l (get l :value) (get l :display))
- (cond
- (nil? v) 0
- (and (string? v) (= (first v) "=")) :not-yet
- :else v)))
- (defn copy-display-values' [data display-values]
- (if (empty? display-values)
- data
- (let [v (first display-values)
- r (:row v)
- c (:col v)
- inserted (map #(if (and (= r (:row %)) (= c (:col %)) (= (first (:value %)) "=")) (assoc % :display (:display v)) %) data)]
- (recur inserted (rest display-values)))))
- (defn copy-display-values [data display-values]
- (let [removed (map #(-> % (dissoc :vars) (dissoc :refs) (dissoc :found) (dissoc :inputs)) display-values)]
- (into data removed)))
- (defn experimental [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 [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)
- (println)
- (println)
- (println "has-formula" has-formula)
- (println "values" values)
- (println "variables" variables)
- (println "mapped-cell-keys" mapped-cell-keys)
- (println "search-values" search-values)
- (println "not ready to eval" not-ready)
- (println " nil?" (nil? not-ready))
- (println "ready to eval" ready)
- (println "prepped-for-eval" prepped-for-eval)
- (println "evaluated" evaluated)
- (println "updated-values" updated-values)
- (println "done!")
- (println)
- (println "loop " values " " mapped-cell-keys)
- (println "")
- (println "recur " updated-values " " not-ready)
- (if (nil? not-ready)
- updated-values
- (recur updated-values not-ready)
- )
- ))
- )
- )
- ;TODO: figure out how to re-evaluate only when the cell modified affects other cells
- (defn re-evaluate-data []
- (let [rt (temp1 @data-atom)
- new-data (temp2 (:evaluated rt) (:remaining rt))
- update-results (map temp3 new-data)
- ]
- (do
- (println rt)
- (println new-data)
- (println "updating sheet data with above new data")
- (println update-results)
- (println "done")
- )))
- (defn on-enter-cell [c r e]
- (println (str "entering cell " c r))
- (toggle-display c r :value))
- (defn on-leave-cell [c r e]
- (println (str "leaving cell " c r))
- (toggle-display c r :display)
- (re-evaluate-data))
- ;; -------------------------
- ;; Views
- (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 "")))
- :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)}]]
- )
- )
- (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"]
- (do
- (println "hi again")
- (experimental @data-atom)
- "hi again")
- [sheet @data-atom]])
- ;; -------------------------
- ;; Initialize app
- (defn mount-root []
- (r/render [app] (.getElementById js/document "app")))
- (defn init! []
- (mount-root))
|