|
1 | 1 | (ns excel-clj.poi
|
2 |
| - "Interface that sits one level above Apache POI. |
| 2 | + "Exposes a low level cell writer that uses Apache POI. |
3 | 3 |
|
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." |
7 | 6 | {:author "Matthew Downey"}
|
8 | 7 | (:require [clojure.java.io :as io]
|
9 | 8 | [taoensso.encore :as enc]
|
10 | 9 | [excel-clj.style :as style]
|
11 | 10 | [clojure.walk :as walk]
|
12 | 11 | [taoensso.tufte :as tufte])
|
13 |
| - (:import (java.io Closeable) |
| 12 | + (:import (java.io Closeable BufferedInputStream InputStream) |
14 | 13 | (org.apache.poi.ss.usermodel RichTextString Sheet Cell Row Workbook)
|
15 | 14 | (java.util Date Calendar)
|
16 | 15 | (org.apache.poi.ss.util CellRangeAddress)
|
|
22 | 21 |
|
23 | 22 |
|
24 | 23 | (defprotocol IWorkbookWriter
|
| 24 | + (dissoc-sheet! [this sheet-name] |
| 25 | + "If there's a sheet with the given name, get rid of it.") |
25 | 26 | (workbook* [this]
|
26 |
| - "Get the underlying Apache POI XSSFWorkbook object.")) |
| 27 | + "Get the underlying Apache POI Workbook object.")) |
27 | 28 |
|
28 | 29 |
|
29 | 30 | (defprotocol IWorksheetWriter
|
|
159 | 160 | (workbook* [this]
|
160 | 161 | workbook)
|
161 | 162 |
|
| 163 | + (dissoc-sheet! [this sheet-name] |
| 164 | + (when-let [sh (.getSheet workbook sheet-name)] |
| 165 | + (.removeSheetAt workbook (.getSheetIndex workbook sh)) |
| 166 | + sh)) |
| 167 | + |
162 | 168 | Closeable
|
163 | 169 | (close [this]
|
164 | 170 | (tufte/p :write-to-disk
|
|
171 | 177 | (.close workbook))))))
|
172 | 178 |
|
173 | 179 |
|
| 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 | + |
174 | 186 | (defn ^SheetWriter sheet-writer
|
175 | 187 | "Create a writer for an individual sheet within the workbook."
|
176 | 188 | [workbook-writer sheet-name]
|
|
179 | 191 | (fn [style]
|
180 | 192 | (let [style (enc/nested-merge style/default-style style)]
|
181 | 193 | (style/build-style workbook style))))
|
182 |
| - sheet (.createSheet workbook ^String sheet-name)] |
| 194 | + sheet (create-sheet workbook sheet-name)] |
183 | 195 |
|
184 | 196 | (map->SheetWriter
|
185 | 197 | {:cell-style-cache cache
|
|
209 | 221 | :owns-created-stream? true})))
|
210 | 222 |
|
211 | 223 |
|
| 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 | + |
212 | 243 | (defn ^WorkbookWriter stream-writer
|
213 | 244 | "Open a stream writer for Excel workbooks.
|
214 | 245 |
|
215 | 246 | If `streaming?` is true (default), uses Apache POI streaming implementations.
|
216 | 247 |
|
217 | 248 | 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!" |
219 | 250 | ([stream]
|
220 | 251 | (stream-writer stream true))
|
221 | 252 | ([stream streaming?]
|
|
225 | 256 | :owns-created-stream? false})))
|
226 | 257 |
|
227 | 258 |
|
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 | + |
230 | 271 |
|
231 |
| - (with-open [w (writer "test.xlsx") |
| 272 | +(defn example [file-to-write-to] |
| 273 | + (with-open [w (writer file-to-write-to) |
232 | 274 | t (sheet-writer w "Test")]
|
233 | 275 | (let [header-style {:border-bottom :thin :font {:bold true}}]
|
234 | 276 | (write! t "First Col" header-style 1 1)
|
|
253 | 295 | (newline! t)
|
254 | 296 | (write! t "Wide" nil 2 1)
|
255 | 297 | (write! t "Wider" nil 3 1)
|
256 |
| - (write! t "Much Wider" nil 5 1))) |
| 298 | + (write! t "Much Wider" nil 5 1)))) |
257 | 299 |
|
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)))))) |
259 | 331 |
|
260 | 332 |
|
261 | 333 | (defn performance-test
|
|
288 | 360 |
|
289 | 361 |
|
290 | 362 | (comment
|
291 |
| - "Testing overall performance, plus looking at streaming vs not streaming." |
| 363 | + "Writing cells very manually" |
| 364 | + (example "cells.xlsx") |
292 | 365 |
|
| 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." |
293 | 370 | ;; To get more detailed profiling output
|
294 | 371 | (tufte/add-basic-println-handler! {})
|
295 | 372 |
|
|
0 commit comments