|
2 | 2 | ^{:doc "A key-value tree for Excel (accounting) data. The format is
|
3 | 3 | [Label [Children]] for nodes and [Label {:column :value}] for leaves.
|
4 | 4 |
|
5 |
| - For any tree, t, the value function returns the sum of the {:column :value} |
6 |
| - attributes under the root. |
7 |
| - (let [t [:everything |
8 |
| - [[:child-1 {:usd 10M :mxn 10M}] |
9 |
| - [:child-2 {:usd 5M :mxn -3M}]]]] |
10 |
| - (value t)) |
11 |
| - ; => {:usd 15M :mxn 7M} |
12 |
| -
|
13 |
| - For some example code, see the functions `balance-sheet-example` or |
14 |
| - `tree-table-example` in this namespace." |
| 5 | + For some example code, check out the various (comment ...) blocks in this |
| 6 | + namespace." |
15 | 7 | :author "Matthew Downey"} excel-clj.tree
|
16 |
| - (:require [clojure.string :as string])) |
| 8 | + (:require [clojure.string :as string] |
| 9 | + [clojure.walk :as cwalk])) |
17 | 10 |
|
18 | 11 | ;;; Utilities for vector math
|
19 | 12 |
|
20 | 13 | (defn sum-maps
|
| 14 | + "Similar to (merge-with + ...) but treats nil keys as 0 values." |
21 | 15 | ([m1 m2] ;; nil == 0
|
22 | 16 | (let [all-keys (into #{} (concat (keys m1) (keys m2)))]
|
23 | 17 | (into {} (map (fn [k] [k (+ (or (m1 k) 0) (or (m2 k) 0))])) all-keys)))
|
|
26 | 20 |
|
27 | 21 | (defn negate-map
|
28 | 22 | [m]
|
29 |
| - (into {} (map (fn [[k v]] [k (* -1M (or v 0))])) m)) |
| 23 | + (into {} (map (fn [[k v]] [k (* -1 (or v 0))])) m)) |
30 | 24 |
|
31 | 25 | (defn subtract-maps
|
| 26 | + "Very important difference from (merge-with - ...): |
| 27 | +
|
| 28 | + (merge-with - {:foo 10} {:foo 5 :bar 5}) |
| 29 | + ; => {:foo 5, :bar 5} |
| 30 | +
|
| 31 | + (subtract-maps {:foo 10} {:foo 5 :bar 5}) |
| 32 | + ; => {:foo 5, :bar -5} |
| 33 | + " |
32 | 34 | ([m1 m2]
|
33 | 35 | (sum-maps m1 (negate-map m2)))
|
34 | 36 | ([m1 m2 & ms]
|
|
52 | 54 | (second node)))
|
53 | 55 |
|
54 | 56 | (defn value
|
55 |
| - "If the node is a leaf, returns the map of values for the leaf. Otherwise |
56 |
| - returns the sum of that value map for all children under the node." |
57 |
| - [node] |
58 |
| - (if-not (leaf? node) |
59 |
| - (loop [children' (children node) summed {}] |
60 |
| - (if-let [nxt (first children')] |
61 |
| - (if (leaf? nxt) |
62 |
| - (recur (rest children') (sum-maps summed (second nxt))) |
63 |
| - (recur (concat (rest children') (children nxt)) summed)) |
64 |
| - summed)) |
65 |
| - (second node))) |
66 |
| - |
67 |
| -(defn force-map |
68 |
| - "Returns the argument if it's a map, otherwise calls `value` on the arg." |
69 |
| - [tree-or-map] |
70 |
| - (if (map? tree-or-map) |
71 |
| - tree-or-map |
72 |
| - (value tree-or-map))) |
73 |
| - |
74 |
| -(defmacro axf |
75 |
| - "Sort of a composition of f with xf, except that xf is applied to _each_ |
76 |
| - argument of f. I.e. f's arguments are transformed by the function xf. |
77 |
| -
|
78 |
| - E.g. 'composing' compare with Math/abs to compare absolute values of the |
79 |
| - numbers: |
80 |
| - (defn abs-compare [n n'] |
81 |
| - (axf compare Math/abs n n')) |
82 |
| -
|
83 |
| - Or comparing two java.util.Date objects: |
84 |
| - ;; This won't work... |
85 |
| - (< (Date.) (Date.)) |
86 |
| - ; => ClassCastException java.util.Date cannot be cast to java.lang.Number |
87 |
| -
|
88 |
| - ;; But this will |
89 |
| - (axf < .getTime (Date.) (Date.)) |
90 |
| - ; => false" |
91 |
| - [f xf & args] |
92 |
| - (let [xformed (map #(-> `(~xf ~%)) args)] |
93 |
| - `(~f ~@xformed))) |
94 |
| - |
95 |
| -(defmacro add-trees [& trees] |
96 |
| - `(axf sum-maps force-map ~@trees)) |
97 |
| - |
98 |
| -(defmacro subtract-trees [& trees] |
99 |
| - `(axf subtract-maps force-map ~@trees)) |
100 |
| - |
101 |
| -(defmacro tree-math |
| 57 | + "Aggregate all of the leaf maps in `tree` by reducing over them with |
| 58 | + `reducing-fn` (defaults to summing maps together). If given a single |
| 59 | + map, returns the map." |
| 60 | + ([tree] |
| 61 | + (value tree sum-maps)) |
| 62 | + ([tree reducing-fn] |
| 63 | + (cond |
| 64 | + (map? tree) tree |
| 65 | + (leaf? tree) (second tree) |
| 66 | + :else |
| 67 | + (transduce |
| 68 | + (comp (filter leaf?) (map second)) |
| 69 | + (completing reducing-fn) |
| 70 | + {} |
| 71 | + (tree-seq (complement leaf?) children tree))))) |
| 72 | + |
| 73 | +(defmacro math |
102 | 74 | "Any calls to + or - within form are modified to work on trees and tree
|
103 |
| - values." |
| 75 | + values (maps of numbers)." |
104 | 76 | [form]
|
105 |
| - (clojure.walk/postwalk-replace |
106 |
| - {'+ `add-trees, '- `subtract-trees} |
| 77 | + (cwalk/postwalk |
| 78 | + (fn [form] |
| 79 | + (if (and (sequential? form) (#{'+ '-} (first form))) |
| 80 | + (let [replace-with ({'+ `sum-maps '- `subtract-maps} (first form))] |
| 81 | + (cons replace-with (map (fn [tree-expr] (list `value tree-expr)) (rest form)))) |
| 82 | + form)) |
107 | 83 | form))
|
108 | 84 |
|
109 |
| -;;; Utilities for constructing & walking trees |
| 85 | +;;; Utilities for constructing & walking / modifying trees |
110 | 86 |
|
111 | 87 | (defn walk
|
112 |
| - "Map f across all [label attrs] and [label [child]] nodes." |
| 88 | + "Map f across all [label attrs] and [label [child]] nodes, depth-first. |
| 89 | +
|
| 90 | + Use with the same `branch?` and `children` functions that you'd give to |
| 91 | + `tree-seq` in order to build a tree of the format used by this namespace." |
113 | 92 | ([f tree]
|
114 | 93 | (walk f (complement leaf?) children tree))
|
115 | 94 | ([f branch? children root]
|
|
119 | 98 | (f node [])))]
|
120 | 99 | (walk root))))
|
121 | 100 |
|
122 |
| -(defn ->tree |
123 |
| - "Construct a tree given the same arguments as `tree-seq`. |
124 |
| -
|
125 |
| - Use in conjunction with some mapping function over the tree to build a tree." |
126 |
| - [branch? children root] |
127 |
| - (walk (fn [node children] [node (vec children)]) branch? children root)) |
128 |
| - |
129 | 101 | (comment
|
130 | 102 | "For example, create a file tree with nodes listing the :size of each file."
|
131 | 103 | (walk
|
|
165 | 137 |
|
166 | 138 | ;;; Coerce a tree format to a tabular format
|
167 | 139 |
|
168 |
| -(def ^:dynamic *min-leaf-depth* |
169 |
| - "For formatting purposes, set this to artificially make a leaf sit at a depth |
170 |
| - of at least this number." |
171 |
| - 2) |
172 |
| - |
173 |
| -(defn tree->raw-table |
174 |
| - "Render the trees into a 'table': a coll of maps, each map containing ::depth |
175 |
| - (distance from the root), ::label (the first part of the [Label [Children]] |
176 |
| - node vector), and ::total? attributes plus the (attrs) for the node/leaf. |
177 |
| - |
178 |
| - The intention being that you can then use the information in the namespace |
179 |
| - qualified attributes to render the tree items into a line format that suits |
180 |
| - you." |
181 |
| - [trees & {:keys [sum-totals?] :or {sum-totals? true}}] |
182 |
| - ;; All the attr keys plus ::depth (root is the max depth) and ::label |
183 |
| - (loop [ts trees, rendered []] |
184 |
| - (if-let [t (first ts)] |
185 |
| - (if (map? t) ;; It's an already processed line, just add it and move on |
186 |
| - (recur (rest ts) (conj rendered t)) |
187 |
| - (let [t-depth (nth t 2 0)] |
188 |
| - (if (leaf? t) ;; It's a leaf, so display with all of its attributes |
189 |
| - (let [line (merge |
190 |
| - {::depth (max *min-leaf-depth* t-depth) |
191 |
| - ::label (label t)} |
192 |
| - (value t))] |
193 |
| - (recur (rest ts) (conj rendered line))) |
194 |
| - ;; It's a node, so add a header, display its children, then a total |
195 |
| - (let [;; w/ depth |
196 |
| - children' (mapv #(assoc % 2 (inc t-depth)) (children t)) |
197 |
| - total (when sum-totals? |
198 |
| - (merge {::depth t-depth ::label "" ::total? true} (value t))) |
199 |
| - ;; Only one child and it's a leaf, no need for a total even if |
200 |
| - ;; its enabled |
201 |
| - show-total? (not (and (= (count (children t)) 1) |
202 |
| - (leaf? (first (children t))))) |
203 |
| - header {::depth t-depth ::label (label t)} |
204 |
| - next-lines (into |
205 |
| - (cond-> children' (and total show-total?) (conj total)) |
206 |
| - (rest ts))] |
207 |
| - (recur next-lines (conj rendered header)))))) |
208 |
| - rendered))) |
209 |
| - |
210 |
| -(defn raw-table->rendered |
211 |
| - "Given table items with a qualified ::depth and ::label keys, render a table |
212 |
| - string indenting labels with ::depth and keeping other keys as column labels." |
| 140 | +(defn accounting-table |
| 141 | + "Render a coll of trees into a coll of tabular maps, where leaf values are |
| 142 | + listed on the same line and aggregated below into a total (default aggregation |
| 143 | + is addition). |
| 144 | +
|
| 145 | + Each item in the coll is a map with ::depth, ::label, ::header?, and ::total? |
| 146 | + attributes, in addition to the attributes in the leaves. |
| 147 | +
|
| 148 | + If an `:aggregate-with` function is provided, total lines are constructed by |
| 149 | + reducing that function over sub-leaves. Defaults to a reducing with `sum-maps`." |
| 150 | + [trees & {:keys [aggregate-with min-leaf-depth] :or {aggregate-with sum-maps |
| 151 | + min-leaf-depth 2}}] |
| 152 | + (->> |
| 153 | + trees |
| 154 | + (mapcat |
| 155 | + (fn [tree] |
| 156 | + (walk |
| 157 | + (fn [node children] |
| 158 | + (if-let [children (seq (flatten children))] |
| 159 | + (concat |
| 160 | + ;; First we show the header |
| 161 | + [{::depth 0 ::label (label node) ::header? true}] |
| 162 | + ;; Then the children & their values |
| 163 | + (mapv #(update % ::depth inc) children) |
| 164 | + ;; And finally an aggregation if there are multiple header children |
| 165 | + ;; or any leaf children |
| 166 | + (when (or (> (count (group-by :depth children)) 2) (not (::header? (first children)))) |
| 167 | + [(merge {::depth 0 ::label "" ::total? true} (value node aggregate-with))])) |
| 168 | + ;; A leaf just has its label & value attrs. The depth is inc'd by each |
| 169 | + ;; parent back to the root, so it does not stay at 0. |
| 170 | + (merge {::depth 0 ::label (label node)} (value node aggregate-with)))) |
| 171 | + tree))) |
| 172 | + (map |
| 173 | + (fn [table-row] |
| 174 | + ;; not a leaf |
| 175 | + (if (or (::header? table-row) (::total? table-row) (not (::depth table-row))) |
| 176 | + table-row |
| 177 | + (update table-row ::depth max min-leaf-depth)))))) |
| 178 | + |
| 179 | +(defn unaggregated-table |
| 180 | + "Similar to account-table, but makes no attempt to aggregate non-leaf headers, |
| 181 | + and accepts a coll of trees." |
| 182 | + [trees] |
| 183 | + (mapcat |
| 184 | + (fn [tree] |
| 185 | + (walk |
| 186 | + (fn [node children] |
| 187 | + (if-let [children (seq (flatten children))] |
| 188 | + (into [{::depth 0 ::label (label node) ::header? true}] (map #(update % ::depth inc)) children) |
| 189 | + (merge {::depth 0 ::label (label node)} (value node)))) |
| 190 | + tree)) |
| 191 | + trees)) |
| 192 | + |
| 193 | +(defn render |
| 194 | + "Given a coll of table items with a qualified ::depth and ::label keys, return |
| 195 | + a table items indenting labels with ::depth and keeping other keys as column |
| 196 | + labels, removing namespace qualified keywords. |
| 197 | +
|
| 198 | + (Used for printing in a string, rather than with Excel.)" |
213 | 199 | [table-items & {:keys [indent-width] :or {indent-width 2}}]
|
214 | 200 | (let [indent-str (apply str (repeat indent-width " "))]
|
215 | 201 | (letfn [(fmt [line-item]
|
216 | 202 | (-> line-item
|
217 |
| - (dissoc ::depth ::label ::total?) |
| 203 | + (dissoc ::depth ::label ::total? ::header?) |
218 | 204 | (assoc "" (str (apply str (repeat (::depth line-item) indent-str))
|
219 | 205 | (::label line-item)))))]
|
220 | 206 | (map fmt table-items))))
|
|
264 | 250 | ["Equity"
|
265 | 251 | [["Common Stock" {2018 102M, 2017 80M}]]]]]))
|
266 | 252 |
|
267 |
| -(defn balance-sheet-example [] |
| 253 | +(comment |
268 | 254 | ;; Render the tree as a table
|
269 |
| - (-> mock-balance-sheet tree->raw-table raw-table->rendered print-table) |
| 255 | + (-> mock-balance-sheet accounting-table render print-table) |
270 | 256 |
|
271 | 257 | ;; Do addition or subtraction with trees using the tree-math macro
|
272 | 258 | (let [[assets [_ [liabilities equity]]] mock-balance-sheet]
|
273 |
| - (println "Assets - Liabilities =" (tree-math (- assets liabilities))) |
| 259 | + (println "Assets - Liabilities =" (math (- assets liabilities))) |
274 | 260 | (println "Equity =" (value equity))
|
275 | 261 | (println)
|
276 |
| - (println "Equity + Liabilities =" (tree-math (+ equity liabilities))) |
| 262 | + (println "Equity + Liabilities =" (math (+ equity liabilities))) |
277 | 263 | (println "Assets =" (value assets))))
|
278 | 264 | ; =>
|
279 | 265 | ; 2018 2017
|
|
358 | 344 | (apply inner-build next-root next-items subsequent)))))))]
|
359 | 345 | (second (apply inner-build "" tabular node-fns))))
|
360 | 346 |
|
361 |
| -(defn tree-table-example [] |
| 347 | +(comment |
362 | 348 | (-> (table->trees
|
363 | 349 | ;; The table we'll convert to a tree
|
364 | 350 | [{:from "MXN" :to "AUD" :on "BrokerA" :return (rand)}
|
|
378 | 364 |
|
379 | 365 | ;; Finally, by broker
|
380 | 366 | :on)
|
381 |
| - (tree->raw-table :sum-totals? false) |
382 |
| - (raw-table->rendered :indent-width 5) |
| 367 | + (unaggregated-table) |
| 368 | + (render :indent-width 5) |
383 | 369 | (print-table {:empty-str "" :pad-width 5})))
|
384 | 370 |
|
385 | 371 | ; => Return Trade Description
|
|
0 commit comments