data.cljs 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. (ns microtables-frontend.utils.data
  2. (:require
  3. ["mathjs" :as mathjs]
  4. [clojure.string :as string]
  5. [microtables-frontend.utils.coordinates :as coords]))
  6. (defn formula?
  7. "Determines if a value is a fomula.
  8. If it is, it returns it (without the leading equals sign).
  9. If not, it returns nil."
  10. [value]
  11. (if (= (first value) "=")
  12. (subs value 1)
  13. nil))
  14. (def range->commalist
  15. "Converts a range in \"A1:B2\" notation to a comma-separated list of cells: \"A1,A2,B1,B2\"."
  16. (memoize (fn [range-string]
  17. (let [cell-list (coords/parse-range range-string)
  18. strings (map #(str (:col %) (:row %)) cell-list)]
  19. (str "(" (string/join "," strings) ")")))))
  20. (def replace-ranges-in-expression
  21. "Receives an expression string, and replaces all ranges in colon notation (\"A1:B2\") into a comma-separated list of cells (\"A1,A2,B1,B2\")."
  22. (memoize (fn [expression]
  23. (string/replace expression #"\(\s*[A-Z]+[0-9]+\s*:\s*[A-Z]+[0-9]+\s*\)" range->commalist))))
  24. (def parse-variables (memoize (fn [expression]
  25. (as-> (js->clj (.parse mathjs (replace-ranges-in-expression expression))) $
  26. (.filter $ #(true? (.-isSymbolNode ^js %)))
  27. (map #(.-name %) $)
  28. (map #(.toUpperCase %) $)
  29. (filter #(re-matches #"[A-Z]+[0-9]+" %) $)))))
  30. (def str->rc (memoize (fn [s]
  31. (let [c (re-find #"^[A-Z]+" s)
  32. r (js/parseInt (re-find #"[0-9]+$" s))]
  33. {:row r :col c}))))
  34. ; leave in the :inbound references, since they probably have not have changed
  35. (defn add-references
  36. "Parses the expression in the value of a datum, and adds refs as necessary"
  37. [datum]
  38. (let [formula (formula? (:value datum))]
  39. (if formula
  40. (let [vars (parse-variables formula)
  41. refs (map str->rc vars)]
  42. (-> datum
  43. (assoc :refs refs)
  44. (dissoc :error)))
  45. (-> datum
  46. (dissoc :refs)
  47. (dissoc :display)
  48. (dissoc :error)))))
  49. (defn create-all-references
  50. "Starting from a clean slate, add in all references.
  51. This wipes any references that may have been present."
  52. [data]
  53. (reduce-kv
  54. (fn [columns c curr-column]
  55. (assoc columns c (reduce-kv
  56. (fn [rows r datum]
  57. (assoc rows r (add-references (dissoc (dissoc datum :refs) :inbound))))
  58. {}
  59. curr-column)))
  60. {}
  61. data))
  62. ;TODO: re-write create-all-references to use walk-modify-data instead
  63. (defn walk-modify-data
  64. "Walks through the data map and updates each datum by applying f (a function accepting col, row, datum)."
  65. [data f]
  66. (reduce-kv
  67. (fn [columns c curr-column]
  68. (assoc columns c (reduce-kv
  69. (fn [rows r datum]
  70. (assoc rows r (f c r datum)))
  71. {}
  72. curr-column)))
  73. {}
  74. data))
  75. (defn walk-get-refs
  76. "Walks through the data map and returns a list of :col/:row maps for each cell which satisfies the predicate (a function accepting col, row, datum)."
  77. [data pred]
  78. (reduce-kv (fn [l c column]
  79. (->> column
  80. (filter (fn [[r datum]] (pred c r datum)))
  81. (map (fn [[r _]] {:col c :row r}))
  82. (concat l)))
  83. '()
  84. data))
  85. (defn- set-dirty-flags
  86. "Sets the target cell to \"dirty\" and recursively repeat with its back-references all the way up.
  87. Returns the new data set."
  88. ([data c r]
  89. (set-dirty-flags data (list {:col c :row r})))
  90. ([data queue]
  91. (if (empty? queue)
  92. data
  93. (let [cur (first queue)
  94. c (:col cur)
  95. r (:row cur)
  96. datum (get-in data [c r])]
  97. (if (true? (:dirty datum))
  98. (recur data (rest queue))
  99. (let [new-data (assoc-in data [c r :dirty] true)
  100. new-queue (concat (rest queue) (:inbound datum))]
  101. (recur new-data new-queue)))))))
  102. (defn change-datum-value
  103. "Modify the value of a datum in the table, and update all applicable references"
  104. [data c r value]
  105. (let [datum (get-in data [c r])
  106. updated (assoc datum :value value)]
  107. (-> data
  108. (assoc-in [c r :value] value)
  109. (set-dirty-flags c r))))