Skip to content

Commit 519f331

Browse files
committed
Add backwards compatible tree/table helpers for v1.X
1 parent 8318efb commit 519f331

File tree

3 files changed

+192
-21
lines changed

3 files changed

+192
-21
lines changed

src/excel_clj/core.clj

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
(:require [excel-clj.cell :refer [style data dims wrapped?]]
1414
[excel-clj.file :as file]
1515
[excel-clj.tree :as tree]
16+
[excel-clj.deprecated :as deprecated]
1617

1718
[clojure.string :as string]
1819

@@ -50,7 +51,7 @@
5051
:else nil)))
5152

5253

53-
(defn table
54+
(defn table-grid
5455
"Build a lazy sheet grid from `rows`.
5556
5657
Applies default styles to cells which are not already styled, but preserves
@@ -62,7 +63,7 @@
6263
6364
This fn has the same shape as clojure.pprint/print-table."
6465
([rows]
65-
(table (keys (data (first rows))) rows))
66+
(table-grid (keys (data (first rows))) rows))
6667
([ks rows]
6768
(assert (seq ks) "Columns are not empty.")
6869
(let [col-style {:border-bottom :thin :font {:bold true}}
@@ -92,7 +93,7 @@
9293
"N as %" (/ i 100)}))
9394

9495
(file/quick-open!
95-
{"My Table" (table (tdata 100)) ;; Write a table
96+
{"My Table" (table-grid (tdata 100)) ;; Write a table
9697

9798
;; Write a table that highlights rows where N has a whole square root
9899
"Highlight Table" (let [highlight {:fill-pattern :solid-foreground
@@ -101,7 +102,7 @@
101102
(when (pos? n)
102103
(let [sqrt (Math/sqrt n)]
103104
(zero? (rem sqrt (int sqrt))))))]
104-
(table
105+
(table-grid
105106
(for [row (tdata 100)]
106107
(if (square? (row "N"))
107108
(style row highlight)
@@ -112,7 +113,7 @@
112113
[(-> "My Big Title"
113114
(dims {:width 3})
114115
(style {:alignment :center}))]
115-
(table (tdata 100)))}))
116+
(table-grid (tdata 100)))}))
116117

117118

118119
(defn- tree->rows [t]
@@ -145,15 +146,15 @@
145146
t)))
146147

147148

148-
(defn tree
149+
(defn tree-grid
149150
"Build a lazy sheet grid from `tree`, whose leaves are shaped key->number.
150151
151152
E.g. (tree {:assets {:cash {:usd 100 :eur 100}}})
152153
153154
See the comment form below this definition for examples."
154155
[tree]
155156
(let [ks (into [""] (keys (tree/fold + tree)))]
156-
(table ks (tree->rows tree))))
157+
(table-grid ks (tree->rows tree))))
157158

158159

159160
(comment
@@ -166,10 +167,10 @@
166167
liabilities {"Current" {:accounts-payable {:usd 50 :eur 0}}}]
167168
(file/quick-open!
168169
{"Just Assets"
169-
(tree {"Assets" assets})
170+
(tree-grid {"Assets" assets})
170171

171172
"Both in One Tree"
172-
(tree
173+
(tree-grid
173174
{"Accounts"
174175
{"Assets" assets
175176
;; Because they're in one tree, assets will sum with liabilities,
@@ -183,11 +184,11 @@
183184
:liabilities-sum (tree/fold - liabilities)})
184185
no-header rest]
185186
(concat
186-
(tree {"Assets" assets})
187+
(tree-grid {"Assets" assets})
187188
[[""]]
188-
(no-header (tree {"Liabilities" liabilities}))
189+
(no-header (tree-grid {"Liabilities" liabilities}))
189190
[[""]]
190-
(no-header (tree {"Assets Less Liabilities" diff}))))}))
191+
(no-header (tree-grid {"Assets Less Liabilities" diff}))))}))
191192

192193
"Example: Trees using `excel-clj.tree/table` and then using the `table`
193194
helper."
@@ -199,7 +200,7 @@
199200
(-> row
200201
(dissoc :tree/indent)
201202
(update "" #(str spaces %)))))))]
202-
(file/quick-open! {"Defaults" (table ["" 2018 2017] table-data)})))
203+
(file/quick-open! {"Defaults" (table-grid ["" 2018 2017] table-data)})))
203204

204205

205206
;;; Helpers to manipulate [[cell]] data structures
@@ -253,11 +254,11 @@
253254

254255
(comment
255256
"Example: juxtaposing two grids with different widths and heights"
256-
(let [squares (-> (table (for [i (range 10)] {"X" i "X^2" (* i i)}))
257+
(let [squares (-> (table-grid (for [i (range 10)] {"X" i "X^2" (* i i)}))
257258
(vec)
258259
(update 5 into [(dims "<- This one is 4^2" {:width 2})])
259260
(update 6 into ["^ Juxt should make room for that cell"]))
260-
cubes (table (for [i (range 20)] {"X" i "X^3" (* i i i)}))]
261+
cubes (table-grid (for [i (range 20)] {"X" i "X^3" (* i i i)}))]
261262
(file/quick-open!
262263
{"Juxtapose" (juxtapose squares cubes)}))
263264

@@ -429,10 +430,10 @@
429430
{"Tree Sheet"
430431
(let [title "Mock Balance Sheet Ending Dec 31st, 2020"]
431432
(with-title (style title {:alignment :center})
432-
(tree tree/mock-balance-sheet)))
433+
(tree-grid tree/mock-balance-sheet)))
433434

434435
"Tabular Sheet"
435-
(table
436+
(table-grid
436437
[{"Date" "2018-01-01" "% Return" 0.05M "USD" 1500.5005M}
437438
{"Date" "2018-02-01" "% Return" 0.04M "USD" 1300.20M}
438439
{"Date" "2018-03-01" "% Return" 0.07M "USD" 2100.66666666M}])
@@ -468,6 +469,34 @@
468469
series, and a 'Summary' sheet, wich uses formulas + the raw data to compute
469470
and plot. We're going to overwrite the 'raw' sheet to fill in the template."
470471
(let [template (clojure.java.io/resource "uptime-template.xlsx")
471-
new-data {"raw" (table example-template-data)}]
472+
new-data {"raw" (table-grid example-template-data)}]
472473
(file/open (append! new-data template "filled-in-template.xlsx"))))
473474

475+
476+
;; Some v1.X backwards compatibility
477+
478+
479+
(def ^:deprecated tree (partial deprecated/tree table-grid with-title))
480+
(def ^:deprecated table deprecated/table)
481+
482+
483+
(comment
484+
"Example: Using deprecated `tree` and `table` functions"
485+
(quick-open!
486+
{"tree" (tree
487+
["Mock Balance Sheet for the year ending Dec 31st, 2018"
488+
["Assets"
489+
[["Current Assets"
490+
[["Cash" {2018 100M, 2017 85M}]
491+
["Accounts Receivable" {2018 5M, 2017 45M}]]]
492+
["Investments" {2018 100M, 2017 10M}]
493+
["Other" {2018 12M, 2017 8M}]]]
494+
["Liabilities & Stockholders' Equity"
495+
[["Liabilities"
496+
[["Current Liabilities"
497+
[["Notes payable" {2018 5M, 2017 8M}]
498+
["Accounts payable" {2018 10M, 2017 10M}]]]
499+
["Long-term liabilities" {2018 100M, 2017 50M}]]]
500+
["Equity"
501+
[["Common Stock" {2018 102M, 2017 80M}]]]]]])
502+
"table" (table (for [n (range 100)] {"X" n "X^2" (* n n)}))}))

src/excel_clj/deprecated.clj

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
(ns ^:deprecated excel-clj.deprecated
2+
"To provide some minimal backwards compatibility with v1.x"
3+
(:require [excel-clj.cell :as cell]
4+
[excel-clj.tree :as tree]
5+
[clojure.string :as string]
6+
[taoensso.encore :as enc]))
7+
8+
9+
(defn- best-guess-row-format
10+
"Try to guess appropriate formatting based on column name and cell value."
11+
[row-data column]
12+
(let [column' (string/lower-case column)
13+
val (get row-data column)]
14+
(cond
15+
(and (string? val) (> (count val) 75))
16+
{:wrap-text true}
17+
18+
(or (string/includes? column' "percent") (string/includes? column' "%"))
19+
{:data-format :percent}
20+
21+
(string/includes? column' "date")
22+
{:data-format :ymd :alignment :left}
23+
24+
(decimal? val)
25+
{:data-format :accounting}
26+
27+
:else nil)))
28+
29+
30+
(def ^:private default-header-style
31+
(constantly
32+
{:border-bottom :thin :font {:bold true}}))
33+
34+
35+
(defn ^:deprecated table
36+
"Build a sheet grid from the provided collection of tabular data, where each
37+
item has the format {Column Name, Cell Value}.
38+
If provided
39+
headers is an ordered coll of column names
40+
header-style is a function header-name => style map for the header.
41+
data-style is a function that takes (datum-map, column name) and returns
42+
a style specification or nil for the default style."
43+
[tabular-data & {:keys [headers header-style data-style]
44+
:or {data-style (constantly {})}}]
45+
(let [;; add the headers either in the order they're provided or in the order
46+
;; of (seq) on the first datum
47+
headers (let [direction (if (> (count (last tabular-data))
48+
(count (first tabular-data)))
49+
reverse identity)
50+
hs (or headers (sequence (comp (mapcat keys) (distinct))
51+
(direction tabular-data)))]
52+
(assert (not-empty hs) "Table headers are not empty.")
53+
hs)
54+
;; A little hack to keep track of which numbers excel will right
55+
;; justify, and therefore which headers to right justify by default
56+
numeric? (volatile! #{})
57+
data-cell (fn [col-name row]
58+
(let [style (enc/nested-merge
59+
(or (data-style row col-name) {})
60+
(best-guess-row-format row col-name))]
61+
(when (or (= (:data-format style) :accounting)
62+
(number? (get row col-name "")))
63+
(vswap! numeric? conj col-name))
64+
(cell/style (get row col-name) style)))
65+
getters (map (fn [col-name] #(data-cell col-name %)) headers)
66+
header-style (or header-style
67+
;; Add right alignment if it's an accounting column
68+
(fn [name]
69+
(cond-> (default-header-style name)
70+
(@numeric? name)
71+
(assoc :alignment :right))))]
72+
(cons
73+
(map (fn [x] (cell/style x (header-style x))) headers)
74+
(map (apply juxt getters) tabular-data))))
75+
76+
77+
(def default-tree-formatters
78+
{0 {:font {:bold true} :border-bottom :medium}
79+
1 {:font {:bold true}}
80+
2 {:indention 2}
81+
3 {:font {:italic true} :alignment :right}})
82+
83+
84+
(def default-tree-total-formatters
85+
{0 {:font {:bold true} :border-top :medium}
86+
1 {:border-top :thin :border-bottom :thin}})
87+
88+
89+
(defn old->new-tree [[title tree]]
90+
(let [branch? (complement (fn [x] (and (vector? x) (map? (second x)))))
91+
children #(when (vector? %) (second %))]
92+
(tree/tree branch? children tree first second)))
93+
94+
95+
(defn ^:deprecated tree
96+
"Build a sheet grid from the provided tree of data
97+
[Tree Title [[Category Label [Children]] ... [Category Label [Children]]]]
98+
with leaves of the shape [Category Label {:column :value}].
99+
E.g. The assets section of a balance sheet might be represented by the tree
100+
[:balance-sheet
101+
[:assets
102+
[[:current-assets
103+
[[:cash {2018 100M, 2017 90M}]
104+
[:inventory {2018 1500M, 2017 1200M}]]]
105+
[:investments {2018 50M, 2017 45M}]]]]
106+
If provided, the formatters argument is a function that takes the integer
107+
depth of a category (increases with nesting) and returns a cell format for
108+
the row, and total-formatters is the same for rows that are totals."
109+
[core-table with-title t & {:keys [headers formatters total-formatters
110+
min-leaf-depth data-format]
111+
:or {formatters default-tree-formatters
112+
total-formatters default-tree-total-formatters
113+
min-leaf-depth 2
114+
data-format :accounting}}]
115+
(let [title (first t)
116+
t (old->new-tree t)
117+
fmts (into (sorted-map) formatters)
118+
total-fmts (into (sorted-map) total-formatters)
119+
get' (fn [m k] (or (get m k) (val (last m))))]
120+
(with-title title
121+
(core-table
122+
(into [""] (remove #{""}) (or headers (keys (tree/fold + t))))
123+
(tree/table
124+
;; Insert total rows below nodes with children
125+
(fn render [parent node depth]
126+
(if-not (tree/leaf? node)
127+
(let [combined (tree/fold + node)
128+
empty-row (zipmap (keys combined) (repeat nil))]
129+
(concat
130+
; header
131+
[(cell/style
132+
(assoc empty-row "" (name parent))
133+
(get' fmts depth))]
134+
; children
135+
(tree/table render node)
136+
; total row
137+
(when (> (count node) 1)
138+
[(cell/style (assoc combined "" "") (get' total-fmts depth))])))
139+
; leaf
140+
[(cell/style (assoc node "" (name parent))
141+
(get' fmts (max min-leaf-depth depth)))]))
142+
t)))))

test/excel_clj/core_test.clj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
(let [td [{"Date" "2018-01-01" "% Return" 0.05M "USD" 1500.5005M}
1212
{"Date" "2018-02-01" "% Return" 0.04M "USD" 1300.20M}
1313
{"Date" "2018-03-01" "% Return" 0.07M "USD" 2100.66666666M}]
14-
generated (table td)]
14+
generated (table-grid td)]
1515
(testing "Generated grid has the expected shape for the tabular data"
1616
(is (= (mapv #(mapv data %) generated)
1717
[["Date" "% Return" "USD"]
@@ -26,7 +26,7 @@
2626
"Another" {2018 3, 2017 1}}
2727
"Tree 2" {"Child" {2018 -2, 2017 -1}}}}]
2828
(testing "Renders tree into a grid with a title and total rows."
29-
(is (= (mapv #(mapv :value %) (tree data)))
29+
(is (= (mapv #(mapv :value %) (tree-grid data)))
3030
[["Title"]
3131
[nil 2018 2017]
3232
["Tree 1" nil nil]
@@ -51,7 +51,7 @@
5151
(try
5252
(testing "Example code snippet writes successfully."
5353
(let [template (clojure.java.io/resource "uptime-template.xlsx")
54-
new-data {"raw" (table example-template-data)}]
54+
new-data {"raw" (table-grid example-template-data)}]
5555
(append! new-data template "filled-in-template.xlsx")))
5656
(finally
5757
(io/delete-file temp-file)))))

0 commit comments

Comments
 (0)