Skip to content

Commit ef3db33

Browse files
committed
Add a prototype namespace for tentative V2 features
1 parent 941df76 commit ef3db33

File tree

1 file changed

+222
-0
lines changed

1 file changed

+222
-0
lines changed

src/excel_clj/prototype.clj

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
(ns excel-clj.prototype
2+
"Prototype features to be included in v2.0.0 -- everything subject to change."
3+
{:author "Matthew Downey"}
4+
(:require [excel-clj.poi :as poi]
5+
[excel-clj.cell :as cell]
6+
[clojure.java.io :as io]
7+
[clojure.string :as string]
8+
[taoensso.encore :as enc])
9+
(:import (java.io File)
10+
(org.jodconverter.office DefaultOfficeManagerBuilder)
11+
(org.jodconverter OfficeDocumentConverter)
12+
(java.awt Desktop HeadlessException)))
13+
14+
15+
(set! *warn-on-reflection* true)
16+
17+
18+
;;; Code to model the 'cell' an Excel document.
19+
;;; A cell can be either a plain value (a string, java.util.Date, etc.) or a
20+
;;; such a value wrapped inside of a map which also includes style and dimension
21+
;;; data.
22+
23+
24+
(defn wrapped
25+
"If `x` contains cell data wrapped in a map (with style & dimension data),
26+
return it as-is. Otherwise return a wrapped version."
27+
[x]
28+
(if (::wrapped? x)
29+
x
30+
{::wrapped? true ::data x}))
31+
32+
33+
(defn style
34+
"Get the style specification for `x`, or deep-merge its current style spec
35+
with the given `style-map`."
36+
([x]
37+
(or (::style x) {}))
38+
([x style-map]
39+
(let [style-map (enc/nested-merge (style x) style-map)]
40+
(assoc (wrapped x) ::style style-map))))
41+
42+
43+
(defn dims
44+
"Get the {:width N, :height N} dimension map for `x`, or merge in the given
45+
`dims-map` of the same format."
46+
([x]
47+
(or (::dims x) {:width 1 :height 1}))
48+
([x dims-map]
49+
(let [dims-map (merge (dims x) dims-map)]
50+
(assoc (wrapped x) ::dims dims-map))))
51+
52+
53+
(defn data
54+
"If `x` contains cell data wrapped in a map (with style & dimension data),
55+
return the wrapped cell value. Otherwise return as-is."
56+
[x]
57+
(if (::wrapped? x)
58+
(::data x)
59+
x))
60+
61+
62+
;;; Code to build Excel worksheets out of Clojure's data structures
63+
64+
65+
(comment
66+
"I'm not really sure if this stuff is helpful..."
67+
68+
(defn- ensure-rows [sheet n] (into sheet (repeat (- n (count sheet)) [])))
69+
(defn- ensure-cols [row n] (into row (repeat (- n (count row)) nil)))
70+
71+
72+
(defn write
73+
"Write to the cell in the `sheet` grid at `(x, y)`."
74+
[sheet x y cell]
75+
(let [sheet-data (ensure-rows sheet y)
76+
row (-> (get sheet-data y [])
77+
(ensure-cols x)
78+
(assoc x cell))]
79+
(assoc sheet-data y row)))
80+
81+
82+
(defn write-row
83+
"Append `row` to the `sheet` grid."
84+
[sheet row]
85+
(conj (or sheet []) row)))
86+
87+
88+
;; TODO: Table -> [[cell]]
89+
;; TODO: Tree -> [[cell]]
90+
91+
92+
;;; Code to convert [[cell]] to .xlsx documents, etc. -- IO stuff
93+
94+
95+
(defn force-extension [path ext]
96+
(let [path (.getCanonicalPath (io/file path))]
97+
(if (string/ends-with? path ext)
98+
path
99+
(let [sep (re-pattern (string/re-quote-replacement File/separator))
100+
parts (string/split path sep)]
101+
(str
102+
(string/join
103+
File/separator (if (> (count parts) 1) (butlast parts) parts))
104+
"." ext)))))
105+
106+
107+
(defn- write-rows!
108+
"Write the rows via the given sheet-writer, returning the number of rows
109+
written."
110+
[sheet-writer rows-seq]
111+
(reduce
112+
(fn [n next-row]
113+
(doseq [cell next-row]
114+
(let [{:keys [width height]} (dims cell)]
115+
(poi/write! sheet-writer (data cell) (style cell) width height)))
116+
(poi/newline! sheet-writer)
117+
(inc n))
118+
0
119+
rows-seq))
120+
121+
122+
(defn write!
123+
"Write the `workbook` to the given `path` and return a file object pointing
124+
at the written file.
125+
126+
The workbook is a key value collection of (sheet-name grid), either as map or
127+
an association list (if ordering is important)."
128+
[workbook path]
129+
(let [f (io/file (force-extension (str path) ".xlsx"))]
130+
(with-open [w (poi/writer f)]
131+
(doseq [[nm rows] workbook
132+
:let [sh (poi/sheet-writer w nm)
133+
n-written (write-rows! sh rows)]]
134+
;; Only auto-size columns for small sheets, otherwise it takes forever
135+
(when (< n-written 2000)
136+
(dotimes [i 10]
137+
(poi/autosize!! sh i)))))
138+
f))
139+
140+
141+
(defn temp
142+
"Return a (string) path to a temp file with the given extension."
143+
[ext]
144+
(-> (File/createTempFile "generated-sheet" ext) .getCanonicalPath))
145+
146+
147+
(defn- convert-pdf!
148+
"Convert the `from-document`, either a File or a path to any office document,
149+
to pdf format and write the pdf to the given pdf-path.
150+
151+
Requires OpenOffice. See https://github.com/sbraconnier/jodconverter.
152+
153+
Returns a File pointing at the PDF."
154+
[from-document pdf-path]
155+
(let [path (force-extension pdf-path "pdf")
156+
office-manager (.build (DefaultOfficeManagerBuilder.))]
157+
(.start office-manager)
158+
(try
159+
(let [document-converter (OfficeDocumentConverter. office-manager)]
160+
(.convert document-converter (io/file from-document) (io/file path)))
161+
(finally
162+
(.stop office-manager)))
163+
(io/file path)))
164+
165+
166+
(defn write-pdf!
167+
"Write the workbook to the given filename and return a file object pointing
168+
at the written file.
169+
170+
Requires OpenOffice. See https://github.com/sbraconnier/jodconverter.
171+
172+
The workbook is a key value collection of (sheet-name grid), either as map or
173+
an association list (if ordering is important)."
174+
[workbook path]
175+
(let [temp-path (temp ".xlsx")
176+
pdf-file (convert-pdf! (write! workbook temp-path) path)]
177+
(.delete (io/file temp-path))
178+
pdf-file))
179+
180+
181+
(defn open
182+
"Open the given file path with the default program."
183+
[file-path]
184+
(try
185+
(let [f (io/file file-path)]
186+
(.open (Desktop/getDesktop) f)
187+
f)
188+
(catch HeadlessException e
189+
(throw (ex-info "There's no desktop." {:opening file-path} e)))))
190+
191+
192+
(defn quick-open
193+
"Write a workbook to a temp file & open it. Useful for quick repl viewing."
194+
[workbook]
195+
(open (write! workbook (temp ".xlsx"))))
196+
197+
198+
(defn quick-open-pdf
199+
"Write a workbook to a temp file as a pdf & open it. Useful for quick repl
200+
viewing."
201+
[workbook]
202+
(open (write-pdf! workbook (temp ".pdf"))))
203+
204+
205+
(comment
206+
207+
208+
;; Ballpark performance test
209+
(dotimes [_ 5]
210+
(time
211+
(let [header-style {:border-bottom :thin :font {:bold true}}
212+
headers (map #(cell/style % header-style) ["N0" "N1" "N2"])]
213+
(write!
214+
[["Test" (cons headers (for [x (range 100000)] [x (str x) x]))]]
215+
"test.xlsx"))))
216+
; "Elapsed time: 5484.565899 msecs"
217+
; "Elapsed time: 5302.312954 msecs"
218+
; "Elapsed time: 4656.451894 msecs"
219+
; "Elapsed time: 4734.160618 msecs"
220+
; "Elapsed time: 5753.986336 msecs"
221+
222+
)

0 commit comments

Comments
 (0)