Skip to content

Commit 08d8818

Browse files
committed
Rewrite & simplify the tree abstraction
1 parent 48050fb commit 08d8818

File tree

4 files changed

+326
-562
lines changed

4 files changed

+326
-562
lines changed

src/excel_clj/core.clj

Lines changed: 63 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,12 @@
1212
{:author "Matthew Downey"}
1313
(:require [excel-clj.cell :refer [style data dims wrapped?]]
1414
[excel-clj.file :as file]
15-
[excel-clj.poi :as poi]
16-
[excel-clj.style :as style]
1715
[excel-clj.tree :as tree]
1816

1917
[clojure.string :as string]
20-
[clojure.java.io :as io]
2118

22-
[taoensso.encore :as enc]
23-
[taoensso.tufte :as tufte]))
19+
[taoensso.tufte :as tufte])
20+
(:import (clojure.lang Named)))
2421

2522

2623
(set! *warn-on-reflection* true)
@@ -32,7 +29,10 @@
3229
(defn best-guess-cell-format
3330
"Try to guess appropriate formatting based on column name and cell value."
3431
[val column-name]
35-
(let [column' (string/lower-case (name column-name))]
32+
(let [column' (string/lower-case
33+
(if (instance? Named column-name)
34+
(name column-name)
35+
(str column-name)))]
3636
(cond
3737
(and (string? val) (> (count val) 75))
3838
{:wrap-text true}
@@ -53,32 +53,32 @@
5353
"Build a lazy sheet grid from `rows`.
5454
5555
Applies default styles to cells which are not already styled, but preserves
56-
any existing styles. Additionally, expands any rows which are wrapped with
57-
style data to apply the style to each cell of the row.
56+
any existing styles.
5857
59-
See the comment block below this function definition for examples.
58+
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+
definition for examples.
6061
6162
This fn has the same shape as clojure.pprint/print-table."
6263
([rows]
63-
(table (keys (first rows)) rows))
64+
(table (keys (data (first rows))) rows))
6465
([ks rows]
6566
(assert (seq ks) "Columns are not empty.")
66-
(let [col-style {:border-bottom :thin :font {:bold true}}]
67+
(let [col-style {:border-bottom :thin :font {:bold true}}
68+
>row (fn [row-style row-data]
69+
(mapv
70+
(fn [key]
71+
(let [cell (get row-data key)]
72+
(style
73+
(if (wrapped? cell)
74+
cell
75+
(style cell (best-guess-cell-format cell key)))
76+
row-style)))
77+
ks))]
6778
(cons
6879
(mapv #(style (data %) col-style) ks)
6980
(for [row rows]
70-
(tufte/p :gen-row
71-
(let [row-style (style row)
72-
row (data row)]
73-
(mapv
74-
(fn [key]
75-
(let [cell (get row key)]
76-
(style
77-
(if (wrapped? cell)
78-
cell
79-
(style cell (best-guess-cell-format cell key)))
80-
row-style)))
81-
ks))))))))
81+
(tufte/p :gen-row (>row (style row) (data row))))))))
8282

8383

8484
(comment
@@ -114,66 +114,48 @@
114114
(table (tdata 100)))}))
115115

116116

117-
;; TODO: (defn tree [])
117+
(defn- tree->rows [t]
118+
(let [total-fmts (sorted-map
119+
0 {:font {:bold true} :border-top :medium}
120+
1 {:border-top :thin :border-bottom :thin})
121+
fmts (sorted-map
122+
0 {:font {:bold true} :border-bottom :medium}
123+
1 {:font {:bold true}}
124+
2 {:indention 2}
125+
3 {:font {:italic true} :alignment :right})
126+
127+
get' (fn [m k] (or (get m k) (val (last m))))]
128+
(tree/table
129+
;; Insert total rows below nodes with children
130+
(fn render [parent node depth]
131+
(if-not (tree/leaf? node)
132+
(let [combined (tree/fold + node)
133+
empty-row (zipmap (keys combined) (repeat nil))]
134+
(concat
135+
; header
136+
[(style (assoc empty-row "" (name parent)) (get' fmts depth))]
137+
; children
138+
(tree/table render node)
139+
; total row
140+
[(style (assoc combined "" "") (get' total-fmts depth))]))
141+
; leaf
142+
[(style (assoc node "" (name parent)) (get' fmts depth))]))
143+
t)))
118144

119145

120146
(defn tree
121-
"Build a sheet grid from the provided tree of data
122-
[Tree Title [[Category Label [Children]] ... [Category Label [Children]]]]
123-
with leaves of the shape [Category Label {:column :value}].
124-
125-
E.g. The assets section of a balance sheet might be represented by the tree
126-
[:balance-sheet
127-
[:assets
128-
[[:current-assets
129-
[[:cash {2018 100M, 2017 90M}]
130-
[:inventory {2018 1500M, 2017 1200M}]]]
131-
[:investments {2018 50M, 2017 45M}]]]]
132-
133-
If provided, the formatters argument is a function that takes the integer
134-
depth of a category (increases with nesting) and returns a cell format for
135-
the row, and total-formatters is the same for rows that are totals."
136-
[t & {:keys [headers formatters total-formatters min-leaf-depth data-format]
137-
:or {formatters style/default-tree-formatters
138-
total-formatters style/default-tree-total-formatters
139-
min-leaf-depth 2
140-
data-format :accounting}}]
141-
(try
142-
(let [tabular (tree/accounting-table (second t) :min-leaf-depth min-leaf-depth)
143-
fmt-or-max (fn [fs n]
144-
(or (get fs n) (second (apply max-key first fs))))
145-
all-colls (or headers
146-
(sequence
147-
(comp
148-
(mapcat keys)
149-
(filter (complement qualified-keyword?))
150-
(distinct))
151-
tabular))
152-
header-style {:font {:bold true} :alignment :right}]
153-
(concat
154-
;; Title
155-
[[(-> (first t)
156-
(style {:alignment :center})
157-
(dims {:width (inc (count all-colls))}))]]
158-
159-
;; Headers
160-
[(into [""] (map #(style % header-style)) all-colls)]
161-
162-
;; Line items
163-
(for [line tabular]
164-
(let [total? (::tree/total? line)
165-
style' (or
166-
(fmt-or-max
167-
(if total? total-formatters formatters)
168-
(::tree/depth line))
169-
{})
170-
style' (enc/nested-merge style' {:data-format data-format})]
171-
(into [(style (::tree/label line) (if total? {} style'))]
172-
(map #(style (get line %) style') all-colls))))))
173-
(catch Exception e
174-
(throw (ex-info "Failed to render tree" {:tree t} e)))))
147+
[t]
148+
(let [ks (into [""] (keys (tree/fold + t)))]
149+
(table ks (tree->rows t))))
175150

176151

152+
(defn with-title
153+
[title [row & _ :as rows]]
154+
(let [width (count row)]
155+
(cons
156+
[(dims title {:width width})]
157+
rows)))
158+
177159
;;; File interaction
178160

179161

@@ -296,9 +278,9 @@
296278

297279
(def example-workbook-data
298280
{"Tree Sheet"
299-
(tree
300-
["Mock Balance Sheet for the year ending Dec 31st, 2018"
301-
tree/mock-balance-sheet])
281+
(let [title "Mock Balance Sheet Ending Dec 31st, 2020"]
282+
(with-title (style title {:alignment :center})
283+
(tree tree/mock-balance-sheet)))
302284

303285
"Tabular Sheet"
304286
(table

src/excel_clj/style.clj

Lines changed: 3 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,9 @@
5858
(coerce-to-obj
5959
workbook :font {:bold true :font-height-in-points 10}))))"
6060
{:author "Matthew Downey"}
61-
(:require [clojure.string :as string])
61+
(:require [clojure.string :as string]
62+
[clojure.reflect :as reflect]
63+
[rhizome.viz :as viz])
6264
(:import (org.apache.poi.ss.usermodel
6365
DataFormat BorderStyle HorizontalAlignment VerticalAlignment
6466
FillPatternType Workbook VerticalAlignment FontUnderline CellStyle)
@@ -283,70 +285,3 @@
283285
(def default-style
284286
"The default cell style."
285287
{:font {:font-height-in-points 10 :font-name "Arial"}})
286-
287-
(defn merge-all
288-
"Recursively merge maps which may be nested.
289-
(merge-nested {:foo {:a :b}} {:foo {:c :d}})
290-
; => {:foo {:a :b, :c :d}}"
291-
[& maps]
292-
(letfn [(merge? [left right]
293-
"Either merge maps or select the latter non-map value."
294-
(if (and (map? left) (map? right))
295-
(merge-2 left right)
296-
right))
297-
(merge-2 [m1 m2]
298-
"Recursively merge the entries in two maps."
299-
(reduce #(trampoline merge-entry %1 %2) (or m1 {}) (seq m2)))
300-
(merge-entry [m e]
301-
"Merge the entry, e, into map m."
302-
(let [k (key e) v (val e)]
303-
(if (contains? m k)
304-
#(assoc m k (merge? (get m k) v))
305-
(assoc m k v))))]
306-
(reduce merge-2 maps)))
307-
308-
(defn merge-style
309-
"Recursively merge the cell's current style with the provided style map,
310-
preserving any style that does not conflict."
311-
[cell style]
312-
(update
313-
(if (map? cell) cell {:value cell})
314-
:style (fn [s] (if-not s style (merge-all s style)))))
315-
316-
;;; Default table formatting functions to produce styles
317-
318-
(defn best-guess-row-format
319-
"Try to guess appropriate formatting based on column name and cell value."
320-
[row-data column]
321-
(let [column' (string/lower-case column)
322-
val (get row-data column)]
323-
(cond
324-
(and (string? val) (> (count val) 75))
325-
{:wrap-text true}
326-
327-
(or (string/includes? column' "percent") (string/includes? column' "%"))
328-
{:data-format :percent}
329-
330-
(string/includes? column' "date")
331-
{:data-format :ymd :alignment :left}
332-
333-
(decimal? val)
334-
{:data-format :accounting}
335-
336-
:else nil)))
337-
338-
(def default-header-style
339-
(constantly
340-
{:border-bottom :thin :font {:bold true}}))
341-
342-
;;; Default tree formatting functions to produce styles
343-
344-
(def default-tree-formatters
345-
{0 {:font {:bold true} :border-bottom :medium}
346-
1 {:font {:bold true}}
347-
2 {:indention 2}
348-
3 {:font {:italic true} :alignment :right}})
349-
350-
(def default-tree-total-formatters
351-
{0 {:font {:bold true} :border-top :medium}
352-
1 {:border-top :thin :border-bottom :thin}})

0 commit comments

Comments
 (0)