write_xlsx 1.12.3 → 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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -0
  3. data/Changes +3 -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 +1 -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. metadata +26 -3
  38. data/lib/write_xlsx/worksheet/page_setup.rb +0 -192
@@ -0,0 +1,1241 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Writexlsx
4
+ class Worksheet
5
+ module XmlWriter
6
+ include Utility
7
+
8
+ def assemble_xml_file # :nodoc:
9
+ write_xml_declaration do
10
+ @writer.tag_elements('worksheet', write_worksheet_attributes) do
11
+ write_sheet_pr
12
+ write_dimension
13
+ write_sheet_views
14
+ write_sheet_format_pr
15
+ write_cols
16
+ write_sheet_data
17
+ write_sheet_protection
18
+ write_protected_ranges
19
+ # write_sheet_calc_pr
20
+ write_phonetic_pr if excel2003_style?
21
+ write_auto_filter
22
+ write_merge_cells
23
+ write_conditional_formats
24
+ write_data_validations
25
+ write_hyperlinks
26
+ write_print_options
27
+ write_page_margins
28
+ write_page_setup
29
+ write_header_footer
30
+ write_row_breaks
31
+ write_col_breaks
32
+ write_ignored_errors
33
+ write_drawings
34
+ write_legacy_drawing
35
+ write_legacy_drawing_hf
36
+ write_picture
37
+ write_table_parts
38
+ write_ext_list
39
+ end
40
+ end
41
+ end
42
+
43
+ #
44
+ # Write the <worksheet> element. This is the root element of Worksheet.
45
+ #
46
+ def write_worksheet_attributes # :nodoc:
47
+ schema = 'http://schemas.openxmlformats.org/'
48
+ attributes = [
49
+ ['xmlns', "#{schema}spreadsheetml/2006/main"],
50
+ ['xmlns:r', "#{schema}officeDocument/2006/relationships"]
51
+ ]
52
+
53
+ if @excel_version == 2010
54
+ attributes << ['xmlns:mc', "#{schema}markup-compatibility/2006"]
55
+ attributes << ['xmlns:x14ac', "#{OFFICE_URL}spreadsheetml/2009/9/ac"]
56
+ attributes << ['mc:Ignorable', 'x14ac']
57
+ end
58
+ attributes
59
+ end
60
+
61
+ #
62
+ # Write the cell value <v> element.
63
+ #
64
+ def write_cell_value(value = '') # :nodoc:
65
+ return write_cell_formula('=NA()') if value.is_a?(Float) && value.nan?
66
+
67
+ value ||= ''
68
+
69
+ int_value = value.to_i
70
+ value = int_value if value == int_value
71
+ @writer.data_element('v', value)
72
+ end
73
+
74
+ #
75
+ # Write the cell formula <f> element.
76
+ #
77
+ def write_cell_formula(formula = '') # :nodoc:
78
+ @writer.data_element('f', formula)
79
+ end
80
+
81
+ #
82
+ # Write the cell array formula <f> element.
83
+ #
84
+ def write_cell_array_formula(formula, range) # :nodoc:
85
+ @writer.data_element(
86
+ 'f', formula,
87
+ [
88
+ %w[t array],
89
+ ['ref', range]
90
+ ]
91
+ )
92
+ end
93
+
94
+ #
95
+ # Write the frozen or split <pane> elements.
96
+ #
97
+ def write_panes # :nodoc:
98
+ return if @panes.empty?
99
+
100
+ if @panes[4] == 2
101
+ write_split_panes
102
+ else
103
+ write_freeze_panes(*@panes)
104
+ end
105
+ end
106
+
107
+ #
108
+ # Write the <pane> element for freeze panes.
109
+ #
110
+ def write_freeze_panes(row, col, top_row, left_col, type) # :nodoc:
111
+ y_split = row
112
+ x_split = col
113
+ top_left_cell = xl_rowcol_to_cell(top_row, left_col)
114
+
115
+ # Move user cell selection to the panes.
116
+ unless @selections.empty?
117
+ _dummy, active_cell, sqref = @selections[0]
118
+ @selections = []
119
+ end
120
+
121
+ active_cell ||= nil
122
+ sqref ||= nil
123
+ active_pane = set_active_pane_and_cell_selections(row, col, row, col, active_cell, sqref)
124
+
125
+ # Set the pane type.
126
+ state = if type == 0
127
+ 'frozen'
128
+ elsif type == 1
129
+ 'frozenSplit'
130
+ else
131
+ 'split'
132
+ end
133
+
134
+ attributes = []
135
+ attributes << ['xSplit', x_split] if x_split > 0
136
+ attributes << ['ySplit', y_split] if y_split > 0
137
+ attributes << ['topLeftCell', top_left_cell]
138
+ attributes << ['activePane', active_pane]
139
+ attributes << ['state', state]
140
+
141
+ @writer.empty_tag('pane', attributes)
142
+ end
143
+
144
+ #
145
+ # Write the <pane> element for split panes.
146
+ #
147
+ # See also, implementers note for split_panes().
148
+ #
149
+ def write_split_panes # :nodoc:
150
+ row, col, top_row, left_col = @panes
151
+ has_selection = false
152
+ y_split = row
153
+ x_split = col
154
+
155
+ # Move user cell selection to the panes.
156
+ unless @selections.empty?
157
+ _dummy, active_cell, sqref = @selections[0]
158
+ @selections = []
159
+ has_selection = true
160
+ end
161
+
162
+ # Convert the row and col to 1/20 twip units with padding.
163
+ y_split = ((20 * y_split) + 300).to_i if y_split > 0
164
+ x_split = calculate_x_split_width(x_split) if x_split > 0
165
+
166
+ # For non-explicit topLeft definitions, estimate the cell offset based
167
+ # on the pixels dimensions. This is only a workaround and doesn't take
168
+ # adjusted cell dimensions into account.
169
+ if top_row == row && left_col == col
170
+ top_row = (0.5 + ((y_split - 300) / 20 / 15)).to_i
171
+ left_col = (0.5 + ((x_split - 390) / 20 / 3 * 4 / 64)).to_i
172
+ end
173
+
174
+ top_left_cell = xl_rowcol_to_cell(top_row, left_col)
175
+
176
+ # If there is no selection set the active cell to the top left cell.
177
+ unless has_selection
178
+ active_cell = top_left_cell
179
+ sqref = top_left_cell
180
+ end
181
+ active_pane = set_active_pane_and_cell_selections(
182
+ row, col, top_row, left_col, active_cell, sqref
183
+ )
184
+
185
+ attributes = []
186
+ attributes << ['xSplit', x_split] if x_split > 0
187
+ attributes << ['ySplit', y_split] if y_split > 0
188
+ attributes << ['topLeftCell', top_left_cell]
189
+ attributes << ['activePane', active_pane] if has_selection
190
+
191
+ @writer.empty_tag('pane', attributes)
192
+ end
193
+
194
+ #
195
+ # Convert column width from user units to pane split width.
196
+ #
197
+ def calculate_x_split_width(width) # :nodoc:
198
+ # Convert to pixels.
199
+ pixels = if width < 1
200
+ int((width * 12) + 0.5)
201
+ else
202
+ ((width * MAX_DIGIT_WIDTH) + 0.5).to_i + PADDING
203
+ end
204
+
205
+ # Convert to points.
206
+ points = pixels * 3 / 4
207
+
208
+ # Convert to twips (twentieths of a point).
209
+ twips = points * 20
210
+
211
+ # Add offset/padding.
212
+ twips + 390
213
+ end
214
+
215
+ #
216
+ # Write the <sheetViews> element.
217
+ #
218
+ def write_sheet_views # :nodoc:
219
+ @writer.tag_elements('sheetViews', []) { write_sheet_view }
220
+ end
221
+
222
+ def write_sheet_view # :nodoc:
223
+ attributes = []
224
+ # Hide screen gridlines if required.
225
+ attributes << ['showGridLines', 0] unless @screen_gridlines
226
+
227
+ # Hide the row/column headers.
228
+ attributes << ['showRowColHeaders', 0] if ptrue?(@hide_row_col_headers)
229
+
230
+ # Hide zeroes in cells.
231
+ attributes << ['showZeros', 0] unless show_zeros?
232
+
233
+ # Display worksheet right to left for Hebrew, Arabic and others.
234
+ attributes << ['rightToLeft', 1] if @right_to_left
235
+
236
+ # Show that the sheet tab is selected.
237
+ attributes << ['tabSelected', 1] if @selected
238
+
239
+ # Turn outlines off. Also required in the outlinePr element.
240
+ attributes << ["showOutlineSymbols", 0] if @outline_on
241
+
242
+ # Set the page view/layout mode if required.
243
+ case @page_view
244
+ when 1
245
+ attributes << %w[view pageLayout]
246
+ when 2
247
+ attributes << %w[view pageBreakPreview]
248
+ end
249
+
250
+ # Set the first visible cell.
251
+ attributes << ['topLeftCell', @top_left_cell] if ptrue?(@top_left_cell)
252
+
253
+ # Set the zoom level.
254
+ if @zoom != 100
255
+ attributes << ['zoomScale', @zoom]
256
+
257
+ if @page_view == 1
258
+ attributes << ['zoomScalePageLayoutView', @zoom]
259
+ elsif @page_view == 2
260
+ attributes << ['zoomScaleSheetLayoutView', @zoom]
261
+ elsif ptrue?(@zoom_scale_normal)
262
+ attributes << ['zoomScaleNormal', @zoom]
263
+ end
264
+ end
265
+
266
+ attributes << ['workbookViewId', 0]
267
+
268
+ if @panes.empty? && @selections.empty?
269
+ @writer.empty_tag('sheetView', attributes)
270
+ else
271
+ @writer.tag_elements('sheetView', attributes) do
272
+ write_panes
273
+ write_selections
274
+ end
275
+ end
276
+ end
277
+
278
+ #
279
+ # Write the <cols> element and <col> sub elements.
280
+ #
281
+ def write_cols # :nodoc:
282
+ # Exit unless some column have been formatted.
283
+ return if @col_info.empty?
284
+
285
+ @writer.tag_elements('cols') do
286
+ # Use the first element of the column informatin structure to set
287
+ # the initial/previous properties.
288
+ first_col = @col_info.keys.min
289
+ last_col = first_col
290
+ previous_options = @col_info[first_col]
291
+ deleted_col = first_col
292
+ deleted_col_options = previous_options
293
+
294
+ @col_info.delete(first_col)
295
+
296
+ @col_info.keys.sort.each do |col|
297
+ col_options = @col_info[col]
298
+
299
+ # Check if the column number is contiguous with the previous
300
+ # column and if the properties are the same.
301
+ if (col == last_col + 1) &&
302
+ compare_col_info(col_options, previous_options)
303
+ last_col = col
304
+ else
305
+ # If not contiguous/equal then we write out the current range
306
+ # of columns and start again.
307
+ write_col_info([first_col, last_col, previous_options])
308
+ first_col = col
309
+ last_col = first_col
310
+ previous_options = col_options
311
+ end
312
+ end
313
+
314
+ # We will exit the previous loop with one unhandled column range.
315
+ write_col_info([first_col, last_col, previous_options])
316
+
317
+ # Put back the deleted first column information structure:
318
+ @col_info[deleted_col] = deleted_col_options
319
+ end
320
+ end
321
+
322
+ #
323
+ # Write the <col> element.
324
+ #
325
+ def write_col_info(args) # :nodoc:
326
+ @writer.empty_tag('col', col_info_attributes(args))
327
+ end
328
+
329
+ def col_info_attributes(args)
330
+ min = args[0] || 0 # First formatted column.
331
+ max = args[1] || 0 # Last formatted column.
332
+ width = args[2].width # Col width in user units.
333
+ format = args[2].format # Format index.
334
+ hidden = args[2].hidden || 0 # Hidden flag.
335
+ level = args[2].level || 0 # Outline level.
336
+ collapsed = args[2].collapsed || 0 # Outline Collapsed
337
+ autofit = args[2].autofit || 0 # Best fit for autofit numbers.
338
+ xf_index = format ? format.get_xf_index : 0
339
+
340
+ custom_width = true
341
+ custom_width = false if width.nil? && hidden == 0
342
+ custom_width = false if !width.nil? && (width - 8.43).abs < 0.01
343
+
344
+ width ||= hidden == 0 ? @default_col_width : 0
345
+
346
+ # Convert column width from user units to character width.
347
+ width = if width && width < 1
348
+ (((width * (MAX_DIGIT_WIDTH + PADDING)) + 0.5).to_i / MAX_DIGIT_WIDTH.to_f * 256).to_i / 256.0
349
+ else
350
+ ((((width * MAX_DIGIT_WIDTH) + 0.5).to_i + PADDING).to_i / MAX_DIGIT_WIDTH.to_f * 256).to_i / 256.0
351
+ end
352
+ width = width.to_i if width - width.to_i == 0
353
+
354
+ attributes = [
355
+ ['min', min + 1],
356
+ ['max', max + 1],
357
+ ['width', width]
358
+ ]
359
+
360
+ attributes << ['style', xf_index] if xf_index != 0
361
+ attributes << ['hidden', 1] if hidden != 0
362
+ attributes << ['bestFit', 1] if autofit != 0
363
+ attributes << ['customWidth', 1] if custom_width
364
+ attributes << ['outlineLevel', level] if level != 0
365
+ attributes << ['collapsed', 1] if collapsed != 0
366
+ attributes
367
+ end
368
+
369
+ #
370
+ # Write the <sheetData> element.
371
+ #
372
+ def write_sheet_data # :nodoc:
373
+ if @dim_rowmin
374
+ @writer.tag_elements('sheetData') { write_rows }
375
+ else
376
+ # If the dimensions aren't defined then there is no data to write.
377
+ @writer.empty_tag('sheetData')
378
+ end
379
+ end
380
+
381
+ #
382
+ # Write out the worksheet data as a series of rows and cells.
383
+ #
384
+ def write_rows # :nodoc:
385
+ calculate_spans
386
+
387
+ (@dim_rowmin..@dim_rowmax).each do |row_num|
388
+ # Skip row if it doesn't contain row formatting or cell data.
389
+ next if not_contain_formatting_or_data?(row_num)
390
+
391
+ span_index = row_num / 16
392
+ span = @row_spans[span_index]
393
+
394
+ # Write the cells if the row contains data.
395
+ if @cell_data_store[row_num]
396
+ args = @set_rows[row_num] || []
397
+ write_row_element(row_num, span, *args) do
398
+ write_cell_column_dimension(row_num)
399
+ end
400
+ else
401
+ # Row attributes only.
402
+ write_empty_row(row_num, span, *@set_rows[row_num])
403
+ end
404
+ end
405
+ end
406
+
407
+ def not_contain_formatting_or_data?(row_num) # :nodoc:
408
+ !@set_rows[row_num] && !@cell_data_store[row_num] && !@comments.has_comment_in_row?(row_num)
409
+ end
410
+
411
+ #
412
+ # Write the <row> element.
413
+ #
414
+ def write_row_element(*args, &block) # :nodoc:
415
+ @writer.tag_elements('row', row_attributes(args), &block)
416
+ end
417
+
418
+ def write_cell_column_dimension(row_num) # :nodoc:
419
+ row = @cell_data_store[row_num]
420
+ row_name = (row_num + 1).to_s
421
+ (@dim_colmin..@dim_colmax).each do |col_num|
422
+ if (cell = row[col_num])
423
+ cell.write_cell(self, row_num, row_name, col_num)
424
+ end
425
+ end
426
+ end
427
+
428
+ #
429
+ # Write and empty <row> element, i.e., attributes only, no cell data.
430
+ #
431
+ def write_empty_row(*args) # :nodoc:
432
+ @writer.empty_tag('row', row_attributes(args))
433
+ end
434
+
435
+ def row_attributes(args)
436
+ r, spans, height, format, hidden, level, collapsed, _empty_row = args
437
+ height ||= @default_row_height
438
+ hidden ||= 0
439
+ level ||= 0
440
+ xf_index = format ? format.get_xf_index : 0
441
+
442
+ attributes = [['r', r + 1]]
443
+
444
+ attributes << ['spans', spans] if spans
445
+ attributes << ['s', xf_index] if ptrue?(xf_index)
446
+ attributes << ['customFormat', 1] if ptrue?(format)
447
+ attributes << ['ht', height] if height != @original_row_height
448
+ attributes << ['hidden', 1] if ptrue?(hidden)
449
+ attributes << ['customHeight', 1] if height != @original_row_height
450
+ attributes << ['outlineLevel', level] if ptrue?(level)
451
+ attributes << ['collapsed', 1] if ptrue?(collapsed)
452
+
453
+ attributes << ['x14ac:dyDescent', '0.25'] if @excel_version == 2010
454
+ attributes
455
+ end
456
+
457
+ #
458
+ # Write the <sheetProtection> element.
459
+ #
460
+ def write_sheet_protection # :nodoc:
461
+ return unless protect?
462
+
463
+ attributes = []
464
+ attributes << ["password", @protect[:password]] if ptrue?(@protect[:password])
465
+ attributes << ["sheet", 1] if ptrue?(@protect[:sheet])
466
+ attributes << ["content", 1] if ptrue?(@protect[:content])
467
+ attributes << ["objects", 1] unless ptrue?(@protect[:objects])
468
+ attributes << ["scenarios", 1] unless ptrue?(@protect[:scenarios])
469
+ attributes << ["formatCells", 0] if ptrue?(@protect[:format_cells])
470
+ attributes << ["formatColumns", 0] if ptrue?(@protect[:format_columns])
471
+ attributes << ["formatRows", 0] if ptrue?(@protect[:format_rows])
472
+ attributes << ["insertColumns", 0] if ptrue?(@protect[:insert_columns])
473
+ attributes << ["insertRows", 0] if ptrue?(@protect[:insert_rows])
474
+ attributes << ["insertHyperlinks", 0] if ptrue?(@protect[:insert_hyperlinks])
475
+ attributes << ["deleteColumns", 0] if ptrue?(@protect[:delete_columns])
476
+ attributes << ["deleteRows", 0] if ptrue?(@protect[:delete_rows])
477
+
478
+ attributes << ["selectLockedCells", 1] unless ptrue?(@protect[:select_locked_cells])
479
+
480
+ attributes << ["sort", 0] if ptrue?(@protect[:sort])
481
+ attributes << ["autoFilter", 0] if ptrue?(@protect[:autofilter])
482
+ attributes << ["pivotTables", 0] if ptrue?(@protect[:pivot_tables])
483
+
484
+ attributes << ["selectUnlockedCells", 1] unless ptrue?(@protect[:select_unlocked_cells])
485
+
486
+ @writer.empty_tag('sheetProtection', attributes)
487
+ end
488
+
489
+ #
490
+ # Write the <protectedRanges> element.
491
+ #
492
+ def write_protected_ranges
493
+ return if @num_protected_ranges == 0
494
+
495
+ @writer.tag_elements('protectedRanges') do
496
+ @protected_ranges.each do |protected_range|
497
+ write_protected_range(*protected_range)
498
+ end
499
+ end
500
+ end
501
+
502
+ #
503
+ # Write the <protectedRange> element.
504
+ #
505
+ def write_protected_range(sqref, name, password)
506
+ attributes = []
507
+
508
+ attributes << ['password', password] if password
509
+ attributes << ['sqref', sqref]
510
+ attributes << ['name', name]
511
+
512
+ @writer.empty_tag('protectedRange', attributes)
513
+ end
514
+
515
+ #
516
+ # Write the <sheetCalcPr> element for the worksheet calculation properties.
517
+ #
518
+ def write_sheet_calc_pr # :nodoc:
519
+ @writer.empty_tag('sheetCalcPr', [['fullCalcOnLoad', 1]])
520
+ end
521
+
522
+ #
523
+ # Write the <phoneticPr> element.
524
+ #
525
+ def write_phonetic_pr # :nodoc:
526
+ attributes = [
527
+ ['fontId', 0],
528
+ %w[type noConversion]
529
+ ]
530
+
531
+ @writer.empty_tag('phoneticPr', attributes)
532
+ end
533
+
534
+ #
535
+ # Write the <autoFilter> element.
536
+ #
537
+ def write_auto_filter # :nodoc:
538
+ return unless autofilter_ref?
539
+
540
+ attributes = [
541
+ ['ref', @autofilter_ref]
542
+ ]
543
+
544
+ if filter_on?
545
+ # Autofilter defined active filters.
546
+ @writer.tag_elements('autoFilter', attributes) do
547
+ write_autofilters
548
+ end
549
+ else
550
+ # Autofilter defined without active filters.
551
+ @writer.empty_tag('autoFilter', attributes)
552
+ end
553
+ end
554
+
555
+ #
556
+ # Function to iterate through the columns that form part of an autofilter
557
+ # range and write the appropriate filters.
558
+ #
559
+ def write_autofilters # :nodoc:
560
+ col1, col2 = @filter_range
561
+
562
+ (col1..col2).each do |col|
563
+ # Skip if column doesn't have an active filter.
564
+ next unless @filter_cols[col]
565
+
566
+ # Retrieve the filter tokens and write the autofilter records.
567
+ tokens = @filter_cols[col]
568
+ type = @filter_type[col]
569
+
570
+ # Filters are relative to first column in the autofilter.
571
+ write_filter_column(col - col1, type, *tokens)
572
+ end
573
+ end
574
+
575
+ #
576
+ # Write the <filterColumn> element.
577
+ #
578
+ def write_filter_column(col_id, type, *filters) # :nodoc:
579
+ @writer.tag_elements('filterColumn', [['colId', col_id]]) do
580
+ if type == 1
581
+ # Type == 1 is the new XLSX style filter.
582
+ write_filters(*filters)
583
+ else
584
+ # Type == 0 is the classic "custom" filter.
585
+ write_custom_filters(*filters)
586
+ end
587
+ end
588
+ end
589
+
590
+ #
591
+ # Write the <filters> element.
592
+ #
593
+ def write_filters(*filters) # :nodoc:
594
+ non_blanks = filters.reject { |filter| filter.to_s =~ /^blanks$/i }
595
+ attributes = []
596
+
597
+ attributes = [['blank', 1]] if filters != non_blanks
598
+
599
+ if filters.size == 1 && non_blanks.empty?
600
+ # Special case for blank cells only.
601
+ @writer.empty_tag('filters', attributes)
602
+ else
603
+ # General case.
604
+ @writer.tag_elements('filters', attributes) do
605
+ non_blanks.sort.each { |filter| write_filter(filter) }
606
+ end
607
+ end
608
+ end
609
+
610
+ #
611
+ # Write the <filter> element.
612
+ #
613
+ def write_filter(val) # :nodoc:
614
+ @writer.empty_tag('filter', [['val', val]])
615
+ end
616
+
617
+ #
618
+ # Write the <customFilters> element.
619
+ #
620
+ def write_custom_filters(*tokens) # :nodoc:
621
+ if tokens.size == 2
622
+ # One filter expression only.
623
+ @writer.tag_elements('customFilters') { write_custom_filter(*tokens) }
624
+ else
625
+ # Two filter expressions.
626
+
627
+ # Check if the "join" operand is "and" or "or".
628
+ attributes = if tokens[2] == 0
629
+ [['and', 1]]
630
+ else
631
+ [['and', 0]]
632
+ end
633
+
634
+ # Write the two custom filters.
635
+ @writer.tag_elements('customFilters', attributes) do
636
+ write_custom_filter(tokens[0], tokens[1])
637
+ write_custom_filter(tokens[3], tokens[4])
638
+ end
639
+ end
640
+ end
641
+
642
+ #
643
+ # Write the <customFilter> element.
644
+ #
645
+ def write_custom_filter(operator, val) # :nodoc:
646
+ operators = {
647
+ 1 => 'lessThan',
648
+ 2 => 'equal',
649
+ 3 => 'lessThanOrEqual',
650
+ 4 => 'greaterThan',
651
+ 5 => 'notEqual',
652
+ 6 => 'greaterThanOrEqual',
653
+ 22 => 'equal'
654
+ }
655
+
656
+ # Convert the operator from a number to a descriptive string.
657
+ if operators[operator]
658
+ operator = operators[operator]
659
+ else
660
+ raise "Unknown operator = #{operator}\n"
661
+ end
662
+
663
+ # The 'equal' operator is the default attribute and isn't stored.
664
+ attributes = []
665
+ attributes << ['operator', operator] unless operator == 'equal'
666
+ attributes << ['val', val]
667
+
668
+ @writer.empty_tag('customFilter', attributes)
669
+ end
670
+
671
+ #
672
+ # Write the <mergeCells> element.
673
+ #
674
+ def write_merge_cells # :nodoc:
675
+ write_some_elements('mergeCells', @merge) do
676
+ @merge.each { |merged_range| write_merge_cell(merged_range) }
677
+ end
678
+ end
679
+
680
+ def write_some_elements(tag, container, &block)
681
+ return if container.empty?
682
+
683
+ @writer.tag_elements(tag, [['count', container.size]], &block)
684
+ end
685
+
686
+ #
687
+ # Write the <mergeCell> element.
688
+ #
689
+ def write_merge_cell(merged_range) # :nodoc:
690
+ row_min, col_min, row_max, col_max = merged_range
691
+
692
+ # Convert the merge dimensions to a cell range.
693
+ cell_1 = xl_rowcol_to_cell(row_min, col_min)
694
+ cell_2 = xl_rowcol_to_cell(row_max, col_max)
695
+
696
+ @writer.empty_tag('mergeCell', [['ref', "#{cell_1}:#{cell_2}"]])
697
+ end
698
+
699
+ #
700
+ # Write the Worksheet conditional formats.
701
+ #
702
+ def write_conditional_formats # :nodoc:
703
+ @cond_formats.keys.sort.each do |range|
704
+ write_conditional_formatting(range, @cond_formats[range])
705
+ end
706
+ end
707
+
708
+ # conditional formatting XML writing moved to worksheet/conditional_formats.rb
709
+ # see Writexlsx::Worksheet::ConditionalFormats#write_conditional_formatting
710
+
711
+ #
712
+ # Write the <dataValidations> element.
713
+ #
714
+ def write_data_validations # :nodoc:
715
+ write_some_elements('dataValidations', @validations) do
716
+ @validations.each { |validation| validation.write_data_validation(@writer) }
717
+ end
718
+ end
719
+
720
+ #
721
+ # Process any sored hyperlinks in row/col order and write the <hyperlinks>
722
+ # element. The attributes are different for internal and external links.
723
+ #
724
+ def write_hyperlinks # :nodoc:
725
+ return unless @hyperlinks
726
+
727
+ hlink_attributes = []
728
+ @hyperlinks.keys.sort.each do |row_num|
729
+ # Sort the hyperlinks into column order.
730
+ col_nums = @hyperlinks[row_num].keys.sort
731
+ # Iterate over the columns.
732
+ col_nums.each do |col_num|
733
+ # Get the link data for this cell.
734
+ link = @hyperlinks[row_num][col_num]
735
+
736
+ # If the cell isn't a string then we have to add the url as
737
+ # the string to display
738
+ if ptrue?(@cell_data_store) &&
739
+ ptrue?(@cell_data_store[row_num]) &&
740
+ ptrue?(@cell_data_store[row_num][col_num]) &&
741
+ @cell_data_store[row_num][col_num].display_url_string?
742
+ link.display_on
743
+ end
744
+
745
+ if link.respond_to?(:external_hyper_link)
746
+ # External link with rel file relationship.
747
+ @rel_count += 1
748
+ # Links for use by the packager.
749
+ @external_hyper_links << link.external_hyper_link
750
+ end
751
+ hlink_attributes << link.attributes(row_num, col_num, @rel_count)
752
+ end
753
+ end
754
+
755
+ return if hlink_attributes.empty?
756
+
757
+ # Write the hyperlink elements.
758
+ @writer.tag_elements('hyperlinks') do
759
+ hlink_attributes.each do |attributes|
760
+ @writer.empty_tag('hyperlink', attributes)
761
+ end
762
+ end
763
+ end
764
+
765
+ #
766
+ # Write the <printOptions> element.
767
+ #
768
+ def write_print_options # :nodoc:
769
+ @page_setup.write_print_options(@writer)
770
+ end
771
+
772
+ #
773
+ # Write the <pageMargins> element.
774
+ #
775
+ def write_page_margins # :nodoc:
776
+ @page_setup.write_page_margins(@writer)
777
+ end
778
+
779
+ #
780
+ # Write the <pageSetup> element.
781
+ #
782
+ def write_page_setup # :nodoc:
783
+ @page_setup.write_page_setup(@writer)
784
+ end
785
+
786
+ #
787
+ # Write the <headerFooter> element.
788
+ #
789
+ def write_header_footer # :nodoc:
790
+ @page_setup.write_header_footer(@writer, excel2003_style?)
791
+ end
792
+
793
+ #
794
+ # Write the <rowBreaks> element.
795
+ #
796
+ def write_row_breaks # :nodoc:
797
+ write_breaks('rowBreaks')
798
+ end
799
+
800
+ #
801
+ # Write the <colBreaks> element.
802
+ #
803
+ def write_col_breaks # :nodoc:
804
+ write_breaks('colBreaks')
805
+ end
806
+
807
+ def write_breaks(tag) # :nodoc:
808
+ case tag
809
+ when 'rowBreaks'
810
+ page_breaks = sort_pagebreaks(*@page_setup.hbreaks)
811
+ max = 16383
812
+ when 'colBreaks'
813
+ page_breaks = sort_pagebreaks(*@page_setup.vbreaks)
814
+ max = 1048575
815
+ else
816
+ raise "Invalid parameter '#{tag}' in write_breaks."
817
+ end
818
+ count = page_breaks.size
819
+
820
+ return if page_breaks.empty?
821
+
822
+ attributes = [
823
+ ['count', count],
824
+ ['manualBreakCount', count]
825
+ ]
826
+
827
+ @writer.tag_elements(tag, attributes) do
828
+ page_breaks.each { |num| write_brk(num, max) }
829
+ end
830
+ end
831
+
832
+ #
833
+ # Write the <brk> element.
834
+ #
835
+ def write_brk(id, max) # :nodoc:
836
+ attributes = [
837
+ ['id', id],
838
+ ['max', max],
839
+ ['man', 1]
840
+ ]
841
+
842
+ @writer.empty_tag('brk', attributes)
843
+ end
844
+
845
+ #
846
+ # Write the <ignoredErrors> element.
847
+ #
848
+ def write_ignored_errors
849
+ return unless @ignore_errors
850
+
851
+ ignore = @ignore_errors
852
+
853
+ @writer.tag_elements('ignoredErrors') do
854
+ {
855
+ number_stored_as_text: 'numberStoredAsText',
856
+ eval_error: 'evalError',
857
+ formula_differs: 'formula',
858
+ formula_range: 'formulaRange',
859
+ formula_unlocked: 'unlockedFormula',
860
+ empty_cell_reference: 'emptyCellReference',
861
+ list_data_validation: 'listDataValidation',
862
+ calculated_column: 'calculatedColumn',
863
+ two_digit_text_year: 'twoDigitTextYear'
864
+ }.each do |key, value|
865
+ write_ignored_error(value, ignore[key]) if ignore[key]
866
+ end
867
+ end
868
+ end
869
+
870
+ #
871
+ # Write the <ignoredError> element.
872
+ #
873
+ def write_ignored_error(type, sqref)
874
+ attributes = [
875
+ ['sqref', sqref],
876
+ [type, 1]
877
+ ]
878
+
879
+ @writer.empty_tag('ignoredError', attributes)
880
+ end
881
+
882
+ #
883
+ # Write the <tableParts> element.
884
+ #
885
+ def write_table_parts
886
+ return if @assets.tables.empty?
887
+
888
+ @writer.tag_elements('tableParts', [['count', tables_count]]) do
889
+ tables_count.times { increment_rel_id_and_write_r_id('tablePart') }
890
+ end
891
+ end
892
+
893
+ #
894
+ # Write the <tablePart> element.
895
+ #
896
+ def write_table_part(id)
897
+ @writer.empty_tag('tablePart', [r_id_attributes(id)])
898
+ end
899
+
900
+ def increment_rel_id_and_write_r_id(tag)
901
+ @rel_count += 1
902
+ write_r_id(tag, @rel_count)
903
+ end
904
+
905
+ def write_r_id(tag, id)
906
+ @writer.empty_tag(tag, [r_id_attributes(id)])
907
+ end
908
+
909
+ #
910
+ # Write the <extLst> element for data bars and sparklines.
911
+ #
912
+ def write_ext_list # :nodoc:
913
+ return if @data_bars_2010.empty? && @assets.sparklines.empty?
914
+
915
+ @writer.tag_elements('extLst') do
916
+ write_ext_list_data_bars unless @data_bars_2010.empty?
917
+ write_ext_list_sparklines unless @assets.sparklines.empty?
918
+ end
919
+ end
920
+
921
+ #
922
+ # Write the Excel 2010 data_bar subelements.
923
+ #
924
+ def write_ext_list_data_bars
925
+ # Write the ext element.
926
+ write_ext('{78C0D931-6437-407d-A8EE-F0AAD7539E65}') do
927
+ @writer.tag_elements('x14:conditionalFormattings') do
928
+ # Write each of the Excel 2010 conditional formatting data bar elements.
929
+ @data_bars_2010.each do |data_bar|
930
+ # Write the x14:conditionalFormatting element.
931
+ write_conditional_formatting_2010(data_bar)
932
+ end
933
+ end
934
+ end
935
+ end
936
+
937
+ def write_ext(url, &block)
938
+ attributes = [
939
+ ['xmlns:x14', "#{OFFICE_URL}spreadsheetml/2009/9/main"],
940
+ ['uri', url]
941
+ ]
942
+ @writer.tag_elements('ext', attributes, &block)
943
+ end
944
+
945
+ #
946
+ # Write the <x14:conditionalFormatting> element.
947
+ #
948
+ def write_conditional_formatting_2010(data_bar)
949
+ xmlns_xm = 'http://schemas.microsoft.com/office/excel/2006/main'
950
+
951
+ attributes = [['xmlns:xm', xmlns_xm]]
952
+
953
+ @writer.tag_elements('x14:conditionalFormatting', attributes) do
954
+ # Write the '<x14:cfRule element.
955
+ write_x14_cf_rule(data_bar)
956
+
957
+ # Write the x14:dataBar element.
958
+ write_x14_data_bar(data_bar)
959
+
960
+ # Write the x14 max and min data bars.
961
+ write_x14_cfvo(data_bar[:x14_min_type], data_bar[:min_value])
962
+ write_x14_cfvo(data_bar[:x14_max_type], data_bar[:max_value])
963
+
964
+ # Write the x14:borderColor element.
965
+ write_x14_border_color(data_bar[:bar_border_color]) unless ptrue?(data_bar[:bar_no_border])
966
+
967
+ # Write the x14:negativeFillColor element.
968
+ write_x14_negative_fill_color(data_bar[:bar_negative_color]) unless ptrue?(data_bar[:bar_negative_color_same])
969
+
970
+ # Write the x14:negativeBorderColor element.
971
+ if !ptrue?(data_bar[:bar_no_border]) &&
972
+ !ptrue?(data_bar[:bar_negative_border_color_same])
973
+ write_x14_negative_border_color(
974
+ data_bar[:bar_negative_border_color]
975
+ )
976
+ end
977
+
978
+ # Write the x14:axisColor element.
979
+ write_x14_axis_color(data_bar[:bar_axis_color]) if data_bar[:bar_axis_position] != 'none'
980
+
981
+ # Write closing elements.
982
+ @writer.end_tag('x14:dataBar')
983
+ @writer.end_tag('x14:cfRule')
984
+
985
+ # Add the conditional format range.
986
+ @writer.data_element('xm:sqref', data_bar[:range])
987
+ end
988
+ end
989
+
990
+ #
991
+ # Write the <cfvo> element.
992
+ #
993
+ def write_x14_cfvo(type, value)
994
+ attributes = [['type', type]]
995
+
996
+ if %w[min max autoMin autoMax].include?(type)
997
+ @writer.empty_tag('x14:cfvo', attributes)
998
+ else
999
+ @writer.tag_elements('x14:cfvo', attributes) do
1000
+ @writer.data_element('xm:f', value)
1001
+ end
1002
+ end
1003
+ end
1004
+
1005
+ #
1006
+ # Write the <'<x14:cfRule> element.
1007
+ #
1008
+ def write_x14_cf_rule(data_bar)
1009
+ type = 'dataBar'
1010
+ id = data_bar[:guid]
1011
+
1012
+ attributes = [
1013
+ ['type', type],
1014
+ ['id', id]
1015
+ ]
1016
+
1017
+ @writer.start_tag('x14:cfRule', attributes)
1018
+ end
1019
+
1020
+ #
1021
+ # Write the <x14:dataBar> element.
1022
+ #
1023
+ def write_x14_data_bar(data_bar)
1024
+ min_length = 0
1025
+ max_length = 100
1026
+
1027
+ attributes = [
1028
+ ['minLength', min_length],
1029
+ ['maxLength', max_length]
1030
+ ]
1031
+
1032
+ attributes << ['border', 1] unless ptrue?(data_bar[:bar_no_border])
1033
+ attributes << ['gradient', 0] if ptrue?(data_bar[:bar_solid])
1034
+
1035
+ attributes << %w[direction leftToRight] if data_bar[:bar_direction] == 'left'
1036
+ attributes << %w[direction rightToLeft] if data_bar[:bar_direction] == 'right'
1037
+
1038
+ attributes << ['negativeBarColorSameAsPositive', 1] if ptrue?(data_bar[:bar_negative_color_same])
1039
+
1040
+ if !ptrue?(data_bar[:bar_no_border]) &&
1041
+ !ptrue?(data_bar[:bar_negative_border_color_same])
1042
+ attributes << ['negativeBarBorderColorSameAsPositive', 0]
1043
+ end
1044
+
1045
+ attributes << %w[axisPosition middle] if data_bar[:bar_axis_position] == 'middle'
1046
+
1047
+ attributes << %w[axisPosition none] if data_bar[:bar_axis_position] == 'none'
1048
+
1049
+ @writer.start_tag('x14:dataBar', attributes)
1050
+ end
1051
+
1052
+ #
1053
+ # Write the <x14:borderColor> element.
1054
+ #
1055
+ def write_x14_border_color(rgb)
1056
+ attributes = [['rgb', rgb]]
1057
+
1058
+ @writer.empty_tag('x14:borderColor', attributes)
1059
+ end
1060
+
1061
+ #
1062
+ # Write the <x14:negativeFillColor> element.
1063
+ #
1064
+ def write_x14_negative_fill_color(rgb)
1065
+ attributes = [['rgb', rgb]]
1066
+
1067
+ @writer.empty_tag('x14:negativeFillColor', attributes)
1068
+ end
1069
+
1070
+ #
1071
+ # Write the <x14:negativeBorderColor> element.
1072
+ #
1073
+ def write_x14_negative_border_color(rgb)
1074
+ attributes = [['rgb', rgb]]
1075
+
1076
+ @writer.empty_tag('x14:negativeBorderColor', attributes)
1077
+ end
1078
+
1079
+ #
1080
+ # Write the <x14:axisColor> element.
1081
+ #
1082
+ def write_x14_axis_color(rgb)
1083
+ attributes = [['rgb', rgb]]
1084
+
1085
+ @writer.empty_tag('x14:axisColor', attributes)
1086
+ end
1087
+
1088
+ #
1089
+ # Write the sparkline subelements.
1090
+ #
1091
+ def write_ext_list_sparklines
1092
+ # Write the ext element.
1093
+ write_ext('{05C60535-1F16-4fd2-B633-F4F36F0B64E0}') do
1094
+ # Write the x14:sparklineGroups element.
1095
+ write_sparkline_groups
1096
+ end
1097
+ end
1098
+
1099
+ def write_sparkline_groups
1100
+ # Write the x14:sparklineGroups element.
1101
+ @writer.tag_elements('x14:sparklineGroups', sparkline_groups_attributes) do
1102
+ # Write the sparkline elements.
1103
+ @assets.sparklines.reverse.each do |sparkline|
1104
+ sparkline.write_sparkline_group(@writer)
1105
+ end
1106
+ end
1107
+ end
1108
+
1109
+ def sparkline_groups_attributes # :nodoc:
1110
+ [
1111
+ ['xmlns:xm', "#{OFFICE_URL}excel/2006/main"]
1112
+ ]
1113
+ end
1114
+
1115
+ #
1116
+ # Write the <sheetPr> element for Sheet level properties.
1117
+ #
1118
+ def write_sheet_pr # :nodoc:
1119
+ return unless tab_outline_fit? || vba_codename? || filter_on?
1120
+
1121
+ attributes = []
1122
+ attributes << ['codeName', @vba_codename] if vba_codename?
1123
+ attributes << ['filterMode', 1] if filter_on?
1124
+
1125
+ if tab_outline_fit?
1126
+ @writer.tag_elements('sheetPr', attributes) do
1127
+ write_tab_color
1128
+ write_outline_pr
1129
+ write_page_set_up_pr
1130
+ end
1131
+ else
1132
+ @writer.empty_tag('sheetPr', attributes)
1133
+ end
1134
+ end
1135
+
1136
+ #
1137
+ # Write the <tabColor> element.
1138
+ #
1139
+ def write_tab_color # :nodoc:
1140
+ return unless tab_color?
1141
+
1142
+ @writer.empty_tag(
1143
+ 'tabColor',
1144
+ [
1145
+ ['rgb', palette_color(@tab_color)]
1146
+ ]
1147
+ )
1148
+ end
1149
+
1150
+ #
1151
+ # Write the <outlinePr> element.
1152
+ #
1153
+ def write_outline_pr
1154
+ return unless outline_changed?
1155
+
1156
+ attributes = []
1157
+ attributes << ["applyStyles", 1] if @outline_style
1158
+ attributes << ["summaryBelow", 0] if @outline_below == 0
1159
+ attributes << ["summaryRight", 0] if @outline_right == 0
1160
+ attributes << ["showOutlineSymbols", 0] if @outline_on == 0
1161
+
1162
+ @writer.empty_tag('outlinePr', attributes)
1163
+ end
1164
+
1165
+ #
1166
+ # Write the <pageSetUpPr> element.
1167
+ #
1168
+ def write_page_set_up_pr # :nodoc:
1169
+ @writer.empty_tag('pageSetUpPr', [['fitToPage', 1]]) if fit_page?
1170
+ end
1171
+
1172
+ # Write the <dimension> element. This specifies the range of cells in the
1173
+ # worksheet. As a special case, empty spreadsheets use 'A1' as a range.
1174
+ #
1175
+ def write_dimension # :nodoc:
1176
+ if !@dim_rowmin && !@dim_colmin
1177
+ # If the min dims are undefined then no dimensions have been set
1178
+ # and we use the default 'A1'.
1179
+ ref = 'A1'
1180
+ elsif !@dim_rowmin && @dim_colmin
1181
+ # If the row dims aren't set but the column dims are then they
1182
+ # have been changed via set_column().
1183
+ if @dim_colmin == @dim_colmax
1184
+ # The dimensions are a single cell and not a range.
1185
+ ref = xl_rowcol_to_cell(0, @dim_colmin)
1186
+ else
1187
+ # The dimensions are a cell range.
1188
+ cell_1 = xl_rowcol_to_cell(0, @dim_colmin)
1189
+ cell_2 = xl_rowcol_to_cell(0, @dim_colmax)
1190
+ ref = cell_1 + ':' + cell_2
1191
+ end
1192
+ elsif @dim_rowmin == @dim_rowmax && @dim_colmin == @dim_colmax
1193
+ # The dimensions are a single cell and not a range.
1194
+ ref = xl_rowcol_to_cell(@dim_rowmin, @dim_colmin)
1195
+ else
1196
+ # The dimensions are a cell range.
1197
+ cell_1 = xl_rowcol_to_cell(@dim_rowmin, @dim_colmin)
1198
+ cell_2 = xl_rowcol_to_cell(@dim_rowmax, @dim_colmax)
1199
+ ref = cell_1 + ':' + cell_2
1200
+ end
1201
+ @writer.empty_tag('dimension', [['ref', ref]])
1202
+ end
1203
+
1204
+ #
1205
+ # Write the <selection> elements.
1206
+ #
1207
+ def write_selections # :nodoc:
1208
+ @selections.each { |selection| write_selection(*selection) }
1209
+ end
1210
+
1211
+ #
1212
+ # Write the <selection> element.
1213
+ #
1214
+ def write_selection(pane, active_cell, sqref) # :nodoc:
1215
+ attributes = []
1216
+ attributes << ['pane', pane] if pane
1217
+ attributes << ['activeCell', active_cell] if active_cell
1218
+ attributes << ['sqref', sqref] if sqref
1219
+
1220
+ @writer.empty_tag('selection', attributes)
1221
+ end
1222
+
1223
+ #
1224
+ # Write the <sheetFormatPr> element.
1225
+ #
1226
+ def write_sheet_format_pr # :nodoc:
1227
+ attributes = [
1228
+ ['defaultRowHeight', @default_row_height]
1229
+ ]
1230
+ attributes << ['customHeight', 1] if @default_row_height != @original_row_height
1231
+
1232
+ attributes << ['zeroHeight', 1] if ptrue?(@default_row_zeroed)
1233
+
1234
+ attributes << ['outlineLevelRow', @outline_row_level] if @outline_row_level > 0
1235
+ attributes << ['outlineLevelCol', @outline_col_level] if @outline_col_level > 0
1236
+ attributes << ['x14ac:dyDescent', '0.25'] if @excel_version == 2010
1237
+ @writer.empty_tag('sheetFormatPr', attributes)
1238
+ end
1239
+ end
1240
+ end
1241
+ end