|  | @@ -35,11 +35,40 @@
 | 
												
													
														
															|  |  
 |  |  
 | 
												
													
														
															|  |  (def col-letters (iterate next-letter "A"))
 |  |  (def col-letters (iterate next-letter "A"))
 | 
												
													
														
															|  |  
 |  |  
 | 
												
													
														
															|  | -(defn get-datum [data c r]
 |  | 
 | 
												
													
														
															|  | -  (some #(if (and (= c (:col %)) (= r (:row %))) %) data))
 |  | 
 | 
												
													
														
															|  | 
 |  | +
 | 
												
													
														
															|  | 
 |  | +(defn order-two-cols
 | 
												
													
														
															|  | 
 |  | +  "Accepts two column names (letters) and returns them in order."
 | 
												
													
														
															|  | 
 |  | +  [col1 col2]
 | 
												
													
														
															|  | 
 |  | +  (cond
 | 
												
													
														
															|  | 
 |  | +    (> (.-length col1) (.-length col2)) [col2 col1]
 | 
												
													
														
															|  | 
 |  | +    (> (.-length col2) (.-length col1)) [col1 col2]
 | 
												
													
														
															|  | 
 |  | +    (= (max col1 col2) col1) [col2 col1]
 | 
												
													
														
															|  | 
 |  | +    :else [col1 col2]))
 | 
												
													
														
															|  | 
 |  | +
 | 
												
													
														
															|  | 
 |  | +; the order goes top to bottom, then left to right - that makes the most sense to me
 | 
												
													
														
															|  | 
 |  | +; I don't know why a different order would be important, or even in what situation order is important at all
 | 
												
													
														
															|  | 
 |  | +(def parse-range
 | 
												
													
														
															|  | 
 |  | +  "Converts a range in \"A1:B2\" notation to a comma-separated list of cells: \"A1,A2,B1,B2\"."
 | 
												
													
														
															|  | 
 |  | +  (memoize (fn [range-string]
 | 
												
													
														
															|  | 
 |  | +             (let [col1 (second (re-find #"\(\s*([A-Z]+)" range-string))
 | 
												
													
														
															|  | 
 |  | +                   col2 (second (re-find #":\s*([A-Z]+)" range-string))
 | 
												
													
														
															|  | 
 |  | +                   row1 (.parseInt js/window (second (re-find #"([0-9]+)\s*:" range-string)))
 | 
												
													
														
															|  | 
 |  | +                   row2 (.parseInt js/window (second (re-find #"([0-9]+)\s*\)" range-string)))
 | 
												
													
														
															|  | 
 |  | +                   [start-col end-col] (order-two-cols col1 col2)
 | 
												
													
														
															|  | 
 |  | +                   start-row (min row1 row2)
 | 
												
													
														
															|  | 
 |  | +                   end-row (max row1 row2)]
 | 
												
													
														
															|  | 
 |  | +               (str "(" (clojure.string/join "," (for [col (take-while #(not= (next-letter end-col) %) (iterate next-letter start-col))
 | 
												
													
														
															|  | 
 |  | +                                                       row (range start-row (inc end-row))]
 | 
												
													
														
															|  | 
 |  | +                                                  (str col row))) ")")))))
 | 
												
													
														
															|  | 
 |  | +
 | 
												
													
														
															|  | 
 |  | +(def replace-ranges-in-expression
 | 
												
													
														
															|  | 
 |  | +  "Receives an expression string, and replaces all ranges in colon notation (\"A1:B2\") into a comma-separated list of cells (\"A1,A2,B1,B2\")."
 | 
												
													
														
															|  | 
 |  | +  (memoize (fn [expression]
 | 
												
													
														
															|  | 
 |  | +             (clojure.string/replace expression #"\(\s*[A-Z]+[0-9]+\s*:\s*[A-Z]+[0-9]+\s*\)" parse-range))))
 | 
												
													
														
															|  | 
 |  | +
 | 
												
													
														
															|  |  
 |  |  
 | 
												
													
														
															|  |  (def parse-variables (memoize (fn [expression]
 |  |  (def parse-variables (memoize (fn [expression]
 | 
												
													
														
															|  | -                                (as-> (js->clj (.parse mathjs expression)) $
 |  | 
 | 
												
													
														
															|  | 
 |  | +                                (as-> (js->clj (.parse mathjs (replace-ranges-in-expression expression))) $
 | 
												
													
														
															|  |                                    (.filter $ #(true? (.-isSymbolNode %)))
 |  |                                    (.filter $ #(true? (.-isSymbolNode %)))
 | 
												
													
														
															|  |                                    (map #(.-name %) $)
 |  |                                    (map #(.-name %) $)
 | 
												
													
														
															|  |                                    (map #(.toUpperCase %) $)
 |  |                                    (map #(.toUpperCase %) $)
 | 
												
											
												
													
														
															|  | @@ -50,26 +79,17 @@
 | 
												
													
														
															|  |                                r (.parseInt js/window (re-find #"[0-9]+$" s))]
 |  |                                r (.parseInt js/window (re-find #"[0-9]+$" s))]
 | 
												
													
														
															|  |                            {:row r :col c}))))
 |  |                            {:row r :col c}))))
 | 
												
													
														
															|  |  
 |  |  
 | 
												
													
														
															|  | -(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))))
 |  | 
 | 
												
													
														
															|  | -
 |  | 
 | 
												
													
														
															|  |  ; leave in the :inbound references, since they probably have not have changed
 |  |  ; leave in the :inbound references, since they probably have not have changed
 | 
												
													
														
															|  |  (defn add-references
 |  |  (defn add-references
 | 
												
													
														
															|  | -  "Parses the expression in the value of a datum, and adds vars and refs as necessary"
 |  | 
 | 
												
													
														
															|  | 
 |  | +  "Parses the expression in the value of a datum, and adds refs as necessary"
 | 
												
													
														
															|  |    [datum]
 |  |    [datum]
 | 
												
													
														
															|  |    (if (= (first (:value datum)) "=")
 |  |    (if (= (first (:value datum)) "=")
 | 
												
													
														
															|  |      (let [vars (parse-variables (subs (:value datum) 1))
 |  |      (let [vars (parse-variables (subs (:value datum) 1))
 | 
												
													
														
															|  |            refs (map str->rc vars)]
 |  |            refs (map str->rc vars)]
 | 
												
													
														
															|  |        (-> datum
 |  |        (-> datum
 | 
												
													
														
															|  | -          (assoc :vars vars)
 |  | 
 | 
												
													
														
															|  |            (assoc :refs refs)
 |  |            (assoc :refs refs)
 | 
												
													
														
															|  |            (dissoc :error)))
 |  |            (dissoc :error)))
 | 
												
													
														
															|  |      (-> datum
 |  |      (-> datum
 | 
												
													
														
															|  | -        (dissoc :vars)
 |  | 
 | 
												
													
														
															|  |          (dissoc :refs)
 |  |          (dissoc :refs)
 | 
												
													
														
															|  |          (dissoc :display)
 |  |          (dissoc :display)
 | 
												
													
														
															|  |          (dissoc :error))))
 |  |          (dissoc :error))))
 | 
												
											
												
													
														
															|  | @@ -177,15 +197,37 @@
 | 
												
													
														
															|  |            (denotify-references {:col c :row r} (:refs datum))
 |  |            (denotify-references {:col c :row r} (:refs datum))
 | 
												
													
														
															|  |            (notify-references {:col c :row r} (:refs parsed))))))
 |  |            (notify-references {:col c :row r} (:refs parsed))))))
 | 
												
													
														
															|  |  
 |  |  
 | 
												
													
														
															|  | 
 |  | +(defn remove-valueless-range-elements
 | 
												
													
														
															|  | 
 |  | +  "Remove nil values specifically from ranges (to solve issues with some functions like average)."
 | 
												
													
														
															|  | 
 |  | +  [variables var-list]
 | 
												
													
														
															|  | 
 |  | +  (println "remove-valueless-range-elements" variables var-list (first var-list))
 | 
												
													
														
															|  | 
 |  | +  (let [l (clojure.string/split (clojure.string/replace (first var-list) #"[()]" "") #",")
 | 
												
													
														
															|  | 
 |  | +        has-values (filter #(not (nil? (variables %))) l)]
 | 
												
													
														
															|  | 
 |  | +    (str "(" (clojure.string/join "," has-values) ")")))
 | 
												
													
														
															|  | 
 |  | +
 | 
												
													
														
															|  | 
 |  | +
 | 
												
													
														
															|  | 
 |  | +
 | 
												
													
														
															|  | 
 |  | +(defn preprocess-expression
 | 
												
													
														
															|  | 
 |  | +  "Handle range cases, rename certain functions (to work with math.js), prepare expression and variables for processing."
 | 
												
													
														
															|  | 
 |  | +  [expression variables]
 | 
												
													
														
															|  | 
 |  | +  (let [renamed-expression (clojure.string/replace expression #"\baverage\(" "mean(")
 | 
												
													
														
															|  | 
 |  | +        new-expression (clojure.string/replace renamed-expression #"\(([A-Z]+[0-9]+,)*[A-Z]+[0-9]+\)" (partial remove-valueless-range-elements variables))
 | 
												
													
														
															|  | 
 |  | +        new-variables (reduce-kv #(assoc %1 %2 (if (nil? %3) "0" %3)) {} variables)]
 | 
												
													
														
															|  | 
 |  | +    (println "PREPROCESS" {:expression new-expression :variables new-variables})
 | 
												
													
														
															|  | 
 |  | +    {:expression new-expression
 | 
												
													
														
															|  | 
 |  | +     :variables new-variables}))
 | 
												
													
														
															|  | 
 |  | +
 | 
												
													
														
															|  |  
 |  |  
 | 
												
													
														
															|  |  (def evaluate-expression
 |  |  (def evaluate-expression
 | 
												
													
														
															|  |    "Convert (via mathjs) an expression string to a final answer (also a string).  A map of variables must also be provided. If there is an error, it will return :calc-error."
 |  |    "Convert (via mathjs) an expression string to a final answer (also a string).  A map of variables must also be provided. If there is an error, it will return :calc-error."
 | 
												
													
														
															|  |    (memoize (fn [expression variables]
 |  |    (memoize (fn [expression variables]
 | 
												
													
														
															|  | -             (try
 |  | 
 | 
												
													
														
															|  | -               (.evaluate mathjs expression (clj->js variables))
 |  | 
 | 
												
													
														
															|  | -               (catch js/Error e
 |  | 
 | 
												
													
														
															|  | -                 (println "mathjs evaluation error" (.-message e) e)
 |  | 
 | 
												
													
														
															|  | -                 :calc-error)))))
 |  | 
 | 
												
													
														
															|  | 
 |  | +             (let [range-replaced (replace-ranges-in-expression expression)
 | 
												
													
														
															|  | 
 |  | +                   {ready-expression :expression ready-variables :variables} (preprocess-expression range-replaced variables)]
 | 
												
													
														
															|  | 
 |  | +               (try
 | 
												
													
														
															|  | 
 |  | +                 (.evaluate mathjs ready-expression (clj->js ready-variables))
 | 
												
													
														
															|  | 
 |  | +                 (catch js/Error e
 | 
												
													
														
															|  | 
 |  | +                   (println "mathjs evaluation error" (.-message e) e)
 | 
												
													
														
															|  | 
 |  | +                   :calc-error))))))
 | 
												
													
														
															|  |  
 |  |  
 | 
												
													
														
															|  |  ;TODO: deal with lowercase cell references
 |  |  ;TODO: deal with lowercase cell references
 | 
												
													
														
															|  |  
 |  |  
 | 
												
											
												
													
														
															|  | @@ -213,11 +255,11 @@
 | 
												
													
														
															|  |          value (:value datum)
 |  |          value (:value datum)
 | 
												
													
														
															|  |          formula? (= (first value) "=")
 |  |          formula? (= (first value) "=")
 | 
												
													
														
															|  |          resolved-refs (map #(merge % (get-in data [(:col %) (:row %)])) refs)
 |  |          resolved-refs (map #(merge % (get-in data [(:col %) (:row %)])) refs)
 | 
												
													
														
															|  | -        evaluated-refs (map #(if (= (first (:value %)) "=") (:display %) (:value % "0")) resolved-refs)
 |  | 
 | 
												
													
														
															|  | 
 |  | +        evaluated-refs (map #(if (= (first (:value %)) "=") (:display %) (:value %)) resolved-refs)
 | 
												
													
														
															|  |          invalid-refs (some nil? resolved-refs)
 |  |          invalid-refs (some nil? resolved-refs)
 | 
												
													
														
															|  |          dirty-refs (some :dirty resolved-refs)
 |  |          dirty-refs (some :dirty resolved-refs)
 | 
												
													
														
															|  |          error-refs (some #(= (:display %) :error) resolved-refs)
 |  |          error-refs (some #(= (:display %) :error) resolved-refs)
 | 
												
													
														
															|  | -        unevaluated-refs (some nil? evaluated-refs)
 |  | 
 | 
												
													
														
															|  | 
 |  | +        ;unevaluated-refs (some nil? evaluated-refs)
 | 
												
													
														
															|  |          cycle-refs (some #(= (:display %) :cycle-error) resolved-refs)
 |  |          cycle-refs (some #(= (:display %) :cycle-error) resolved-refs)
 | 
												
													
														
															|  |          disqualified? (or invalid-refs dirty-refs error-refs)]
 |  |          disqualified? (or invalid-refs dirty-refs error-refs)]
 | 
												
													
														
															|  |      (cond
 |  |      (cond
 | 
												
											
												
													
														
															|  | @@ -225,7 +267,7 @@
 | 
												
													
														
															|  |        cycle-refs (-> data                           ; if one of its references has a reference cycle, then this one is "poisoned" as well
 |  |        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] datum)
 | 
												
													
														
															|  |                       (assoc-in [c r :display] :cycle-error))
 |  |                       (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
 |  | 
 | 
												
													
														
															|  | 
 |  | +      ;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
 |  |        disqualified? (-> data                        ; some other error is present
 | 
												
													
														
															|  |                          (assoc-in [c r] datum)
 |  |                          (assoc-in [c r] datum)
 | 
												
													
														
															|  |                          (assoc-in [c r :display] :error))
 |  |                          (assoc-in [c r :display] :error))
 |