|
12 | 12 | {:author "Matthew Downey"}
|
13 | 13 | (:require [excel-clj.cell :refer [style data dims wrapped?]]
|
14 | 14 | [excel-clj.file :as file]
|
15 |
| - [excel-clj.poi :as poi] |
16 |
| - [excel-clj.style :as style] |
17 | 15 | [excel-clj.tree :as tree]
|
18 | 16 |
|
19 | 17 | [clojure.string :as string]
|
20 |
| - [clojure.java.io :as io] |
21 | 18 |
|
22 |
| - [taoensso.encore :as enc] |
23 |
| - [taoensso.tufte :as tufte])) |
| 19 | + [taoensso.tufte :as tufte]) |
| 20 | + (:import (clojure.lang Named))) |
24 | 21 |
|
25 | 22 |
|
26 | 23 | (set! *warn-on-reflection* true)
|
|
32 | 29 | (defn best-guess-cell-format
|
33 | 30 | "Try to guess appropriate formatting based on column name and cell value."
|
34 | 31 | [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)))] |
36 | 36 | (cond
|
37 | 37 | (and (string? val) (> (count val) 75))
|
38 | 38 | {:wrap-text true}
|
|
53 | 53 | "Build a lazy sheet grid from `rows`.
|
54 | 54 |
|
55 | 55 | 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. |
58 | 57 |
|
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. |
60 | 61 |
|
61 | 62 | This fn has the same shape as clojure.pprint/print-table."
|
62 | 63 | ([rows]
|
63 |
| - (table (keys (first rows)) rows)) |
| 64 | + (table (keys (data (first rows))) rows)) |
64 | 65 | ([ks rows]
|
65 | 66 | (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))] |
67 | 78 | (cons
|
68 | 79 | (mapv #(style (data %) col-style) ks)
|
69 | 80 | (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)))))))) |
82 | 82 |
|
83 | 83 |
|
84 | 84 | (comment
|
|
114 | 114 | (table (tdata 100)))}))
|
115 | 115 |
|
116 | 116 |
|
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))) |
118 | 144 |
|
119 | 145 |
|
120 | 146 | (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)))) |
175 | 150 |
|
176 | 151 |
|
| 152 | +(defn with-title |
| 153 | + [title [row & _ :as rows]] |
| 154 | + (let [width (count row)] |
| 155 | + (cons |
| 156 | + [(dims title {:width width})] |
| 157 | + rows))) |
| 158 | + |
177 | 159 | ;;; File interaction
|
178 | 160 |
|
179 | 161 |
|
|
296 | 278 |
|
297 | 279 | (def example-workbook-data
|
298 | 280 | {"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))) |
302 | 284 |
|
303 | 285 | "Tabular Sheet"
|
304 | 286 | (table
|
|
0 commit comments