zaxcel 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +4 -0
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +29 -0
- data/CONTRIBUTING.md +110 -0
- data/LICENSE +22 -0
- data/QUICK_START.md +187 -0
- data/README.md +372 -0
- data/Rakefile +18 -0
- data/SETUP.md +178 -0
- data/lib/enumerable.rb +47 -0
- data/lib/zaxcel/README.md +37 -0
- data/lib/zaxcel/arithmetic.rb +88 -0
- data/lib/zaxcel/binary_expression.rb +74 -0
- data/lib/zaxcel/binary_expressions/addition.rb +36 -0
- data/lib/zaxcel/binary_expressions/division.rb +24 -0
- data/lib/zaxcel/binary_expressions/multiplication.rb +24 -0
- data/lib/zaxcel/binary_expressions/subtraction.rb +41 -0
- data/lib/zaxcel/binary_expressions.rb +38 -0
- data/lib/zaxcel/cell.rb +141 -0
- data/lib/zaxcel/cell_formula.rb +16 -0
- data/lib/zaxcel/column.rb +142 -0
- data/lib/zaxcel/document.rb +136 -0
- data/lib/zaxcel/function.rb +6 -0
- data/lib/zaxcel/functions/abs.rb +18 -0
- data/lib/zaxcel/functions/and.rb +23 -0
- data/lib/zaxcel/functions/average.rb +17 -0
- data/lib/zaxcel/functions/choose.rb +20 -0
- data/lib/zaxcel/functions/concatenate.rb +20 -0
- data/lib/zaxcel/functions/if.rb +38 -0
- data/lib/zaxcel/functions/if_error.rb +25 -0
- data/lib/zaxcel/functions/index.rb +20 -0
- data/lib/zaxcel/functions/len.rb +16 -0
- data/lib/zaxcel/functions/match/match_type.rb +13 -0
- data/lib/zaxcel/functions/match.rb +27 -0
- data/lib/zaxcel/functions/max.rb +17 -0
- data/lib/zaxcel/functions/min.rb +17 -0
- data/lib/zaxcel/functions/negate.rb +26 -0
- data/lib/zaxcel/functions/or.rb +23 -0
- data/lib/zaxcel/functions/round.rb +20 -0
- data/lib/zaxcel/functions/sum.rb +18 -0
- data/lib/zaxcel/functions/sum_if.rb +20 -0
- data/lib/zaxcel/functions/sum_ifs.rb +34 -0
- data/lib/zaxcel/functions/sum_product.rb +18 -0
- data/lib/zaxcel/functions/text.rb +17 -0
- data/lib/zaxcel/functions/unique.rb +23 -0
- data/lib/zaxcel/functions/x_lookup.rb +28 -0
- data/lib/zaxcel/functions/xirr.rb +27 -0
- data/lib/zaxcel/functions.rb +169 -0
- data/lib/zaxcel/if_builder.rb +22 -0
- data/lib/zaxcel/lang.rb +23 -0
- data/lib/zaxcel/reference.rb +28 -0
- data/lib/zaxcel/references/cell.rb +42 -0
- data/lib/zaxcel/references/column.rb +49 -0
- data/lib/zaxcel/references/range.rb +35 -0
- data/lib/zaxcel/references/row.rb +34 -0
- data/lib/zaxcel/references.rb +5 -0
- data/lib/zaxcel/roundable.rb +14 -0
- data/lib/zaxcel/row.rb +93 -0
- data/lib/zaxcel/sheet.rb +425 -0
- data/lib/zaxcel/sorbet/enumerizable_enum.rb +50 -0
- data/lib/zaxcel/version.rb +6 -0
- data/lib/zaxcel.rb +73 -0
- data/zaxcel.gemspec +73 -0
- metadata +266 -0
data/lib/zaxcel/sheet.rb
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
class Zaxcel::Sheet
|
|
5
|
+
extend T::Sig
|
|
6
|
+
|
|
7
|
+
HEADER_ROW_KEY = :header_row
|
|
8
|
+
|
|
9
|
+
sig { returns(String) }
|
|
10
|
+
attr_reader :name
|
|
11
|
+
|
|
12
|
+
sig { returns(Zaxcel::Document) }
|
|
13
|
+
attr_reader :document
|
|
14
|
+
|
|
15
|
+
sig { returns(T::Hash[Zaxcel::Cell::CellExtractionKey, Zaxcel::Cell]) }
|
|
16
|
+
attr_reader :cells_by_extraction_key
|
|
17
|
+
|
|
18
|
+
class SheetVisibility < T::Enum
|
|
19
|
+
include Sorbet::EnumerizableEnum
|
|
20
|
+
|
|
21
|
+
enums do
|
|
22
|
+
Visible = new(:visible)
|
|
23
|
+
Hidden = new(:hidden) # Can be unhidden from Excel UI
|
|
24
|
+
# VeryHidden is here if we really need it at some point, but pay attention to:
|
|
25
|
+
# - who is potentially going to be accessing this sheet and how will they react to a sheet referenced which cannot be accessed via UI
|
|
26
|
+
# VeryHidden = new(:very_hidden) # Requires code to unhide (e.g. VBA)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
sig { params(name: String, document: Zaxcel::Document, worksheet: Axlsx::Worksheet, visibility: Zaxcel::Sheet::SheetVisibility).void }
|
|
31
|
+
def initialize(name:, document:, worksheet:, visibility: Zaxcel::Sheet::SheetVisibility::Visible)
|
|
32
|
+
@name = name
|
|
33
|
+
@document = document
|
|
34
|
+
@worksheet = worksheet
|
|
35
|
+
@visibility = visibility
|
|
36
|
+
@cells_by_extraction_key = T.let({}, T::Hash[Zaxcel::Cell::CellExtractionKey, Zaxcel::Cell])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# returns the underlying axlsx worksheet. this an escape hatch to allow operations not supported by zaxcel.
|
|
40
|
+
# this is unsafe because it allows you to mutate the worksheet in ways that zaxcel doesn't know about.
|
|
41
|
+
sig { returns(Axlsx::Worksheet) }
|
|
42
|
+
def unsafe_axlsx_worksheet
|
|
43
|
+
@worksheet
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { returns(String) }
|
|
47
|
+
def to_excel
|
|
48
|
+
@worksheet.name
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sig { void }
|
|
52
|
+
def position_rows!
|
|
53
|
+
rows.each_with_index do |row, i|
|
|
54
|
+
row_position = i + 1 # 1 indexed not 0 indexed
|
|
55
|
+
row.position!(row_position)
|
|
56
|
+
|
|
57
|
+
# First persist the location of each cell
|
|
58
|
+
columns.each_with_index do |column, j|
|
|
59
|
+
column.position!(j)
|
|
60
|
+
|
|
61
|
+
# add an empty cell anywhere that's missing. this makes cell references work more intuitively.
|
|
62
|
+
# for instance, if I want to reference the top left cell in some area so I can define a merge range,
|
|
63
|
+
# it should resolve whether I define that cell or not.
|
|
64
|
+
cell = column.cell(row.name)
|
|
65
|
+
if cell.nil?
|
|
66
|
+
column.add_cell!(row: row)
|
|
67
|
+
elsif cell.to_extract?
|
|
68
|
+
@cells_by_extraction_key[cell.extraction_key] = cell
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sig { returns(T::Array[T.nilable(T.any(Float, Integer))]) }
|
|
75
|
+
def column_widths
|
|
76
|
+
columns.map do |col|
|
|
77
|
+
next T.cast(col.width, T.nilable(T.any(Float, Integer))) if !Zaxcel::Column::ComputedColumnWidth.values.include?(col.width)
|
|
78
|
+
|
|
79
|
+
character_length = case col.width
|
|
80
|
+
when Zaxcel::Column::ComputedColumnWidth::MaxContent
|
|
81
|
+
rows.map do |row|
|
|
82
|
+
cell = col.cell(row.name)
|
|
83
|
+
next 0 if cell.nil? || cell.value.is_a?(Zaxcel::References::Column)
|
|
84
|
+
|
|
85
|
+
cell.estimated_formatted_character_length
|
|
86
|
+
end.compact.max
|
|
87
|
+
when Zaxcel::Column::ComputedColumnWidth::Header
|
|
88
|
+
col.cell(HEADER_ROW_KEY)&.estimated_formatted_character_length || 0
|
|
89
|
+
when Zaxcel::Column::ComputedColumnWidth::HeaderTwoLines
|
|
90
|
+
# if we ever have to do fit to more than two lines (don't think we ever would),
|
|
91
|
+
# just refac this to support N lines and pass N in somehow
|
|
92
|
+
uneven_line_break_buffer = 3
|
|
93
|
+
(((col.cell(HEADER_ROW_KEY)&.estimated_formatted_character_length || 0) / 2) + uneven_line_break_buffer)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
next if character_length.nil?
|
|
97
|
+
|
|
98
|
+
[column_width_from_content_length(character_length), col.min_width].max
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# An alternative to allowing CAXLSX to auto-calculate the width of a column,
|
|
103
|
+
# since it sometimes gives way too much extra space.
|
|
104
|
+
sig { params(content_length: Integer).returns(Integer) }
|
|
105
|
+
def column_width_from_content_length(content_length)
|
|
106
|
+
(document.width_units_by_default_character * content_length).ceil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
sig { void }
|
|
110
|
+
def add_rows_to_worksheet!
|
|
111
|
+
rows.each_with_index do |row, _rowi|
|
|
112
|
+
cells_in_row = columns.map { |col| col.cell(row.name) }
|
|
113
|
+
formatted_cells = cells_in_row.map { |cell| format_cell_contents(cell) }
|
|
114
|
+
@worksheet.add_row(
|
|
115
|
+
formatted_cells,
|
|
116
|
+
style: cells_in_row.map { |c| @document.style(c&.style || :default_cell) },
|
|
117
|
+
height: row.height,
|
|
118
|
+
)
|
|
119
|
+
@worksheet.column_widths(*T.unsafe(column_widths))
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
sig { returns(T.nilable(Zaxcel::Column)) }
|
|
124
|
+
def print_boundary_column
|
|
125
|
+
columns.find(&:print_boundary?)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
sig { returns(T.nilable(Zaxcel::Row)) }
|
|
129
|
+
def print_boundary_row
|
|
130
|
+
rows.find(&:print_boundary?)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
sig { void }
|
|
134
|
+
def set_print_area!
|
|
135
|
+
first_column_to_print = columns.first!
|
|
136
|
+
last_column_to_print = print_boundary_column || T.must(columns.last)
|
|
137
|
+
|
|
138
|
+
first_row_to_print = rows.first!
|
|
139
|
+
last_row_to_print = print_boundary_row || T.must(rows.last)
|
|
140
|
+
|
|
141
|
+
print_area_range = "'#{to_excel}'!#{first_column_to_print.to_excel}#{first_row_to_print.to_excel}:#{last_column_to_print.to_excel}#{last_row_to_print.to_excel}"
|
|
142
|
+
|
|
143
|
+
@document.set_print_area(sheet: @worksheet, range_to_include: print_area_range)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
sig { void }
|
|
147
|
+
def apply_conditional_formatting!
|
|
148
|
+
conditional_formatting.each do |formatting|
|
|
149
|
+
range = formatting[:range]
|
|
150
|
+
if range.is_a?(Zaxcel::References::Column)
|
|
151
|
+
first_cell = range.cells.first!
|
|
152
|
+
last_cell = T.must(range.cells.last)
|
|
153
|
+
else
|
|
154
|
+
range = T.cast(range, T::Array[Zaxcel::References::Cell])
|
|
155
|
+
first_cell = range.first!.cell
|
|
156
|
+
last_cell = T.must(range.last).cell
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
@worksheet.add_conditional_formatting(
|
|
160
|
+
"#{T.must(first_cell).to_excel}:#{T.must(last_cell).to_excel}",
|
|
161
|
+
type: formatting[:rule],
|
|
162
|
+
dxfId: @document.style(formatting[:style]),
|
|
163
|
+
priority: 1, # i don't know what this means :)
|
|
164
|
+
)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
sig { void }
|
|
169
|
+
def apply_cell_merges!
|
|
170
|
+
ranges_to_merge.each do |range|
|
|
171
|
+
cells = range.map(&:cell).compact
|
|
172
|
+
|
|
173
|
+
first_column_cell = cells.min_by { |cell| T.must(cell.col_position) }
|
|
174
|
+
first_row_cell = cells.min_by { |cell| T.must(cell.row_position) }
|
|
175
|
+
last_column_cell = cells.max_by { |cell| T.must(cell.col_position) }
|
|
176
|
+
last_row_cell = cells.max_by { |cell| T.must(cell.row_position) }
|
|
177
|
+
# if any of the references don't resolve, then just skip this range
|
|
178
|
+
next if first_column_cell.nil? || first_row_cell.nil? || last_column_cell.nil? || last_row_cell.nil?
|
|
179
|
+
|
|
180
|
+
top_left_cell_position = "#{first_column_cell.column.to_excel}#{first_row_cell.row.to_excel}"
|
|
181
|
+
bottom_right_cell_position = "#{last_column_cell.column.to_excel}#{last_row_cell.row.to_excel}"
|
|
182
|
+
|
|
183
|
+
@worksheet.merge_cells("#{top_left_cell_position}:#{bottom_right_cell_position}")
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
sig { void }
|
|
188
|
+
def hide_columns!
|
|
189
|
+
columns.select(&:hidden?).each do |column|
|
|
190
|
+
@worksheet.column_info[column.position].hidden = true
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
sig { void }
|
|
195
|
+
def hide_rows!
|
|
196
|
+
rows.each_with_index do |row, i|
|
|
197
|
+
next if !row.hidden?
|
|
198
|
+
|
|
199
|
+
@worksheet.rows[i].hidden = true
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
sig { void }
|
|
204
|
+
def apply_sheet_visibility!
|
|
205
|
+
@worksheet.state = @visibility.serialize
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# it woudl be cool to specify a cell as an image instead...
|
|
209
|
+
sig do
|
|
210
|
+
params(image_path: String, width: Integer, height: Integer, row_position: Integer, column_position: Integer).void
|
|
211
|
+
end
|
|
212
|
+
def add_image_to_worksheet!(image_path, width:, height:, row_position:, column_position:)
|
|
213
|
+
@worksheet.add_image(
|
|
214
|
+
image_src: image_path,
|
|
215
|
+
noMove: true,
|
|
216
|
+
) do |image|
|
|
217
|
+
image.width = width
|
|
218
|
+
image.height = height
|
|
219
|
+
image.start_at(column_position, row_position)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
sig { void }
|
|
224
|
+
def generate_sheet!
|
|
225
|
+
add_rows_to_worksheet!
|
|
226
|
+
set_print_area!
|
|
227
|
+
|
|
228
|
+
apply_cell_merges!
|
|
229
|
+
apply_conditional_formatting!
|
|
230
|
+
hide_columns!
|
|
231
|
+
hide_rows!
|
|
232
|
+
apply_sheet_visibility!
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
sig { params(row_name: T.any(Symbol, String)).returns(T.nilable(Zaxcel::Row)) }
|
|
236
|
+
def row(row_name)
|
|
237
|
+
rows_by_name[row_name.to_sym]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
sig { params(column_name: T.any(Symbol, String)).returns(T.nilable(Zaxcel::Column)) }
|
|
241
|
+
def column(column_name)
|
|
242
|
+
columns_by_name[column_name.to_sym]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
sig do
|
|
246
|
+
params(
|
|
247
|
+
name: T.any(Symbol, String, Numeric),
|
|
248
|
+
style_group: Symbol,
|
|
249
|
+
height: T.nilable(Integer),
|
|
250
|
+
hidden: T::Boolean,
|
|
251
|
+
).returns(Zaxcel::Row)
|
|
252
|
+
end
|
|
253
|
+
def add_row!(name, style_group: :row_style, height: nil, hidden: false)
|
|
254
|
+
name = name.to_s if name.is_a?(Numeric)
|
|
255
|
+
rows_by_name[name.to_sym] ||= Zaxcel::Row.new(
|
|
256
|
+
name.to_sym,
|
|
257
|
+
sheet: self,
|
|
258
|
+
style_group: style_group,
|
|
259
|
+
height: height,
|
|
260
|
+
hidden: hidden,
|
|
261
|
+
)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
sig { params(style_group: Symbol).returns(Zaxcel::Row) }
|
|
265
|
+
def add_empty_row!(style_group: :row_style)
|
|
266
|
+
add_row!(SecureRandom.uuid.delete('-'), style_group: style_group)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
sig do
|
|
270
|
+
params(
|
|
271
|
+
name: T.any(Symbol, String),
|
|
272
|
+
header: String,
|
|
273
|
+
header_style: Symbol,
|
|
274
|
+
row_style: Symbol,
|
|
275
|
+
first_row_style: Symbol,
|
|
276
|
+
total_style: T.nilable(Symbol),
|
|
277
|
+
width: T.nilable(T.any(Integer, Float, Zaxcel::Column::ComputedColumnWidth)),
|
|
278
|
+
alt_row_style: T.nilable(Symbol),
|
|
279
|
+
min_width: T.nilable(T.any(Integer, Float)),
|
|
280
|
+
).returns(Zaxcel::Column)
|
|
281
|
+
end
|
|
282
|
+
def add_column!(
|
|
283
|
+
name,
|
|
284
|
+
header: '',
|
|
285
|
+
header_style: :default_cell,
|
|
286
|
+
row_style: :default_cell,
|
|
287
|
+
first_row_style: :default_cell,
|
|
288
|
+
total_style: nil,
|
|
289
|
+
width: Zaxcel::Column::DEFAULT_COLUMN_WIDTH,
|
|
290
|
+
alt_row_style: nil,
|
|
291
|
+
min_width: 0
|
|
292
|
+
)
|
|
293
|
+
columns_by_name[name.to_sym] ||= Zaxcel::Column.new(
|
|
294
|
+
sheet: self,
|
|
295
|
+
name: name.to_sym,
|
|
296
|
+
header: header,
|
|
297
|
+
header_style: header_style,
|
|
298
|
+
row_style: row_style,
|
|
299
|
+
first_row_style: first_row_style,
|
|
300
|
+
total_style: total_style,
|
|
301
|
+
width: width,
|
|
302
|
+
alt_row_style: alt_row_style,
|
|
303
|
+
min_width: min_width,
|
|
304
|
+
)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
sig { params(width: T.nilable(T.any(Integer, Float))).returns(Zaxcel::Column) }
|
|
308
|
+
def add_empty_column!(width: Zaxcel::Column::DEFAULT_COLUMN_WIDTH)
|
|
309
|
+
add_column!(SecureRandom.uuid.delete('-'), width: width)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
sig do
|
|
313
|
+
params(
|
|
314
|
+
range: T.any(Zaxcel::References::Column, T::Array[Zaxcel::References::Cell]),
|
|
315
|
+
rule: Symbol,
|
|
316
|
+
style: Symbol,
|
|
317
|
+
).void
|
|
318
|
+
end
|
|
319
|
+
def add_conditional_formatting!(range:, rule:, style:)
|
|
320
|
+
conditional_formatting << { range: range, rule: rule, style: style }
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
sig { params(range: T::Array[Zaxcel::References::Cell]).void }
|
|
324
|
+
def merge_cells!(range)
|
|
325
|
+
ranges_to_merge << range
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
sig { returns(T::Array[T::Array[Zaxcel::References::Cell]]) }
|
|
329
|
+
def ranges_to_merge
|
|
330
|
+
@ranges_to_merge ||= T.let([], T.nilable(T::Array[T::Array[Zaxcel::References::Cell]]))
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
sig { returns(T::Array[Zaxcel::Column]) }
|
|
334
|
+
def columns
|
|
335
|
+
columns_by_name.values
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
sig { returns(T::Array[Zaxcel::Row]) }
|
|
339
|
+
def rows
|
|
340
|
+
rows_by_name.values
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
sig { params(height: T.nilable(Integer)).returns(Zaxcel::Row) }
|
|
344
|
+
def add_column_header_row!(height: nil)
|
|
345
|
+
add_row!(HEADER_ROW_KEY, style_group: :header_style, height: height)
|
|
346
|
+
.add_many!(columns.map { |c| [c.name, c.header] }.to_h)
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
sig { returns(T::Hash[Symbol, Zaxcel::Row]) }
|
|
350
|
+
def rows_by_name
|
|
351
|
+
@rows_by_name ||= T.let({}, T.nilable(T::Hash[Symbol, Zaxcel::Row]))
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
sig { returns(T::Hash[Symbol, Zaxcel::Column]) }
|
|
355
|
+
def columns_by_name
|
|
356
|
+
@columns_by_name ||= T.let({}, T.nilable(T::Hash[Symbol, Zaxcel::Column]))
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
sig { returns(T::Array[{ range: T.any(Zaxcel::References::Column, T::Array[Zaxcel::References::Cell]), rule: Symbol, style: Symbol }]) }
|
|
360
|
+
def conditional_formatting
|
|
361
|
+
@conditional_formatting ||= T.let(
|
|
362
|
+
[],
|
|
363
|
+
T.nilable(T::Array[{
|
|
364
|
+
range: T.any(Zaxcel::References::Column, T::Array[Zaxcel::References::Cell]),
|
|
365
|
+
rule: Symbol,
|
|
366
|
+
style: Symbol,
|
|
367
|
+
}]),
|
|
368
|
+
)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
sig { void }
|
|
372
|
+
def hide_gridlines!
|
|
373
|
+
@worksheet.sheet_view.show_grid_lines = false
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
sig { void }
|
|
377
|
+
def print_landscape!
|
|
378
|
+
@worksheet.print_options.horizontal_centered = true
|
|
379
|
+
@worksheet.page_setup.orientation = :landscape
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
sig do
|
|
383
|
+
params(
|
|
384
|
+
col_name: T.any(Symbol, String),
|
|
385
|
+
row_name: T.any(Symbol, String, Numeric),
|
|
386
|
+
sheet_name: T.nilable(String),
|
|
387
|
+
).returns(Zaxcel::References::Cell)
|
|
388
|
+
end
|
|
389
|
+
def cell_ref(col_name, row_name, sheet_name: nil)
|
|
390
|
+
row_name = row_name.to_s if row_name.is_a?(Numeric)
|
|
391
|
+
Zaxcel::References::Cell.new(
|
|
392
|
+
document: @document,
|
|
393
|
+
sheet_name: sheet_name || @name,
|
|
394
|
+
row_name: row_name.to_sym,
|
|
395
|
+
col_name: col_name.to_sym,
|
|
396
|
+
)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
sig { params(col_name: T.any(Symbol, String), sheet_name: T.nilable(String)).returns(Zaxcel::References::Column) }
|
|
400
|
+
def column_ref(col_name, sheet_name: nil)
|
|
401
|
+
Zaxcel::References::Column.new(document: @document, sheet_name: sheet_name || @name, col_name: col_name.to_sym)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
private
|
|
405
|
+
|
|
406
|
+
sig { params(cell: T.nilable(Zaxcel::Cell)).returns(T.nilable(T.any(Numeric, Money, String))) }
|
|
407
|
+
def format_cell_contents(cell)
|
|
408
|
+
value = cell&.value
|
|
409
|
+
base_format = Zaxcel::Cell.format(value, on_sheet: @name, quote_strings: false)
|
|
410
|
+
# always pass along numerics + money + nil as is for caxlsx to print directly
|
|
411
|
+
return base_format if !base_format.is_a?(String)
|
|
412
|
+
|
|
413
|
+
# prefix the evaluation operator if the result is a cell formula
|
|
414
|
+
if value.is_a?(Time) || value.is_a?(Zaxcel::CellFormula) || value.is_a?(Zaxcel::Reference) || value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
415
|
+
base_format = "=#{base_format}"
|
|
416
|
+
|
|
417
|
+
# array formulas need to be wrapped in curly braces for CAXLSX. See
|
|
418
|
+
# https://github.com/caxlsx/caxlsx/blob/master/lib/axlsx/workbook/worksheet/cell.rb#L422 and
|
|
419
|
+
# https://github.com/caxlsx/caxlsx/blob/master/lib/axlsx/workbook/worksheet/cell_serializer.rb#L101
|
|
420
|
+
base_format = "{#{base_format}}" if value.respond_to?(:array_formula?) && T.unsafe(value).array_formula?
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
base_format
|
|
424
|
+
end
|
|
425
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# typed: strict
|
|
3
|
+
|
|
4
|
+
require 'active_support/concern'
|
|
5
|
+
|
|
6
|
+
# Include this in a sorbet typed enum (T::Enum) so that it can be used easily with the `enumerize` gem:
|
|
7
|
+
# ```ruby
|
|
8
|
+
# class Category < T::Enum
|
|
9
|
+
# enums do
|
|
10
|
+
# One = new(:one)
|
|
11
|
+
# Two = new(:two)
|
|
12
|
+
# end
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# enumerize(
|
|
16
|
+
# :category,
|
|
17
|
+
# in: Category.enumerize_values,
|
|
18
|
+
# value_class: Category,
|
|
19
|
+
# )
|
|
20
|
+
# ```
|
|
21
|
+
class Sorbet
|
|
22
|
+
module EnumerizableEnum
|
|
23
|
+
extend ActiveSupport::Concern
|
|
24
|
+
extend T::Sig
|
|
25
|
+
extend T::Generic
|
|
26
|
+
|
|
27
|
+
# Enumerize requires that `value` on an instance of the `value_class` returns the enum's underlying value.
|
|
28
|
+
sig { returns(Symbol) }
|
|
29
|
+
def value = T.bind(self, T::Enum).serialize
|
|
30
|
+
|
|
31
|
+
class_methods do
|
|
32
|
+
extend T::Sig
|
|
33
|
+
|
|
34
|
+
# Monkey patch new to call `new` on `T::Enum` if a single value is passed in, otherwise call `deserialize` on the
|
|
35
|
+
# on the enum class. Enumerize calls `new` on the value class with two args; the second of which is the underlying
|
|
36
|
+
# value.
|
|
37
|
+
sig { params(args: T.untyped).returns(T.untyped) }
|
|
38
|
+
def new(*args)
|
|
39
|
+
return super if args.length == 1
|
|
40
|
+
|
|
41
|
+
T.bind(self, T.class_of(T::Enum)).deserialize(args[1].to_sym)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { returns(T::Array[Symbol]) }
|
|
45
|
+
def enumerize_values
|
|
46
|
+
T.bind(self, T.class_of(T::Enum)).values.map(&:serialize)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/zaxcel.rb
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
require 'caxlsx'
|
|
6
|
+
require 'active_support/core_ext/object/blank'
|
|
7
|
+
require 'active_support/core_ext/object/try'
|
|
8
|
+
require 'active_support/core_ext/string/filters'
|
|
9
|
+
require 'active_support/core_ext/string/access'
|
|
10
|
+
require 'active_support/core_ext/string/inflections'
|
|
11
|
+
require 'money'
|
|
12
|
+
|
|
13
|
+
require_relative 'zaxcel/version'
|
|
14
|
+
require_relative 'zaxcel/sorbet/enumerizable_enum'
|
|
15
|
+
require_relative 'enumerable'
|
|
16
|
+
|
|
17
|
+
# Core classes
|
|
18
|
+
require_relative 'zaxcel/arithmetic'
|
|
19
|
+
require_relative 'zaxcel/roundable'
|
|
20
|
+
require_relative 'zaxcel/reference'
|
|
21
|
+
require_relative 'zaxcel/cell_formula'
|
|
22
|
+
require_relative 'zaxcel/binary_expression'
|
|
23
|
+
require_relative 'zaxcel/function'
|
|
24
|
+
require_relative 'zaxcel/if_builder'
|
|
25
|
+
require_relative 'zaxcel/lang'
|
|
26
|
+
|
|
27
|
+
# Binary expressions
|
|
28
|
+
require_relative 'zaxcel/binary_expressions'
|
|
29
|
+
require_relative 'zaxcel/binary_expressions/addition'
|
|
30
|
+
require_relative 'zaxcel/binary_expressions/subtraction'
|
|
31
|
+
require_relative 'zaxcel/binary_expressions/multiplication'
|
|
32
|
+
require_relative 'zaxcel/binary_expressions/division'
|
|
33
|
+
|
|
34
|
+
# References
|
|
35
|
+
require_relative 'zaxcel/references'
|
|
36
|
+
require_relative 'zaxcel/references/cell'
|
|
37
|
+
require_relative 'zaxcel/references/column'
|
|
38
|
+
require_relative 'zaxcel/references/row'
|
|
39
|
+
require_relative 'zaxcel/references/range'
|
|
40
|
+
|
|
41
|
+
# Functions
|
|
42
|
+
require_relative 'zaxcel/functions'
|
|
43
|
+
require_relative 'zaxcel/functions/abs'
|
|
44
|
+
require_relative 'zaxcel/functions/and'
|
|
45
|
+
require_relative 'zaxcel/functions/average'
|
|
46
|
+
require_relative 'zaxcel/functions/choose'
|
|
47
|
+
require_relative 'zaxcel/functions/concatenate'
|
|
48
|
+
require_relative 'zaxcel/functions/if'
|
|
49
|
+
require_relative 'zaxcel/functions/if_error'
|
|
50
|
+
require_relative 'zaxcel/functions/index'
|
|
51
|
+
require_relative 'zaxcel/functions/len'
|
|
52
|
+
require_relative 'zaxcel/functions/match'
|
|
53
|
+
require_relative 'zaxcel/functions/match/match_type'
|
|
54
|
+
require_relative 'zaxcel/functions/max'
|
|
55
|
+
require_relative 'zaxcel/functions/min'
|
|
56
|
+
require_relative 'zaxcel/functions/negate'
|
|
57
|
+
require_relative 'zaxcel/functions/or'
|
|
58
|
+
require_relative 'zaxcel/functions/round'
|
|
59
|
+
require_relative 'zaxcel/functions/sum'
|
|
60
|
+
require_relative 'zaxcel/functions/sum_if'
|
|
61
|
+
require_relative 'zaxcel/functions/sum_ifs'
|
|
62
|
+
require_relative 'zaxcel/functions/sum_product'
|
|
63
|
+
require_relative 'zaxcel/functions/text'
|
|
64
|
+
require_relative 'zaxcel/functions/unique'
|
|
65
|
+
require_relative 'zaxcel/functions/x_lookup'
|
|
66
|
+
require_relative 'zaxcel/functions/xirr'
|
|
67
|
+
|
|
68
|
+
# Main classes
|
|
69
|
+
require_relative 'zaxcel/cell'
|
|
70
|
+
require_relative 'zaxcel/column'
|
|
71
|
+
require_relative 'zaxcel/row'
|
|
72
|
+
require_relative 'zaxcel/sheet'
|
|
73
|
+
require_relative 'zaxcel/document'
|
data/zaxcel.gemspec
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/zaxcel/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'zaxcel'
|
|
7
|
+
spec.version = Zaxcel::VERSION
|
|
8
|
+
spec.authors = ['AngelList Engineering']
|
|
9
|
+
spec.email = ['engineering@angellist.com']
|
|
10
|
+
|
|
11
|
+
spec.summary = 'A Ruby DSL for building Excel spreadsheets programmatically'
|
|
12
|
+
spec.description = <<~DESC
|
|
13
|
+
Zaxcel is a Ruby library built on top of caxlsx that adds an abstraction layer#{' '}
|
|
14
|
+
to building Excel documents. It provides simple methods for building formulas#{' '}
|
|
15
|
+
and references to other cells, even across many worksheets, using clean Ruby idioms#{' '}
|
|
16
|
+
without having to think about the underlying Excel implementation.
|
|
17
|
+
DESC
|
|
18
|
+
spec.homepage = 'https://github.com/angellist/zaxcel'
|
|
19
|
+
spec.license = 'MIT'
|
|
20
|
+
spec.required_ruby_version = '>= 3.0.0'
|
|
21
|
+
|
|
22
|
+
spec.metadata = {
|
|
23
|
+
'bug_tracker_uri' => 'https://github.com/angellist/zaxcel/issues',
|
|
24
|
+
'changelog_uri' => 'https://github.com/angellist/zaxcel/blob/main/CHANGELOG.md',
|
|
25
|
+
'documentation_uri' => 'https://github.com/angellist/zaxcel',
|
|
26
|
+
'homepage_uri' => 'https://github.com/angellist/zaxcel',
|
|
27
|
+
'source_code_uri' => 'https://github.com/angellist/zaxcel',
|
|
28
|
+
'rubygems_mfa_required' => 'true',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
spec.files = Dir.chdir(__dir__) do
|
|
32
|
+
%x(git ls-files -z).split("\x0").reject do |f|
|
|
33
|
+
(File.expand_path(f) == __FILE__) ||
|
|
34
|
+
f.start_with?(*%w[
|
|
35
|
+
bin/
|
|
36
|
+
test/
|
|
37
|
+
spec/
|
|
38
|
+
features/
|
|
39
|
+
.git
|
|
40
|
+
.circleci
|
|
41
|
+
.github/
|
|
42
|
+
appveyor
|
|
43
|
+
Gemfile
|
|
44
|
+
vendor/
|
|
45
|
+
vendor/bundle/
|
|
46
|
+
coverage/
|
|
47
|
+
tmp/
|
|
48
|
+
docs/
|
|
49
|
+
script/
|
|
50
|
+
sorbet/
|
|
51
|
+
examples/
|
|
52
|
+
])
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
spec.bindir = 'exe'
|
|
56
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
57
|
+
spec.require_paths = ['lib']
|
|
58
|
+
|
|
59
|
+
# Runtime dependencies
|
|
60
|
+
spec.add_dependency 'activesupport', '>= 6.0'
|
|
61
|
+
spec.add_dependency 'caxlsx', '~> 4.0'
|
|
62
|
+
spec.add_dependency 'money', '~> 6.0'
|
|
63
|
+
spec.add_dependency 'sorbet-runtime', '~> 0.5'
|
|
64
|
+
|
|
65
|
+
# Development dependencies
|
|
66
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
|
67
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
|
68
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
69
|
+
spec.add_development_dependency 'rubocop', '~> 1.72'
|
|
70
|
+
spec.add_development_dependency 'rubocop-rspec', '~> 3.5'
|
|
71
|
+
spec.add_development_dependency 'sorbet', '~> 0.5'
|
|
72
|
+
spec.add_development_dependency 'tapioca', '~> 0.11'
|
|
73
|
+
end
|