|
1 | 1 | (ns
|
2 |
| - ^{:doc "A tree for Excel (accounting) data. The format is [Label [Children]] |
3 |
| - for nodes and [Label {:column :value}] for leaves. |
| 2 | + ^{:doc "A key-value tree for Excel (accounting) data. The format is |
| 3 | + [Label [Children]] for nodes and [Label {:column :value}] for leaves. |
4 | 4 |
|
5 | 5 | For any tree, t, the value function returns the sum of the {:column :value}
|
6 | 6 | attributes under the root.
|
|
174 | 174 | of at least this number."
|
175 | 175 | 2)
|
176 | 176 |
|
177 |
| -(defn render-table |
178 |
| - "Render the trees into a 'table': a coll of maps, each map containing :depth |
179 |
| - (distance from the root) and :label (the first part of the [Label [Children]] |
180 |
| - node vector) attributes plus the (attrs) for the node/leaf." |
181 |
| - [& trees] |
182 |
| - ;; All the attr keys plus :depth (root is the max depth) and :label |
183 |
| - (loop [ts trees rendered []] |
| 177 | +(defn tree->raw-table |
| 178 | + "Render the trees into a 'table': a coll of maps, each map containing ::depth |
| 179 | + (distance from the root), ::label (the first part of the [Label [Children]] |
| 180 | + node vector), and ::total? attributes plus the (attrs) for the node/leaf. |
| 181 | + |
| 182 | + The intention being that you can then use the information in the namespace |
| 183 | + qualified attributes to render the tree items into a line format that suits |
| 184 | + you." |
| 185 | + [trees & {:keys [sum-totals?] :or {sum-totals? true}}] |
| 186 | + ;; All the attr keys plus ::depth (root is the max depth) and ::label |
| 187 | + (loop [ts trees, rendered []] |
184 | 188 | (if-let [t (first ts)]
|
185 | 189 | (if (map? t) ;; It's an already processed line, just add it and move on
|
186 | 190 | (recur (rest ts) (conj rendered t))
|
187 | 191 | (let [t-depth (nth t 2 0)]
|
188 | 192 | (if (leaf? t) ;; It's a leaf, so display with all of its attributes
|
189 | 193 | (let [line (merge
|
190 |
| - {:depth (max *min-leaf-depth* t-depth) |
191 |
| - :label (label t)} |
| 194 | + {::depth (max *min-leaf-depth* t-depth) |
| 195 | + ::label (label t)} |
192 | 196 | (value t))]
|
193 | 197 | (recur (rest ts) (conj rendered line)))
|
194 | 198 | ;; It's a node, so add a header, display its children, then a total
|
195 | 199 | (let [;; w/ depth
|
196 | 200 | children' (mapv #(assoc % 2 (inc t-depth)) (children t))
|
197 |
| - total (merge {:depth t-depth :label ""} (value t)) |
198 |
| - ;; Only one child and it's a leaf, no need for a total |
| 201 | + total (when sum-totals? |
| 202 | + (merge {::depth t-depth ::label "" ::total? true} (value t))) |
| 203 | + ;; Only one child and it's a leaf, no need for a total even if |
| 204 | + ;; its enabled |
199 | 205 | show-total? (not (and (= (count (children t)) 1)
|
200 | 206 | (leaf? (first (children t)))))
|
201 |
| - header {:depth t-depth :label (label t)} |
| 207 | + header {::depth t-depth ::label (label t)} |
202 | 208 | next-lines (into
|
203 |
| - (cond-> children' show-total? (conj total)) |
| 209 | + (cond-> children' (and total show-total?) (conj total)) |
204 | 210 | (rest ts))]
|
205 | 211 | (recur next-lines (conj rendered header))))))
|
206 | 212 | rendered)))
|
207 | 213 |
|
| 214 | +(defn raw-table->rendered |
| 215 | + "Given table items with a qualified ::depth and ::label keys, render a table |
| 216 | + string indenting labels with ::depth and keeping other keys as column labels." |
| 217 | + [table-items & {:keys [indent-width] :or {indent-width 2}}] |
| 218 | + (let [indent-str (apply str (repeat indent-width " "))] |
| 219 | + (letfn [(fmt [line-item] |
| 220 | + (-> line-item |
| 221 | + (dissoc ::depth ::label ::total?) |
| 222 | + (assoc "" (str (apply str (repeat (::depth line-item) indent-str)) |
| 223 | + (::label line-item)))))] |
| 224 | + (map fmt table-items)))) |
| 225 | + |
| 226 | +(defn print-table |
| 227 | + "Display tabular data in a way that preserves label indentation in a way the |
| 228 | + clojure.pprint/print-table does not." |
| 229 | + ([xs] |
| 230 | + (print-table xs {})) |
| 231 | + ([xs {:keys [ks empty-str pad-width]}] |
| 232 | + (let [ks (or ks (sequence (comp (mapcat keys) (distinct)) xs)) |
| 233 | + empty-str (or empty-str "-") |
| 234 | + pad-width (or pad-width 2)] |
| 235 | + (let [len (fn [k] |
| 236 | + (let [len' #(or (some-> (% k) str count) 0)] |
| 237 | + (+ pad-width (transduce (map len') (completing max) 0 (conj xs {k k}))))) |
| 238 | + header (into {} (map (juxt identity identity)) ks) |
| 239 | + ks' (mapv (juxt identity len) ks)] |
| 240 | + (doseq [x (cons header xs)] |
| 241 | + (doseq [[k l] ks'] |
| 242 | + (print (format (str "%-" l "s") (get x k empty-str)))) |
| 243 | + (println "")))))) |
| 244 | + |
208 | 245 | (defn headers
|
209 | 246 | "Return a vector of headers in the tree, with any headers given in first-hs
|
210 | 247 | at the beginning and and in last-hs in order."
|
|
214 | 251 | other-headers (apply disj all-headers all-specified)]
|
215 | 252 | (vec (filter all-headers (concat first-hs other-headers last-hs)))))
|
216 | 253 |
|
217 |
| -(defn tbl |
218 |
| - "Display tabular data in a way that preserves label indentation in a way the |
219 |
| - clojure.pprint/print-table does not." |
220 |
| - ([xs] |
221 |
| - (tbl |
222 |
| - (sequence (comp (mapcat keys) (distinct)) xs) |
223 |
| - xs)) |
224 |
| - ([ks xs] |
225 |
| - (let [pad 2 |
226 |
| - len (fn [k] |
227 |
| - (let [len' #(or (some-> (% k) str count) 0)] |
228 |
| - (+ pad (transduce (map len') (completing max) 0 xs)))) |
229 |
| - header (into {} (map (juxt identity identity)) ks) |
230 |
| - ks' (mapv (juxt identity len) ks)] |
231 |
| - (doseq [x (cons header xs)] |
232 |
| - (doseq [[k l] ks'] |
233 |
| - (print (format (str "%-" l "s") (get x k "-")))) |
234 |
| - (println ""))))) |
235 |
| - |
236 |
| -(defn render-tbl-string |
237 |
| - [& trees] |
238 |
| - (letfn [(fmt [{:keys [depth label] :as line}] |
239 |
| - (-> line |
240 |
| - (dissoc :depth :label) |
241 |
| - (assoc "" (str (apply str (repeat depth " ")) label))))] |
242 |
| - (with-out-str |
243 |
| - (tbl (map fmt (apply render-table trees)))))) |
244 |
| - |
245 | 254 | (def mock-balance-sheet
|
246 | 255 | (vector
|
247 | 256 | ["Assets"
|
|
261 | 270 |
|
262 | 271 | (defn example []
|
263 | 272 | ;; Render the tree as a table
|
264 |
| - (println (apply render-tbl-string mock-balance-sheet)) |
| 273 | + (-> mock-balance-sheet tree->raw-table raw-table->rendered print-table) |
265 | 274 |
|
266 | 275 | ;; Do addition or subtraction with trees using the tree-math macro
|
267 | 276 | (let [[assets [_ [liabilities equity]]] mock-balance-sheet]
|
|
302 | 311 | (comment
|
303 | 312 | ;; Or you can visualize with ztellman/rhizome
|
304 | 313 | ;; Keep in mind that this requires $ apt-get install graphviz
|
| 314 | + (use '(rhizome viz)) |
305 | 315 | (rhizome.viz/view-tree
|
306 | 316 | (complement leaf?) children (second mock-balance-sheet)
|
307 | 317 | :edge->descriptor (fn [x y] (when (leaf? y) {:label (label y)}))
|
|
0 commit comments