write_xlsx 1.12.2 → 1.13.0

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -0
  3. data/Changes +6 -0
  4. data/README.md +1 -1
  5. data/lib/write_xlsx/chartsheet.rb +4 -1
  6. data/lib/write_xlsx/object_positioning.rb +189 -0
  7. data/lib/write_xlsx/package/app.rb +2 -1
  8. data/lib/write_xlsx/page_setup.rb +190 -0
  9. data/lib/write_xlsx/sheets.rb +3 -3
  10. data/lib/write_xlsx/utility.rb +57 -9
  11. data/lib/write_xlsx/version.rb +1 -1
  12. data/lib/write_xlsx/workbook.rb +34 -34
  13. data/lib/write_xlsx/worksheet/asset_manager.rb +60 -0
  14. data/lib/write_xlsx/worksheet/autofilter.rb +388 -0
  15. data/lib/write_xlsx/worksheet/cell_data.rb +8 -5
  16. data/lib/write_xlsx/worksheet/cell_data_manager.rb +47 -0
  17. data/lib/write_xlsx/worksheet/cell_data_store.rb +61 -0
  18. data/lib/write_xlsx/worksheet/columns.rb +199 -0
  19. data/lib/write_xlsx/worksheet/comments_support.rb +61 -0
  20. data/lib/write_xlsx/worksheet/conditional_formats.rb +30 -0
  21. data/lib/write_xlsx/worksheet/data_writing.rb +990 -0
  22. data/lib/write_xlsx/worksheet/drawing_methods.rb +308 -0
  23. data/lib/write_xlsx/worksheet/drawing_preparation.rb +290 -0
  24. data/lib/write_xlsx/worksheet/drawing_relations.rb +76 -0
  25. data/lib/write_xlsx/worksheet/drawing_xml_writer.rb +50 -0
  26. data/lib/write_xlsx/worksheet/formatting.rb +416 -0
  27. data/lib/write_xlsx/worksheet/initialization.rb +146 -0
  28. data/lib/write_xlsx/worksheet/panes.rb +64 -0
  29. data/lib/write_xlsx/worksheet/print_options.rb +72 -0
  30. data/lib/write_xlsx/worksheet/protection.rb +65 -0
  31. data/lib/write_xlsx/worksheet/rich_text_helpers.rb +78 -0
  32. data/lib/write_xlsx/worksheet/row_col_sizing.rb +67 -0
  33. data/lib/write_xlsx/worksheet/rows.rb +84 -0
  34. data/lib/write_xlsx/worksheet/selection.rb +41 -0
  35. data/lib/write_xlsx/worksheet/xml_writer.rb +1241 -0
  36. data/lib/write_xlsx/worksheet.rb +359 -4530
  37. data/lib/write_xlsx/zip_file_utils.rb +1 -1
  38. data/write_xlsx.gemspec +1 -1
  39. metadata +34 -5
  40. data/lib/write_xlsx/worksheet/page_setup.rb +0 -192
@@ -0,0 +1,990 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ ###############################################################################
5
+ #
6
+ # DataWriting - A module for writing data to worksheet cells.
7
+ #
8
+ # Used in conjunction with WriteXLSX
9
+ #
10
+ # Copyright 2000-2011, John McNamara, jmcnamara@cpan.org
11
+ # Convert to ruby by Hideo NAKAMURA, nakamura.hideo@gmail.com
12
+ #
13
+
14
+ module Writexlsx
15
+ class Worksheet
16
+ module DataWriting
17
+ include Writexlsx::Utility
18
+
19
+ #
20
+ # :call-seq:
21
+ # write(row, column [ , token [ , format ] ])
22
+ #
23
+ # Excel makes a distinction between data types such as strings, numbers,
24
+ # blanks, formulas and hyperlinks. To simplify the process of writing
25
+ # data the {#write()}[#method-i-write] method acts as a general alias for several more
26
+ # specific methods:
27
+ #
28
+ def write(row, col, token = nil, format = nil, value1 = nil, value2 = nil)
29
+ # Check for a cell reference in A1 notation and substitute row and column
30
+ if (row_col_array = row_col_notation(row))
31
+ _row, _col = row_col_array
32
+ _token = col
33
+ _format = token
34
+ _value1 = format
35
+ _value2 = value1
36
+ else
37
+ _row = row
38
+ _col = col
39
+ _token = token
40
+ _format = format
41
+ _value1 = value1
42
+ _value2 = value2
43
+ end
44
+ _token ||= ''
45
+ _token = _token.to_s if token.instance_of?(Time) || token.instance_of?(Date)
46
+
47
+ if _format.respond_to?(:force_text_format?) && _format.force_text_format?
48
+ write_string(_row, _col, _token, _format) # Force text format
49
+ # Match an array ref.
50
+ elsif _token.respond_to?(:to_ary)
51
+ write_row(_row, _col, _token, _format, _value1, _value2)
52
+ elsif _token.respond_to?(:coerce) # Numeric
53
+ write_number(_row, _col, _token, _format)
54
+ elsif _token.respond_to?(:=~) # String
55
+ # Match integer with leading zero(s)
56
+ if @leading_zeros && _token =~ /^0\d*$/
57
+ write_string(_row, _col, _token, _format)
58
+ elsif _token =~ /\A([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?\Z/
59
+ write_number(_row, _col, _token, _format)
60
+ # Match formula
61
+ elsif _token =~ /^=/
62
+ write_formula(_row, _col, _token, _format, _value1)
63
+ # Match array formula
64
+ elsif _token =~ /^\{=.*\}$/
65
+ write_formula(_row, _col, _token, _format, _value1)
66
+ # Match blank
67
+ elsif _token == ''
68
+ # row_col_args.delete_at(2) # remove the empty string from the parameter list
69
+ write_blank(_row, _col, _format)
70
+ elsif @workbook.strings_to_urls
71
+ # https://, http://, ftp://, mailto:, internal:, external:
72
+ url_token_re = %r{\A(?:(?:https?|ftp)://|mailto:|(?:in|ex)ternal:)}
73
+
74
+ if _token =~ url_token_re
75
+ write_url(_row, _col, _token, _format, _value1, _value2)
76
+ else
77
+ write_string(_row, _col, _token, _format)
78
+ end
79
+ else
80
+ write_string(_row, _col, _token, _format)
81
+ end
82
+ else
83
+ write_string(_row, _col, _token, _format)
84
+ end
85
+ end
86
+
87
+ #
88
+ # :call-seq:
89
+ # write_row(row, col, array [ , format ])
90
+ #
91
+ # Write a row of data starting from (row, col). Call write_col() if any of
92
+ # the elements of the array are in turn array. This allows the writing
93
+ # of 1D or 2D arrays of data in one go.
94
+ #
95
+ def write_row(row, col, tokens = nil, *options)
96
+ # Check for a cell reference in A1 notation and substitute row and column
97
+ if (row_col_array = row_col_notation(row))
98
+ _row, _col = row_col_array
99
+ _tokens = col
100
+ _options = [tokens] + options
101
+ else
102
+ _row = row
103
+ _col = col
104
+ _tokens = tokens
105
+ _options = options
106
+ end
107
+ raise "Not an array ref in call to write_row()$!" unless _tokens.respond_to?(:to_ary)
108
+
109
+ _tokens.each do |_token|
110
+ # Check for nested arrays
111
+ if _token.respond_to?(:to_ary)
112
+ write_col(_row, _col, _token, *_options)
113
+ else
114
+ write(_row, _col, _token, *_options)
115
+ end
116
+ _col += 1
117
+ end
118
+ end
119
+
120
+ #
121
+ # :call-seq:
122
+ # write_col(row, col, array [ , format ])
123
+ #
124
+ # Write a column of data starting from (row, col). Call write_row() if any of
125
+ # the elements of the array are in turn array. This allows the writing
126
+ # of 1D or 2D arrays of data in one go.
127
+ #
128
+ def write_col(row, col, tokens = nil, *options)
129
+ if (row_col_array = row_col_notation(row))
130
+ _row, _col = row_col_array
131
+ _tokens = col
132
+ _options = [tokens] + options if options
133
+ else
134
+ _row = row
135
+ _col = col
136
+ _tokens = tokens
137
+ _options = options
138
+ end
139
+
140
+ _tokens.each do |_token|
141
+ # write() will deal with any nested arrays
142
+ write(_row, _col, _token, *_options)
143
+ _row += 1
144
+ end
145
+ end
146
+
147
+ #
148
+ # :call-seq:
149
+ # write_comment(row, column, string, options = {})
150
+ #
151
+ # Write a comment to the specified row and column (zero indexed).
152
+ #
153
+ def write_comment(row, col, string = nil, options = nil)
154
+ # Check for a cell reference in A1 notation and substitute row and column
155
+ if (row_col_array = row_col_notation(row))
156
+ _row, _col = row_col_array
157
+ _string = col
158
+ _options = string
159
+ else
160
+ _row = row
161
+ _col = col
162
+ _string = string
163
+ _options = options
164
+ end
165
+ raise WriteXLSXInsufficientArgumentError if [_row, _col, _string].include?(nil)
166
+
167
+ # Check that row and col are valid and store max and min values
168
+ check_dimensions(_row, _col)
169
+ store_row_col_max_min_values(_row, _col)
170
+
171
+ @has_vml = true
172
+
173
+ # Process the properties of the cell comment.
174
+ @comments.add(@workbook, self, _row, _col, _string, _options)
175
+ end
176
+
177
+ #
178
+ # :call-seq:
179
+ # write_number(row, column, number [ , format ])
180
+ #
181
+ # Write an integer or a float to the cell specified by row and column:
182
+ #
183
+ def write_number(row, col, number, format = nil)
184
+ # Check for a cell reference in A1 notation and substitute row and column
185
+ if (row_col_array = row_col_notation(row))
186
+ _row, _col = row_col_array
187
+ _number = col
188
+ _format = number
189
+ else
190
+ _row = row
191
+ _col = col
192
+ _number = number
193
+ _format = format
194
+ end
195
+ raise WriteXLSXInsufficientArgumentError if _row.nil? || _col.nil? || _number.nil?
196
+
197
+ # Check that row and col are valid and store max and min values
198
+ check_dimensions(_row, _col)
199
+ store_row_col_max_min_values(_row, _col)
200
+
201
+ store_data_to_table(NumberCellData.new(_number, _format), _row, _col)
202
+ end
203
+
204
+ #
205
+ # :call-seq:
206
+ # write_string(row, column, string [, format ])
207
+ #
208
+ # Write a string to the specified row and column (zero indexed).
209
+ # +format+ is optional.
210
+ #
211
+ def write_string(row, col, string = nil, format = nil)
212
+ # Check for a cell reference in A1 notation and substitute row and column
213
+ if (row_col_array = row_col_notation(row))
214
+ _row, _col = row_col_array
215
+ _string = col
216
+ _format = string
217
+ else
218
+ _row = row
219
+ _col = col
220
+ _string = string
221
+ _format = format
222
+ end
223
+ _string &&= _string.to_s
224
+ raise WriteXLSXInsufficientArgumentError if _row.nil? || _col.nil? || _string.nil?
225
+
226
+ # Check that row and col are valid and store max and min values
227
+ check_dimensions(_row, _col)
228
+ store_row_col_max_min_values(_row, _col)
229
+
230
+ index = shared_string_index(_string.length > STR_MAX ? _string[0, STR_MAX] : _string)
231
+
232
+ store_data_to_table(StringCellData.new(index, _format, _string), _row, _col)
233
+ end
234
+
235
+ #
236
+ # :call-seq:
237
+ # write_rich_string(row, column, (string | format, string)+, [,cell_format])
238
+ #
239
+ # The write_rich_string() method is used to write strings with multiple formats.
240
+ # The method receives string fragments prefixed by format objects. The final
241
+ # format object is used as the cell format.
242
+ #
243
+ def write_rich_string(row, col, *rich_strings)
244
+ # Check for a cell reference in A1 notation and substitute row and column
245
+ if (row_col_array = row_col_notation(row))
246
+ _row, _col = row_col_array
247
+ _rich_strings = [col] + rich_strings
248
+ else
249
+ _row = row
250
+ _col = col
251
+ _rich_strings = rich_strings
252
+ end
253
+ raise WriteXLSXInsufficientArgumentError if [_row, _col, _rich_strings[0]].include?(nil)
254
+
255
+ _xf = cell_format_of_rich_string(_rich_strings)
256
+
257
+ # Check that row and col are valid and store max and min values
258
+ check_dimensions(_row, _col)
259
+ store_row_col_max_min_values(_row, _col)
260
+
261
+ _fragments, _raw_string = rich_strings_fragments(_rich_strings)
262
+ # can't allow 2 formats in a row
263
+ return -4 unless _fragments
264
+
265
+ # Check that the string si < 32767 chars.
266
+ return 3 if _raw_string.size > @xls_strmax
267
+
268
+ index = shared_string_index(xml_str_of_rich_string(_fragments))
269
+
270
+ store_data_to_table(RichStringCellData.new(index, _xf, _raw_string), _row, _col)
271
+ end
272
+
273
+ #
274
+ # :call-seq:
275
+ # write_blank(row, col, format)
276
+ #
277
+ # Write a blank cell to the specified row and column (zero indexed).
278
+ # A blank cell is used to specify formatting without adding a string
279
+ # or a number.
280
+ #
281
+ def write_blank(row, col, format = nil)
282
+ # Check for a cell reference in A1 notation and substitute row and column
283
+ if (row_col_array = row_col_notation(row))
284
+ _row, _col = row_col_array
285
+ _format = col
286
+ else
287
+ _row = row
288
+ _col = col
289
+ _format = format
290
+ end
291
+ raise WriteXLSXInsufficientArgumentError if [_row, _col].include?(nil)
292
+
293
+ # Don't write a blank cell unless it has a format
294
+ return unless _format
295
+
296
+ # Check that row and col are valid and store max and min values
297
+ check_dimensions(_row, _col)
298
+ store_row_col_max_min_values(_row, _col)
299
+
300
+ store_data_to_table(BlankCellData.new(_format), _row, _col)
301
+ end
302
+
303
+ #
304
+ # Utility method to strip equal sign and array braces from a formula
305
+ # and also expand out future and dynamic array formulas.
306
+ #
307
+ def prepare_formula(given_formula, expand_future_functions = nil)
308
+ # Ignore empty/null formulas.
309
+ return given_formula unless ptrue?(given_formula)
310
+
311
+ # Remove array formula braces and the leading =.
312
+ formula = given_formula.sub(/^\{(.*)\}$/, '\1').sub(/^=/, '')
313
+
314
+ # # Don't expand formulas that the user has already expanded.
315
+ return formula if formula =~ /_xlfn\./
316
+
317
+ # Expand dynamic array formulas.
318
+ formula = expand_formula(formula, 'ANCHORARRAY\(')
319
+ formula = expand_formula(formula, 'BYCOL\(')
320
+ formula = expand_formula(formula, 'BYROW\(')
321
+ formula = expand_formula(formula, 'CHOOSECOLS\(')
322
+ formula = expand_formula(formula, 'CHOOSEROWS\(')
323
+ formula = expand_formula(formula, 'DROP\(')
324
+ formula = expand_formula(formula, 'EXPAND\(')
325
+ formula = expand_formula(formula, 'FILTER\(', '._xlws')
326
+ formula = expand_formula(formula, 'HSTACK\(')
327
+ formula = expand_formula(formula, 'LAMBDA\(')
328
+ formula = expand_formula(formula, 'MAKEARRAY\(')
329
+ formula = expand_formula(formula, 'MAP\(')
330
+ formula = expand_formula(formula, 'RANDARRAY\(')
331
+ formula = expand_formula(formula, 'REDUCE\(')
332
+ formula = expand_formula(formula, 'SCAN\(')
333
+ formula = expand_formula(formula, 'SEQUENCE\(')
334
+ formula = expand_formula(formula, 'SINGLE\(')
335
+ formula = expand_formula(formula, 'SORT\(', '._xlws')
336
+ formula = expand_formula(formula, 'SORTBY\(')
337
+ formula = expand_formula(formula, 'SWITCH\(')
338
+ formula = expand_formula(formula, 'TAKE\(')
339
+ formula = expand_formula(formula, 'TEXTSPLIT\(')
340
+ formula = expand_formula(formula, 'TOCOL\(')
341
+ formula = expand_formula(formula, 'TOROW\(')
342
+ formula = expand_formula(formula, 'UNIQUE\(')
343
+ formula = expand_formula(formula, 'VSTACK\(')
344
+ formula = expand_formula(formula, 'WRAPCOLS\(')
345
+ formula = expand_formula(formula, 'WRAPROWS\(')
346
+ formula = expand_formula(formula, 'XLOOKUP\(')
347
+
348
+ if !@use_future_functions && !ptrue?(expand_future_functions)
349
+ return formula
350
+ end
351
+
352
+ # Future functions.
353
+ formula = expand_formula(formula, 'ACOTH\(')
354
+ formula = expand_formula(formula, 'ACOT\(')
355
+ formula = expand_formula(formula, 'AGGREGATE\(')
356
+ formula = expand_formula(formula, 'ARABIC\(')
357
+ formula = expand_formula(formula, 'ARRAYTOTEXT\(')
358
+ formula = expand_formula(formula, 'BASE\(')
359
+ formula = expand_formula(formula, 'BETA.DIST\(')
360
+ formula = expand_formula(formula, 'BETA.INV\(')
361
+ formula = expand_formula(formula, 'BINOM.DIST.RANGE\(')
362
+ formula = expand_formula(formula, 'BINOM.DIST\(')
363
+ formula = expand_formula(formula, 'BINOM.INV\(')
364
+ formula = expand_formula(formula, 'BITAND\(')
365
+ formula = expand_formula(formula, 'BITLSHIFT\(')
366
+ formula = expand_formula(formula, 'BITOR\(')
367
+ formula = expand_formula(formula, 'BITRSHIFT\(')
368
+ formula = expand_formula(formula, 'BITXOR\(')
369
+ formula = expand_formula(formula, 'CEILING.MATH\(')
370
+ formula = expand_formula(formula, 'CEILING.PRECISE\(')
371
+ formula = expand_formula(formula, 'CHISQ.DIST.RT\(')
372
+ formula = expand_formula(formula, 'CHISQ.DIST\(')
373
+ formula = expand_formula(formula, 'CHISQ.INV.RT\(')
374
+ formula = expand_formula(formula, 'CHISQ.INV\(')
375
+ formula = expand_formula(formula, 'CHISQ.TEST\(')
376
+ formula = expand_formula(formula, 'COMBINA\(')
377
+ formula = expand_formula(formula, 'CONCAT\(')
378
+ formula = expand_formula(formula, 'CONFIDENCE.NORM\(')
379
+ formula = expand_formula(formula, 'CONFIDENCE.T\(')
380
+ formula = expand_formula(formula, 'COTH\(')
381
+ formula = expand_formula(formula, 'COT\(')
382
+ formula = expand_formula(formula, 'COVARIANCE.P\(')
383
+ formula = expand_formula(formula, 'COVARIANCE.S\(')
384
+ formula = expand_formula(formula, 'CSCH\(')
385
+ formula = expand_formula(formula, 'CSC\(')
386
+ formula = expand_formula(formula, 'DAYS\(')
387
+ formula = expand_formula(formula, 'DECIMAL\(')
388
+ formula = expand_formula(formula, 'ERF.PRECISE\(')
389
+ formula = expand_formula(formula, 'ERFC.PRECISE\(')
390
+ formula = expand_formula(formula, 'EXPON.DIST\(')
391
+ formula = expand_formula(formula, 'F.DIST.RT\(')
392
+ formula = expand_formula(formula, 'F.DIST\(')
393
+ formula = expand_formula(formula, 'F.INV.RT\(')
394
+ formula = expand_formula(formula, 'F.INV\(')
395
+ formula = expand_formula(formula, 'F.TEST\(')
396
+ formula = expand_formula(formula, 'FILTERXML\(')
397
+ formula = expand_formula(formula, 'FLOOR.MATH\(')
398
+ formula = expand_formula(formula, 'FLOOR.PRECISE\(')
399
+ formula = expand_formula(formula, 'FORECAST.ETS.CONFINT\(')
400
+ formula = expand_formula(formula, 'FORECAST.ETS.SEASONALITY\(')
401
+ formula = expand_formula(formula, 'FORECAST.ETS.STAT\(')
402
+ formula = expand_formula(formula, 'FORECAST.ETS\(')
403
+ formula = expand_formula(formula, 'FORECAST.LINEAR\(')
404
+ formula = expand_formula(formula, 'FORMULATEXT\(')
405
+ formula = expand_formula(formula, 'GAMMA.DIST\(')
406
+ formula = expand_formula(formula, 'GAMMA.INV\(')
407
+ formula = expand_formula(formula, 'GAMMALN.PRECISE\(')
408
+ formula = expand_formula(formula, 'GAMMA\(')
409
+ formula = expand_formula(formula, 'GAUSS\(')
410
+ formula = expand_formula(formula, 'HYPGEOM.DIST\(')
411
+ formula = expand_formula(formula, 'IFNA\(')
412
+ formula = expand_formula(formula, 'IFS\(')
413
+ formula = expand_formula(formula, 'IMAGE\(')
414
+ formula = expand_formula(formula, 'IMCOSH\(')
415
+ formula = expand_formula(formula, 'IMCOT\(')
416
+ formula = expand_formula(formula, 'IMCSCH\(')
417
+ formula = expand_formula(formula, 'IMCSC\(')
418
+ formula = expand_formula(formula, 'IMSECH\(')
419
+ formula = expand_formula(formula, 'IMSEC\(')
420
+ formula = expand_formula(formula, 'IMSINH\(')
421
+ formula = expand_formula(formula, 'IMTAN\(')
422
+ formula = expand_formula(formula, 'ISFORMULA\(')
423
+ formula = expand_formula(formula, 'ISOMITTED\(')
424
+ formula = expand_formula(formula, 'ISOWEEKNUM\(')
425
+ formula = expand_formula(formula, 'LET\(')
426
+ formula = expand_formula(formula, 'LOGNORM.DIST\(')
427
+ formula = expand_formula(formula, 'LOGNORM.INV\(')
428
+ formula = expand_formula(formula, 'MAXIFS\(')
429
+ formula = expand_formula(formula, 'MINIFS\(')
430
+ formula = expand_formula(formula, 'MODE.MULT\(')
431
+ formula = expand_formula(formula, 'MODE.SNGL\(')
432
+ formula = expand_formula(formula, 'MUNIT\(')
433
+ formula = expand_formula(formula, 'NEGBINOM.DIST\(')
434
+ formula = expand_formula(formula, 'NORM.DIST\(')
435
+ formula = expand_formula(formula, 'NORM.INV\(')
436
+ formula = expand_formula(formula, 'NORM.S.DIST\(')
437
+ formula = expand_formula(formula, 'NORM.S.INV\(')
438
+ formula = expand_formula(formula, 'NUMBERVALUE\(')
439
+ formula = expand_formula(formula, 'PDURATION\(')
440
+ formula = expand_formula(formula, 'PERCENTILE.EXC\(')
441
+ formula = expand_formula(formula, 'PERCENTILE.INC\(')
442
+ formula = expand_formula(formula, 'PERCENTRANK.EXC\(')
443
+ formula = expand_formula(formula, 'PERCENTRANK.INC\(')
444
+ formula = expand_formula(formula, 'PERMUTATIONA\(')
445
+ formula = expand_formula(formula, 'PHI\(')
446
+ formula = expand_formula(formula, 'POISSON.DIST\(')
447
+ formula = expand_formula(formula, 'QUARTILE.EXC\(')
448
+ formula = expand_formula(formula, 'QUARTILE.INC\(')
449
+ formula = expand_formula(formula, 'QUERYSTRING\(')
450
+ formula = expand_formula(formula, 'RANK.AVG\(')
451
+ formula = expand_formula(formula, 'RANK.EQ\(')
452
+ formula = expand_formula(formula, 'RRI\(')
453
+ formula = expand_formula(formula, 'SECH\(')
454
+ formula = expand_formula(formula, 'SEC\(')
455
+ formula = expand_formula(formula, 'SHEETS\(')
456
+ formula = expand_formula(formula, 'SHEET\(')
457
+ formula = expand_formula(formula, 'SKEW.P\(')
458
+ formula = expand_formula(formula, 'STDEV.P\(')
459
+ formula = expand_formula(formula, 'STDEV.S\(')
460
+ formula = expand_formula(formula, 'T.DIST.2T\(')
461
+ formula = expand_formula(formula, 'T.DIST.RT\(')
462
+ formula = expand_formula(formula, 'T.DIST\(')
463
+ formula = expand_formula(formula, 'T.INV.2T\(')
464
+ formula = expand_formula(formula, 'T.INV\(')
465
+ formula = expand_formula(formula, 'T.TEST\(')
466
+ formula = expand_formula(formula, 'TEXTAFTER\(')
467
+ formula = expand_formula(formula, 'TEXTBEFORE\(')
468
+ formula = expand_formula(formula, 'TEXTJOIN\(')
469
+ formula = expand_formula(formula, 'UNICHAR\(')
470
+ formula = expand_formula(formula, 'UNICODE\(')
471
+ formula = expand_formula(formula, 'VALUETOTEXT\(')
472
+ formula = expand_formula(formula, 'VAR.P\(')
473
+ formula = expand_formula(formula, 'VAR.S\(')
474
+ formula = expand_formula(formula, 'WEBSERVICE\(')
475
+ formula = expand_formula(formula, 'WEIBULL.DIST\(')
476
+ formula = expand_formula(formula, 'XMATCH\(')
477
+ formula = expand_formula(formula, 'XOR\(')
478
+ expand_formula(formula, 'Z.TEST\(')
479
+ end
480
+
481
+ #
482
+ # :call-seq:
483
+ # write_formula(row, column, formula [ , format [ , value ] ])
484
+ #
485
+ # Write a formula or function to the cell specified by +row+ and +column+:
486
+ #
487
+ def write_formula(row, col, formula = nil, format = nil, value = nil)
488
+ # Check for a cell reference in A1 notation and substitute row and column
489
+ if (row_col_array = row_col_notation(row))
490
+ _row, _col = row_col_array
491
+ _formula = col
492
+ _format = formula
493
+ _value = format
494
+ else
495
+ _row = row
496
+ _col = col
497
+ _formula = formula
498
+ _format = format
499
+ _value = value
500
+ end
501
+ raise WriteXLSXInsufficientArgumentError if [_row, _col, _formula].include?(nil)
502
+
503
+ # Check for dynamic array functions.
504
+ regex = /\bANCHORARRAY\(|\bBYCOL\(|\bBYROW\(|\bCHOOSECOLS\(|\bCHOOSEROWS\(|\bDROP\(|\bEXPAND\(|\bFILTER\(|\bHSTACK\(|\bLAMBDA\(|\bMAKEARRAY\(|\bMAP\(|\bRANDARRAY\(|\bREDUCE\(|\bSCAN\(|\bSEQUENCE\(|\bSINGLE\(|\bSORT\(|\bSORTBY\(|\bSWITCH\(|\bTAKE\(|\bTEXTSPLIT\(|\bTOCOL\(|\bTOROW\(|\bUNIQUE\(|\bVSTACK\(|\bWRAPCOLS\(|\bWRAPROWS\(|\bXLOOKUP\(/
505
+ if _formula =~ regex
506
+ return write_dynamic_array_formula(
507
+ _row, _col, _row, _col, _formula, _format, _value
508
+ )
509
+ end
510
+
511
+ # Hand off array formulas.
512
+ if _formula =~ /^\{=.*\}$/
513
+ write_array_formula(_row, _col, _row, _col, _formula, _format, _value)
514
+ else
515
+ check_dimensions(_row, _col)
516
+ store_row_col_max_min_values(_row, _col)
517
+ _formula = prepare_formula(_formula)
518
+
519
+ store_data_to_table(FormulaCellData.new(_formula, _format, _value), _row, _col)
520
+ end
521
+ end
522
+
523
+ #
524
+ # Internal method shared by the write_array_formula() and
525
+ # write_dynamic_array_formula() methods.
526
+ #
527
+ def write_array_formula_base(type, *args)
528
+ # Check for a cell reference in A1 notation and substitute row and column
529
+ # Convert single cell to range
530
+ if args.first.to_s =~ /^([A-Za-z]+[0-9]+)$/
531
+ range = "#{::Regexp.last_match(1)}:#{::Regexp.last_match(1)}"
532
+ params = [range] + args[1..-1]
533
+ else
534
+ params = args
535
+ end
536
+
537
+ if (row_col_array = row_col_notation(params.first))
538
+ row1, col1, row2, col2 = row_col_array
539
+ formula, xf, value = params[1..-1]
540
+ else
541
+ row1, col1, row2, col2, formula, xf, value = params
542
+ end
543
+ raise WriteXLSXInsufficientArgumentError if [row1, col1, row2, col2, formula].include?(nil)
544
+
545
+ # Swap last row/col with first row/col as necessary
546
+ row1, row2 = row2, row1 if row1 > row2
547
+ col1, col2 = col2, col1 if col1 > col2
548
+
549
+ # Check that row and col are valid and store max and min values
550
+ check_dimensions(row1, col1)
551
+ check_dimensions(row2, col2)
552
+ store_row_col_max_min_values(row1, col1)
553
+ store_row_col_max_min_values(row2, col2)
554
+
555
+ # Define array range
556
+ range = if row1 == row2 && col1 == col2
557
+ xl_rowcol_to_cell(row1, col1)
558
+ else
559
+ "#{xl_rowcol_to_cell(row1, col1)}:#{xl_rowcol_to_cell(row2, col2)}"
560
+ end
561
+
562
+ # Modify the formula string, as needed.
563
+ formula = prepare_formula(formula, 1)
564
+
565
+ store_data_to_table(
566
+ if type == 'a'
567
+ FormulaArrayCellData.new(formula, xf, range, value)
568
+ elsif type == 'd'
569
+ DynamicFormulaArrayCellData.new(formula, xf, range, value)
570
+ else
571
+ raise "invalid type in write_array_formula_base()."
572
+ end,
573
+ row1, col1
574
+ )
575
+
576
+ # Pad out the rest of the area with formatted zeroes.
577
+ (row1..row2).each do |row|
578
+ (col1..col2).each do |col|
579
+ next if row == row1 && col == col1
580
+
581
+ write_number(row, col, 0, xf)
582
+ end
583
+ end
584
+ end
585
+
586
+ #
587
+ # write_array_formula(row1, col1, row2, col2, formula, format)
588
+ #
589
+ # Write an array formula to the specified row and column (zero indexed).
590
+ #
591
+ def write_array_formula(row1, col1, row2 = nil, col2 = nil, formula = nil, format = nil, value = nil)
592
+ write_array_formula_base('a', row1, col1, row2, col2, formula, format, value)
593
+ end
594
+
595
+ #
596
+ # write_dynamic_array_formula(row1, col1, row2, col2, formula, format)
597
+ #
598
+ # Write a dynamic formula to the specified row and column (zero indexed).
599
+ #
600
+ def write_dynamic_array_formula(row1, col1, row2 = nil, col2 = nil, formula = nil, format = nil, value = nil)
601
+ write_array_formula_base('d', row1, col1, row2, col2, formula, format, value)
602
+ @has_dynamic_functions = true
603
+ end
604
+
605
+ #
606
+ # write_boolean(row, col, val, format)
607
+ #
608
+ # Write a boolean value to the specified row and column (zero indexed).
609
+ #
610
+ def write_boolean(row, col, val = nil, format = nil)
611
+ if (row_col_array = row_col_notation(row))
612
+ _row, _col = row_col_array
613
+ _val = col
614
+ _format = val
615
+ else
616
+ _row = row
617
+ _col = col
618
+ _val = val
619
+ _format = format
620
+ end
621
+ raise WriteXLSXInsufficientArgumentError if _row.nil? || _col.nil?
622
+
623
+ _val = _val ? 1 : 0 # Boolean value.
624
+ # xf : cell format.
625
+
626
+ # Check that row and col are valid and store max and min values
627
+ check_dimensions(_row, _col)
628
+ store_row_col_max_min_values(_row, _col)
629
+
630
+ store_data_to_table(BooleanCellData.new(_val, _format), _row, _col)
631
+ end
632
+
633
+ #
634
+ # :call-seq:
635
+ # update_format_with_params(row, col, format_params)
636
+ #
637
+ # Update formatting of the cell to the specified row and column (zero indexed).
638
+ #
639
+ def update_format_with_params(row, col, params = nil)
640
+ if (row_col_array = row_col_notation(row))
641
+ _row, _col = row_col_array
642
+ _params = args[1]
643
+ else
644
+ _row = row
645
+ _col = col
646
+ _params = params
647
+ end
648
+ raise WriteXLSXInsufficientArgumentError if _row.nil? || _col.nil? || _params.nil?
649
+
650
+ # Check that row and col are valid and store max and min values
651
+ check_dimensions(_row, _col)
652
+ store_row_col_max_min_values(_row, _col)
653
+
654
+ format = nil
655
+ cell_data = nil
656
+ if @cell_data_store[_row].nil? || @cell_data_store[_row][_col].nil?
657
+ format = @workbook.add_format(_params)
658
+ write_blank(_row, _col, format)
659
+ else
660
+ if @cell_data_store[_row][_col].xf.nil?
661
+ format = @workbook.add_format(_params)
662
+ cell_data = @cell_data_store[_row][_col]
663
+ else
664
+ format = @workbook.add_format
665
+ cell_data = @cell_data_store[_row][_col]
666
+ format.copy(cell_data.xf)
667
+ format.set_format_properties(_params)
668
+ end
669
+ value = case cell_data
670
+ when FormulaCellData
671
+ "=#{cell_data.token}"
672
+ when FormulaArrayCellData
673
+ "{=#{cell_data.token}}"
674
+ when StringCellData
675
+ @workbook.shared_strings.string(cell_data.data[:sst_id])
676
+ else
677
+ cell_data.data
678
+ end
679
+ write(_row, _col, value, format)
680
+ end
681
+ end
682
+
683
+ #
684
+ # :call-seq:
685
+ # write_url(row, column, url [ , format, label, tip ])
686
+ #
687
+ # Write a hyperlink to a URL in the cell specified by +row+ and +column+.
688
+ # The hyperlink is comprised of two elements: the visible label and
689
+ # the invisible link. The visible label is the same as the link unless
690
+ # an alternative label is specified. The label parameter is optional.
691
+ # The label is written using the {#write()}[#method-i-write] method. Therefore it is
692
+ # possible to write strings, numbers or formulas as labels.
693
+ #
694
+ def write_url(row, col, url = nil, format = nil, str = nil, tip = nil, ignore_write_string = false)
695
+ # Check for a cell reference in A1 notation and substitute row and column
696
+ if (row_col_array = row_col_notation(row))
697
+ _row, _col = row_col_array
698
+ _url = col
699
+ _format = url
700
+ _str = format
701
+ _tip = str
702
+ _ignore_write_string = tip
703
+ else
704
+ _row = row
705
+ _col = col
706
+ _url = url
707
+ _format = format
708
+ _str = str
709
+ _tip = tip
710
+ _ignore_write_string = ignore_write_string
711
+ end
712
+
713
+ _format, _str = _str, _format if _str.respond_to?(:xf_index) || (_format && !_format.respond_to?(:xf_index))
714
+ raise WriteXLSXInsufficientArgumentError if [_row, _col, _url].include?(nil)
715
+
716
+ # Check that row and col are valid and store max and min values
717
+ check_dimensions(_row, _col)
718
+ store_row_col_max_min_values(_row, _col)
719
+
720
+ hyperlink = Hyperlink.factory(_url, _str, _tip, @max_url_length)
721
+ store_hyperlink(_row, _col, hyperlink)
722
+
723
+ raise "URL '#{url}' added but URL exceeds Excel's limit of 65,530 URLs per worksheet." if hyperlinks_count > 65_530
724
+
725
+ # Add the default URL format.
726
+ _format ||= @default_url_format
727
+
728
+ # Write the hyperlink string.
729
+ write_string(_row, _col, hyperlink.str, _format) unless _ignore_write_string
730
+ end
731
+
732
+ #
733
+ # :call-seq:
734
+ # write_date_time (row, col, date_string [ , format ])
735
+ #
736
+ # Write a datetime string in ISO8601 "yyyy-mm-ddThh:mm:ss.ss" format as a
737
+ # number representing an Excel date. format is optional.
738
+ #
739
+ def write_date_time(row, col, str, format = nil)
740
+ # Check for a cell reference in A1 notation and substitute row and column
741
+ if (row_col_array = row_col_notation(row))
742
+ _row, _col = row_col_array
743
+ _str = col
744
+ _format = str
745
+ else
746
+ _row = row
747
+ _col = col
748
+ _str = str
749
+ _format = format
750
+ end
751
+ raise WriteXLSXInsufficientArgumentError if [_row, _col, _str].include?(nil)
752
+
753
+ # Check that row and col are valid and store max and min values
754
+ check_dimensions(_row, _col)
755
+ store_row_col_max_min_values(_row, _col)
756
+
757
+ date_time = convert_date_time(_str)
758
+
759
+ if date_time
760
+ store_data_to_table(DateTimeCellData.new(date_time, _format), _row, _col)
761
+ else
762
+ # If the date isn't valid then write it as a string.
763
+ write_string(_row, _col, _str, _format)
764
+ end
765
+ end
766
+
767
+ #
768
+ # Causes the write() method to treat integers with a leading zero as a string.
769
+ # This ensures that any leading zeros such, as in zip codes, are maintained.
770
+ #
771
+ def keep_leading_zeros(flag = true)
772
+ @leading_zeros = !!flag
773
+ end
774
+
775
+ #
776
+ # merge_range(first_row, first_col, last_row, last_col, string, format)
777
+ #
778
+ # Merge a range of cells. The first cell should contain the data and the
779
+ # others should be blank. All cells should contain the same format.
780
+ #
781
+ def merge_range(*args)
782
+ if (row_col_array = row_col_notation(args.first))
783
+ row_first, col_first, row_last, col_last = row_col_array
784
+ string, format, *extra_args = args[1..-1]
785
+ else
786
+ row_first, col_first, row_last, col_last,
787
+ string, format, *extra_args = args
788
+ end
789
+
790
+ raise "Incorrect number of arguments" if [row_first, col_first, row_last, col_last, format].include?(nil)
791
+ raise "Fifth parameter must be a format object" unless format.respond_to?(:xf_index)
792
+ raise "Can't merge single cell" if row_first == row_last && col_first == col_last
793
+
794
+ # Swap last row/col with first row/col as necessary
795
+ row_first, row_last = row_last, row_first if row_first > row_last
796
+ col_first, col_last = col_last, col_first if col_first > col_last
797
+
798
+ # Check that the data range is valid and store the max and min values.
799
+ check_dimensions(row_first, col_first)
800
+ check_dimensions(row_last, col_last)
801
+ store_row_col_max_min_values(row_first, col_first)
802
+ store_row_col_max_min_values(row_last, col_last)
803
+
804
+ # Store the merge range.
805
+ @merge << [row_first, col_first, row_last, col_last]
806
+
807
+ # Write the first cell
808
+ write(row_first, col_first, string, format, *extra_args)
809
+
810
+ # Pad out the rest of the area with formatted blank cells.
811
+ write_formatted_blank_to_area(row_first, row_last, col_first, col_last, format)
812
+ end
813
+
814
+ #
815
+ # Same as merge_range() above except the type of
816
+ # {#write()}[#method-i-write] is specified.
817
+ #
818
+ def merge_range_type(type, *args)
819
+ case type
820
+ when 'array_formula', 'blank', 'rich_string'
821
+ if (row_col_array = row_col_notation(args.first))
822
+ row_first, col_first, row_last, col_last = row_col_array
823
+ *others = args[1..-1]
824
+ else
825
+ row_first, col_first, row_last, col_last, *others = args
826
+ end
827
+ format = others.pop
828
+ else
829
+ if (row_col_array = row_col_notation(args.first))
830
+ row_first, col_first, row_last, col_last = row_col_array
831
+ token, format, *others = args[1..-1]
832
+ else
833
+ row_first, col_first, row_last, col_last,
834
+ token, format, *others = args
835
+ end
836
+ end
837
+
838
+ raise "Format object missing or in an incorrect position" unless format.respond_to?(:xf_index)
839
+ raise "Can't merge single cell" if row_first == row_last && col_first == col_last
840
+
841
+ # Swap last row/col with first row/col as necessary
842
+ row_first, row_last = row_last, row_first if row_first > row_last
843
+ col_first, col_last = col_last, col_first if col_first > col_last
844
+
845
+ # Check that the data range is valid and store the max and min values.
846
+ check_dimensions(row_first, col_first)
847
+ check_dimensions(row_last, col_last)
848
+ store_row_col_max_min_values(row_first, col_first)
849
+ store_row_col_max_min_values(row_last, col_last)
850
+
851
+ # Store the merge range.
852
+ @merge << [row_first, col_first, row_last, col_last]
853
+
854
+ # Write the first cell
855
+ case type
856
+ when 'blank', 'rich_string', 'array_formula'
857
+ others << format
858
+ end
859
+
860
+ case type
861
+ when 'string'
862
+ write_string(row_first, col_first, token, format, *others)
863
+ when 'number'
864
+ write_number(row_first, col_first, token, format, *others)
865
+ when 'blank'
866
+ write_blank(row_first, col_first, *others)
867
+ when 'date_time'
868
+ write_date_time(row_first, col_first, token, format, *others)
869
+ when 'rich_string'
870
+ write_rich_string(row_first, col_first, *others)
871
+ when 'url'
872
+ write_url(row_first, col_first, token, format, *others)
873
+ when 'formula'
874
+ write_formula(row_first, col_first, token, format, *others)
875
+ when 'array_formula'
876
+ write_formula_array(row_first, col_first, *others)
877
+ else
878
+ raise "Unknown type '#{type}'"
879
+ end
880
+
881
+ # Pad out the rest of the area with formatted blank cells.
882
+ write_formatted_blank_to_area(row_first, row_last, col_first, col_last, format)
883
+ end
884
+
885
+ #
886
+ # :call-seq:
887
+ # repeat_formula(row, column, formula [ , format ])
888
+ #
889
+ # Deprecated. This is a writeexcel gem's method that is no longer
890
+ # required by WriteXLSX.
891
+ #
892
+ def repeat_formula(row, col, formula, format, *pairs)
893
+ # Check for a cell reference in A1 notation and substitute row and column.
894
+ if (row_col_array = row_col_notation(row))
895
+ _row, _col = row_col_array
896
+ _formula = col
897
+ _format = formula
898
+ _pairs = [format] + pairs
899
+ else
900
+ _row = row
901
+ _col = col
902
+ _formula = formula
903
+ _format = format
904
+ _pairs = pairs
905
+ end
906
+ raise WriteXLSXInsufficientArgumentError if [_row, _col].include?(nil)
907
+
908
+ raise "Odd number of elements in pattern/replacement list" unless _pairs.size.even?
909
+ raise "Not a valid formula" unless _formula.respond_to?(:to_ary)
910
+
911
+ tokens = _formula.join("\t").split("\t")
912
+ raise "No tokens in formula" if tokens.empty?
913
+
914
+ _value = nil
915
+ if _pairs[-2] == 'result'
916
+ _value = _pairs.pop
917
+ _pairs.pop
918
+ end
919
+ until _pairs.empty?
920
+ pattern = _pairs.shift
921
+ replace = _pairs.shift
922
+
923
+ tokens.each do |token|
924
+ break if token.sub!(pattern, replace)
925
+ end
926
+ end
927
+ _formula = tokens.join
928
+ write_formula(_row, _col, _formula, _format, _value)
929
+ end
930
+
931
+ #
932
+ # :call-seq:
933
+ # update_range_format_with_params(row_first, col_first, row_last, col_last, format_params)
934
+ #
935
+ # Update formatting of cells in range to the specified row and column (zero indexed).
936
+ #
937
+ def update_range_format_with_params(row_first, col_first, row_last = nil, col_last = nil, params = nil)
938
+ if (row_col_array = row_col_notation(row_first))
939
+ _row_first, _col_first, _row_last, _col_last = row_col_array
940
+ params = args[1..-1]
941
+ else
942
+ _row_first = row_first
943
+ _col_first = col_first
944
+ _row_last = row_last
945
+ _col_last = col_last
946
+ _params = params
947
+ end
948
+
949
+ raise WriteXLSXInsufficientArgumentError if [_row_first, _col_first, _row_last, _col_last, _params].include?(nil)
950
+
951
+ # Swap last row/col with first row/col as necessary
952
+ _row_first, _row_last = _row_last, _row_first if _row_first > _row_last
953
+ _col_first, _col_last = _col_last, _col_first if _col_first > _col_last
954
+
955
+ # Check that column number is valid and store the max value
956
+ check_dimensions(_row_last, _col_last)
957
+ store_row_col_max_min_values(_row_last, _col_last)
958
+
959
+ (_row_first.._row_last).each do |row|
960
+ (_col_first.._col_last).each do |col|
961
+ update_format_with_params(row, col, _params)
962
+ end
963
+ end
964
+ end
965
+
966
+ private
967
+
968
+ def store_hyperlink(row, col, hyperlink)
969
+ @hyperlinks ||= {}
970
+ @hyperlinks[row] ||= {}
971
+ @hyperlinks[row][col] = hyperlink
972
+ end
973
+
974
+ def hyperlinks_count
975
+ @hyperlinks.keys.inject(0) { |s, n| s += @hyperlinks[n].keys.size }
976
+ end
977
+
978
+ # Pad out the rest of the area with formatted blank cells.
979
+ def write_formatted_blank_to_area(row_first, row_last, col_first, col_last, format)
980
+ (row_first..row_last).each do |row|
981
+ (col_first..col_last).each do |col|
982
+ next if row == row_first && col == col_first
983
+
984
+ write_blank(row, col, format)
985
+ end
986
+ end
987
+ end
988
+ end
989
+ end
990
+ end