Skip to content

Commit 4f6600f

Browse files
committed
Add support for merging data into templates
1 parent 08d8818 commit 4f6600f

File tree

8 files changed

+228
-60
lines changed

8 files changed

+228
-60
lines changed

src/excel_clj/cell.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(ns excel-clj.cell
22
"A lightweight wrapper over cell values that allows combining both simple
3-
and wrapped cells with new stile and dimensions."
3+
and wrapped cells with new styles and dimensions."
44
{:author "Matthew Downey"}
55
(:require [taoensso.encore :as enc]))
66

src/excel_clj/core.clj

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
The tree and table functions convert tree formatted or tabular data into a
99
grid of [[cell]].
1010
11-
Run the (example) function at the bottom of this namespace to see more."
11+
See the (comment) form with examples at the bottom of this namespace."
1212
{:author "Matthew Downey"}
1313
(:require [excel-clj.cell :refer [style data dims wrapped?]]
1414
[excel-clj.file :as file]
@@ -17,7 +17,8 @@
1717
[clojure.string :as string]
1818

1919
[taoensso.tufte :as tufte])
20-
(:import (clojure.lang Named)))
20+
(:import (clojure.lang Named)
21+
(java.util Date)))
2122

2223

2324
(set! *warn-on-reflection* true)
@@ -56,7 +57,7 @@
5657
any existing styles.
5758
5859
Additionally, expands any rows which are wrapped with style data to apply the
59-
style to each cell of the row. See the comment block below this function
60+
style to each cell of the row. See the comment form below this function
6061
definition for examples.
6162
6263
This fn has the same shape as clojure.pprint/print-table."
@@ -137,16 +138,68 @@
137138
; children
138139
(tree/table render node)
139140
; total row
140-
[(style (assoc combined "" "") (get' total-fmts depth))]))
141+
(when (> (count node) 1)
142+
[(style (assoc combined "" "") (get' total-fmts depth))])))
141143
; leaf
142144
[(style (assoc node "" (name parent)) (get' fmts depth))]))
143145
t)))
144146

145147

146148
(defn tree
147-
[t]
148-
(let [ks (into [""] (keys (tree/fold + t)))]
149-
(table ks (tree->rows t))))
149+
"Build a lazy sheet grid from `tree`, whose leaves are shaped key->number.
150+
151+
E.g. (tree {:assets {:cash {:usd 100 :eur 100}}})
152+
153+
See the comment form below this definition for examples."
154+
[tree]
155+
(let [ks (into [""] (keys (tree/fold + tree)))]
156+
(table ks (tree->rows tree))))
157+
158+
159+
(comment
160+
161+
"Example: Trees using the 'tree' helper with default formatting."
162+
(let [assets {"Current" {:cash {:usd 100 :eur 100}
163+
:inventory {:usd 500}}
164+
"Other" {:loans {:bank {:usd 500}
165+
:margin {:usd 1000 :eur 30000}}}}
166+
liabilities {"Current" {:accounts-payable {:usd 50 :eur 0}}}]
167+
(file/quick-open!
168+
{"Just Assets"
169+
(tree {"Assets" assets})
170+
171+
"Both in One Tree"
172+
(tree
173+
{"Accounts"
174+
{"Assets" assets
175+
;; Because they're in one tree, assets will sum with liabilities,
176+
;; so we should invert the sign on the liabilities to get a
177+
;; meaningful sum
178+
"Liabilities" (tree/negate liabilities)}})
179+
180+
"Both in Two Trees"
181+
(let [diff (tree/fold
182+
- {:assets-sum (tree/fold + assets)
183+
:liabilities-sum (tree/fold - liabilities)})
184+
no-header rest]
185+
(concat
186+
(tree {"Assets" assets})
187+
[[""]]
188+
(no-header (tree {"Liabilities" liabilities}))
189+
[[""]]
190+
(no-header (tree {"Assets Less Liabilities" diff}))))}))
191+
192+
"Example: Trees using `excel-clj.tree/table` and then using the `table`
193+
helper."
194+
(let [table-data
195+
(->> (tree/table tree/mock-balance-sheet)
196+
(map
197+
(fn [row]
198+
(let [spaces (apply str (repeat (:tree/indent row) " "))]
199+
(-> row
200+
(dissoc :tree/indent)
201+
(update "" #(str spaces %)))))))]
202+
(file/quick-open! {"Defaults" (table ["" 2018 2017] table-data)})))
150203

151204

152205
(defn with-title
@@ -172,6 +225,23 @@
172225
(file/write! workbook path ops)))
173226

174227

228+
(defn append!
229+
"Merge the `workbook` with the one saved at `from-path`, write it to the
230+
given `path`, and return a file object pointing at the written file.
231+
232+
The workbook is a key value collection of (sheet-name grid), either as map or
233+
an association list (if ordering is important).
234+
235+
The 'merge' logic overwrites sheets of the same name in the workbook at
236+
`from-path`, so this function is only capable of appending sheets to a
237+
workbook, not appending cells to a sheet."
238+
([workbook from-path path] (file/append! workbook from-path path))
239+
([workbook from-path path {:keys [streaming? auto-size-cols?]
240+
:or {streaming? true}
241+
:as ops}]
242+
(file/append! workbook from-path path ops)))
243+
244+
175245
(defn write-stream!
176246
"Like `write!`, but for a stream."
177247
([workbook stream]
@@ -182,6 +252,16 @@
182252
(file/write-stream! workbook stream ops)))
183253

184254

255+
(defn append-stream!
256+
"Like `append!`, but for streams."
257+
([workbook from-stream stream]
258+
(file/append-stream! workbook from-stream stream))
259+
([workbook from-stream stream {:keys [streaming? auto-size-cols?]
260+
:or {streaming? true}
261+
:as ops}]
262+
(file/append-stream! workbook from-stream stream ops)))
263+
264+
185265
(defn write-pdf!
186266
"Write the workbook to the given filename and return a file object pointing
187267
at the written file.
@@ -296,3 +376,29 @@
296376

297377
(defn example []
298378
(file/quick-open! example-workbook-data))
379+
380+
381+
(def example-template-data
382+
;; Some mocked tabular uptime data to inject into the template
383+
(let [start-ts (inst-ms #inst"2020-05-01")
384+
one-hour (* 1000 60 60)]
385+
(for [i (range 99)]
386+
{"Date" (Date. ^long (+ start-ts (* i one-hour)))
387+
"Webserver Uptime" (- 1.0 (rand 0.25))
388+
"REST API Uptime" (- 1.0 (rand 0.25))
389+
"WebSocket API Uptime" (- 1.0 (rand 0.25))})))
390+
391+
392+
(comment
393+
"Example: Creating a workbooks different kinds of worksheets"
394+
(example)
395+
396+
"Example: Creating a workbook by filling in a template.
397+
398+
The template here has a 'raw' sheet, which contains uptime data for 3 time
399+
series, and a 'Summary' sheet, wich uses formulas + the raw data to compute
400+
and plot. We're going to overwrite the 'raw' sheet to fill in the template."
401+
(let [template (clojure.java.io/resource "uptime-template.xlsx")
402+
new-data {"raw" (table example-template-data)}]
403+
(file/open (append! new-data template "filled-in-template.xlsx"))))
404+

src/excel_clj/file.clj

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525

2626
(defn write-rows!
27-
"Write the rows via the poi/SheetWriter `sh`, returning the max row width."
27+
"Write the rows via the `poi/SheetWriter` `sh`, returning the max row width."
2828
[sh rows-seq]
2929
(reduce
3030
(fn [n next-row]
@@ -40,7 +40,7 @@
4040

4141

4242
(defn write*
43-
"For best performance, using {:streaming true, :auto-size-cols? false}."
43+
"For best performance, use `{:streaming true, :auto-size-cols? false}`."
4444
[workbook poi-writer {:keys [streaming? auto-size-cols?] :as ops}]
4545
(doseq [[nm rows] workbook
4646
:let [sh (poi/sheet-writer poi-writer nm)
@@ -101,7 +101,29 @@
101101
([workbook stream {:keys [streaming? auto-size-cols?]
102102
:or {streaming? true}
103103
:as ops}]
104-
(with-open [w (poi/stream-writer stream)]
104+
(with-open [w (poi/stream-writer stream streaming?)]
105+
(write* workbook w (assoc ops :streaming? streaming?)))))
106+
107+
108+
(defn append! ; see core/append!
109+
([workbook from-path to-path]
110+
(append! workbook from-path to-path (default-ops workbook)))
111+
([workbook from-path to-path {:keys [streaming? auto-size-cols?]
112+
:or {streaming? true}
113+
:as ops}]
114+
(let [f (io/file (force-extension (str to-path) ".xlsx"))]
115+
(with-open [w (poi/appender from-path f streaming?)]
116+
(write* workbook w (assoc ops :streaming? streaming?)))
117+
f)))
118+
119+
120+
(defn append-stream! ; see core/append-stream!
121+
([workbook from-stream stream]
122+
(append-stream! workbook from-stream stream (default-ops workbook)))
123+
([workbook from-stream stream {:keys [streaming? auto-size-cols?]
124+
:or {streaming? true}
125+
:as ops}]
126+
(with-open [w (poi/stream-appender from-stream stream streaming?)]
105127
(write* workbook w (assoc ops :streaming? streaming?)))))
106128

107129

src/excel_clj/poi.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@
210210
If `streaming?` is true (default), uses Apache POI streaming implementations.
211211
212212
N.B. The streaming version is an order of magnitude faster than the
213-
alternative, so override this default only if you have a good reason!"
213+
alternative, so override this default only if you have a good reason!"
214214
([path]
215215
(writer path true))
216216
([path streaming?]
@@ -302,7 +302,7 @@
302302
; The template here has a 'raw' sheet, which contains uptime data for 3 time
303303
; series, and a 'Summary' sheet, wich uses formulas + the raw data to compute
304304
; and plot. We're going to overwrite the 'raw' sheet to fill in the template.
305-
(let [template "resources/uptime-template.xlsx"]
305+
(let [template (io/resource "uptime-template.xlsx")]
306306
(with-open [w (appender template file-to-write-to)
307307
; the template sheet to overwrite completely
308308
sh (sheet-writer w "raw")]

0 commit comments

Comments
 (0)