Skip to content

Commit 96f4500

Browse files
committed
Add template functionality + example in poi.clj
1 parent 4c8c0d0 commit 96f4500

File tree

2 files changed

+112
-14
lines changed

2 files changed

+112
-14
lines changed

src/excel_clj/poi.clj

Lines changed: 91 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
(ns excel-clj.poi
2-
"Interface that sits one level above Apache POI.
2+
"Exposes a low level cell writer that uses Apache POI.
33
4-
Handles all apache POI interaction besides styling (style.clj).
5-
See the examples at the bottom of the namespace inside of (comment ...)
6-
expressions for how to use the writers."
4+
See the `example` and `performance-test` functions at the end of
5+
this ns + the adjacent (comment ...) forms for more detail."
76
{:author "Matthew Downey"}
87
(:require [clojure.java.io :as io]
98
[taoensso.encore :as enc]
109
[excel-clj.style :as style]
1110
[clojure.walk :as walk]
1211
[taoensso.tufte :as tufte])
13-
(:import (java.io Closeable)
12+
(:import (java.io Closeable BufferedInputStream InputStream)
1413
(org.apache.poi.ss.usermodel RichTextString Sheet Cell Row Workbook)
1514
(java.util Date Calendar)
1615
(org.apache.poi.ss.util CellRangeAddress)
@@ -22,8 +21,10 @@
2221

2322

2423
(defprotocol IWorkbookWriter
24+
(dissoc-sheet! [this sheet-name]
25+
"If there's a sheet with the given name, get rid of it.")
2526
(workbook* [this]
26-
"Get the underlying Apache POI XSSFWorkbook object."))
27+
"Get the underlying Apache POI Workbook object."))
2728

2829

2930
(defprotocol IWorksheetWriter
@@ -159,6 +160,11 @@
159160
(workbook* [this]
160161
workbook)
161162

163+
(dissoc-sheet! [this sheet-name]
164+
(when-let [sh (.getSheet workbook sheet-name)]
165+
(.removeSheetAt workbook (.getSheetIndex workbook sh))
166+
sh))
167+
162168
Closeable
163169
(close [this]
164170
(tufte/p :write-to-disk
@@ -171,6 +177,12 @@
171177
(.close workbook))))))
172178

173179

180+
(defn ^Sheet create-sheet [^Workbook workbook sheet-name]
181+
(when-let [sh (.getSheet workbook sheet-name)]
182+
(.removeSheetAt workbook (.getSheetIndex workbook sh)))
183+
(.createSheet workbook sheet-name))
184+
185+
174186
(defn ^SheetWriter sheet-writer
175187
"Create a writer for an individual sheet within the workbook."
176188
[workbook-writer sheet-name]
@@ -179,7 +191,7 @@
179191
(fn [style]
180192
(let [style (enc/nested-merge style/default-style style)]
181193
(style/build-style workbook style))))
182-
sheet (.createSheet workbook ^String sheet-name)]
194+
sheet (create-sheet workbook sheet-name)]
183195

184196
(map->SheetWriter
185197
{:cell-style-cache cache
@@ -209,13 +221,32 @@
209221
:owns-created-stream? true})))
210222

211223

224+
(defn- ^XSSFWorkbook appendable [path]
225+
(XSSFWorkbook. (BufferedInputStream. (io/input-stream path))))
226+
227+
228+
(defn ^WorkbookWriter appender
229+
"Like `writer`, but allows overwriting individual sheets within a template
230+
workbook."
231+
([from-path to-path]
232+
(appender from-path to-path true))
233+
([from-path to-path streaming?]
234+
(map->WorkbookWriter
235+
{:workbook (if streaming?
236+
(SXSSFWorkbook. (appendable from-path))
237+
(appendable from-path))
238+
:path to-path
239+
:stream-factory #(io/output-stream (io/file (:path %)))
240+
:owns-created-stream? true})))
241+
242+
212243
(defn ^WorkbookWriter stream-writer
213244
"Open a stream writer for Excel workbooks.
214245
215246
If `streaming?` is true (default), uses Apache POI streaming implementations.
216247
217248
N.B. The streaming version is an order of magnitude faster than the
218-
alternative, so override this default only if you have a good reason!"
249+
alternative, so override this default only if you have a good reason!"
219250
([stream]
220251
(stream-writer stream true))
221252
([stream streaming?]
@@ -225,10 +256,21 @@
225256
:owns-created-stream? false})))
226257

227258

228-
(comment
229-
"For example..."
259+
(defn ^WorkbookWriter stream-appender
260+
"Like `stream-writer`, but allows overwriting individual sheets within a
261+
template workbook."
262+
([from-stream to-stream]
263+
(stream-appender from-stream to-stream true))
264+
([^InputStream from-stream to-stream streaming?]
265+
(map->WorkbookWriter
266+
(let [wb (XSSFWorkbook. from-stream)]
267+
{:workbook (if streaming? (SXSSFWorkbook. wb) wb)
268+
:stream-factory (constantly to-stream)
269+
:owns-created-stream? false}))))
270+
230271

231-
(with-open [w (writer "test.xlsx")
272+
(defn example [file-to-write-to]
273+
(with-open [w (writer file-to-write-to)
232274
t (sheet-writer w "Test")]
233275
(let [header-style {:border-bottom :thin :font {:bold true}}]
234276
(write! t "First Col" header-style 1 1)
@@ -253,9 +295,39 @@
253295
(newline! t)
254296
(write! t "Wide" nil 2 1)
255297
(write! t "Wider" nil 3 1)
256-
(write! t "Much Wider" nil 5 1)))
298+
(write! t "Much Wider" nil 5 1))))
257299

258-
)
300+
301+
(defn template-example [file-to-write-to]
302+
; The template here has a 'raw' sheet, which contains uptime data for 3 time
303+
; series, and a 'Summary' sheet, wich uses formulas + the raw data to compute
304+
; and plot. We're going to overwrite the 'raw' sheet to fill in the template.
305+
(let [template "resources/uptime-template.xlsx"]
306+
(with-open [w (appender template file-to-write-to)
307+
; the template sheet to overwrite completely
308+
sh (sheet-writer w "raw")]
309+
310+
(doseq [header ["Date" ; use the same headers as in the template
311+
"Webserver Uptime"
312+
"REST API Uptime"
313+
"WebSocket API Uptime"]]
314+
(write! sh header))
315+
316+
(newline! sh)
317+
318+
; then write random uptime values in one hour intervals
319+
(let [start-ts (inst-ms #inst"2020-05-01")
320+
one-hour (* 1000 60 60)]
321+
(dotimes [i 99]
322+
(let [row-ts (+ start-ts (* i one-hour))
323+
ymd {:data-format :ymd :alignment :left}]
324+
(write! sh (Date. ^long row-ts) ymd 1 1))
325+
326+
; random uptime values
327+
(write! sh (- 1.0 (rand 0.25)))
328+
(write! sh (- 1.0 (rand 0.25)))
329+
(write! sh (- 1.0 (rand 0.25)))
330+
(newline! sh))))))
259331

260332

261333
(defn performance-test
@@ -288,8 +360,13 @@
288360

289361

290362
(comment
291-
"Testing overall performance, plus looking at streaming vs not streaming."
363+
"Writing cells very manually"
364+
(example "cells.xlsx")
292365

366+
"Filling in a template with random data"
367+
(template-example "filled-in-template.xlsx")
368+
369+
"Testing overall performance, plus looking at streaming vs not streaming."
293370
;; To get more detailed profiling output
294371
(tufte/add-basic-println-handler! {})
295372

test/excel_clj/poi_test.clj

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
(ns excel-clj.poi-test
2+
(:require [clojure.test :refer :all]
3+
4+
[excel-clj.poi :as poi]
5+
[excel-clj.file :as file]))
6+
7+
(deftest poi-writer-test
8+
(is (= (try (poi/example (file/temp ".xlsx")) :success
9+
(catch Exception e e))
10+
:success)
11+
"Example function writes successfully."))
12+
13+
14+
(deftest performance-test
15+
(testing "Performance is reasonable"
16+
(println "Starting performance test -- writing to a temp file...")
17+
(dotimes [_ 3]
18+
(println "")
19+
(is (<= (poi/performance-test (file/temp ".xlsx") 50000) 1000)
20+
"It should be (much) faster than 1 second to write 50k rows."))
21+
(println "Performance test complete.")))

0 commit comments

Comments
 (0)