Skip to content

Commit 3045b5b

Browse files
committed
Various tree flexibility improvements
1 parent d8431fd commit 3045b5b

File tree

5 files changed

+127
-141
lines changed

5 files changed

+127
-141
lines changed

src/excel_clj/core.clj

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
[excel-clj.style :as style]
1515
[clojure.string :as string]
1616
[clojure.java.io :as io])
17-
(:import (org.apache.poi.ss.usermodel Cell RichTextString BorderStyle)
17+
(:import (org.apache.poi.ss.usermodel Cell RichTextString)
1818
(org.apache.poi.xssf.usermodel XSSFWorkbook XSSFSheet)
1919
(java.io FileOutputStream File)
2020
(java.awt Desktop HeadlessException)
@@ -200,18 +200,19 @@
200200
If provided, the formatters argument is a function that takes the integer
201201
depth of a category (increases with nesting) and returns a cell format for
202202
the row, and total-formatters is the same for rows that are totals."
203-
[t & {:keys [headers formatters total-formatters]
203+
[t & {:keys [headers formatters total-formatters data-format]
204204
:or {formatters style/default-tree-formatters
205-
total-formatters style/default-tree-total-formatters}}]
205+
total-formatters style/default-tree-total-formatters
206+
data-format :accounting}}]
206207
(try
207-
(let [tabular (tree/tree->raw-table (second t))
208+
(let [tabular (tree/accounting-table (second t))
208209
fmt-or-max (fn [fs n]
209210
(or (get fs n) (second (apply max-key first fs))))
210211
all-colls (or headers
211212
(sequence
212213
(comp
213214
(mapcat keys)
214-
(filter (complement #{:depth :label}))
215+
(filter (complement qualified-keyword?))
215216
(distinct))
216217
tabular))
217218
header-style {:font {:bold true} :alignment :right}]
@@ -225,14 +226,14 @@
225226

226227
;; Line items
227228
(for [line tabular]
228-
(let [total? (empty? (str (:label line)))
229+
(let [total? (::tree/total? line)
229230
format (or
230231
(fmt-or-max
231232
(if total? total-formatters formatters)
232-
(:depth line))
233+
(::tree/depth line))
233234
{})
234-
style (style/merge-all format {:data-format :accounting})]
235-
(into [{:value (:label line) :style (if total? {} style)}]
235+
style (style/merge-all format {:data-format data-format})]
236+
(into [{:value (::tree/label line) :style (if total? {} style)}]
236237
(map #(->{:value (get line %) :style style})) all-colls)))))
237238
(catch Exception e
238239
(throw (ex-info "Failed to render tree" {:tree t} e)))))

src/excel_clj/style.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@
175175

176176
(def data-formats
177177
{:accounting "_($* #,##0.00_);_($* (#,##0.00);_($* \"-\"??_);_(@_)"
178+
:number "#.###############"
178179
:ymd "yyyy-MM-dd"
179180
:percent "0.00%"})
180181

src/excel_clj/tree.clj

Lines changed: 110 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,16 @@
22
^{:doc "A key-value tree for Excel (accounting) data. The format is
33
[Label [Children]] for nodes and [Label {:column :value}] for leaves.
44
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."
157
:author "Matthew Downey"} excel-clj.tree
16-
(:require [clojure.string :as string]))
8+
(:require [clojure.string :as string]
9+
[clojure.walk :as cwalk]))
1710

1811
;;; Utilities for vector math
1912

2013
(defn sum-maps
14+
"Similar to (merge-with + ...) but treats nil keys as 0 values."
2115
([m1 m2] ;; nil == 0
2216
(let [all-keys (into #{} (concat (keys m1) (keys m2)))]
2317
(into {} (map (fn [k] [k (+ (or (m1 k) 0) (or (m2 k) 0))])) all-keys)))
@@ -26,9 +20,17 @@
2620

2721
(defn negate-map
2822
[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))
3024

3125
(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+
"
3234
([m1 m2]
3335
(sum-maps m1 (negate-map m2)))
3436
([m1 m2 & ms]
@@ -52,64 +54,41 @@
5254
(second node)))
5355

5456
(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
10274
"Any calls to + or - within form are modified to work on trees and tree
103-
values."
75+
values (maps of numbers)."
10476
[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))
10783
form))
10884

109-
;;; Utilities for constructing & walking trees
85+
;;; Utilities for constructing & walking / modifying trees
11086

11187
(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."
11392
([f tree]
11493
(walk f (complement leaf?) children tree))
11594
([f branch? children root]
@@ -119,13 +98,6 @@
11998
(f node [])))]
12099
(walk root))))
121100

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-
129101
(comment
130102
"For example, create a file tree with nodes listing the :size of each file."
131103
(walk
@@ -165,56 +137,70 @@
165137

166138
;;; Coerce a tree format to a tabular format
167139

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.)"
213199
[table-items & {:keys [indent-width] :or {indent-width 2}}]
214200
(let [indent-str (apply str (repeat indent-width " "))]
215201
(letfn [(fmt [line-item]
216202
(-> line-item
217-
(dissoc ::depth ::label ::total?)
203+
(dissoc ::depth ::label ::total? ::header?)
218204
(assoc "" (str (apply str (repeat (::depth line-item) indent-str))
219205
(::label line-item)))))]
220206
(map fmt table-items))))
@@ -264,16 +250,16 @@
264250
["Equity"
265251
[["Common Stock" {2018 102M, 2017 80M}]]]]]))
266252

267-
(defn balance-sheet-example []
253+
(comment
268254
;; 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)
270256

271257
;; Do addition or subtraction with trees using the tree-math macro
272258
(let [[assets [_ [liabilities equity]]] mock-balance-sheet]
273-
(println "Assets - Liabilities =" (tree-math (- assets liabilities)))
259+
(println "Assets - Liabilities =" (math (- assets liabilities)))
274260
(println "Equity =" (value equity))
275261
(println)
276-
(println "Equity + Liabilities =" (tree-math (+ equity liabilities)))
262+
(println "Equity + Liabilities =" (math (+ equity liabilities)))
277263
(println "Assets =" (value assets))))
278264
; =>
279265
; 2018 2017
@@ -358,7 +344,7 @@
358344
(apply inner-build next-root next-items subsequent)))))))]
359345
(second (apply inner-build "" tabular node-fns))))
360346

361-
(defn tree-table-example []
347+
(comment
362348
(-> (table->trees
363349
;; The table we'll convert to a tree
364350
[{:from "MXN" :to "AUD" :on "BrokerA" :return (rand)}
@@ -378,8 +364,8 @@
378364

379365
;; Finally, by broker
380366
:on)
381-
(tree->raw-table :sum-totals? false)
382-
(raw-table->rendered :indent-width 5)
367+
(unaggregated-table)
368+
(render :indent-width 5)
383369
(print-table {:empty-str "" :pad-width 5})))
384370

385371
; => Return Trade Description

test/excel_clj/core_test.clj

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
(ns excel-clj.core-test
22
(:require [clojure.test :refer :all]
3-
[excel-clj.core :refer :all]
4-
[excel-clj.tree :as tree]))
3+
[excel-clj.core :refer :all]))
54

65
(deftest table-test
76
(let [data [{"Date" "2018-01-01" "% Return" 0.05M "USD" 1500.5005M}
@@ -30,4 +29,3 @@
3029
["" 5 2]
3130
["Tree 2" nil nil]
3231
["Child" -2 -1]]))))
33-

0 commit comments

Comments
 (0)