xlsxrb 0.1.13 → 0.1.15

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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +104 -0
  3. data/README.md +17 -14
  4. data/lib/xlsxrb/colors.rb +118 -0
  5. data/lib/xlsxrb/elements/cell.rb +205 -16
  6. data/lib/xlsxrb/elements/column.rb +6 -4
  7. data/lib/xlsxrb/elements/coordinate_access.rb +32 -0
  8. data/lib/xlsxrb/elements/image.rb +128 -0
  9. data/lib/xlsxrb/elements/row.rb +206 -9
  10. data/lib/xlsxrb/elements/styles.rb +157 -0
  11. data/lib/xlsxrb/elements/types.rb +95 -1
  12. data/lib/xlsxrb/elements/workbook.rb +121 -4
  13. data/lib/xlsxrb/elements/worksheet.rb +519 -29
  14. data/lib/xlsxrb/elements.rb +2 -0
  15. data/lib/xlsxrb/number_formatter.rb +429 -0
  16. data/lib/xlsxrb/ooxml/cfb.rb +1 -1
  17. data/lib/xlsxrb/ooxml/reader/listeners/core_listeners.rb +7 -5
  18. data/lib/xlsxrb/ooxml/reader/listeners/drawing_listeners.rb +11 -4
  19. data/lib/xlsxrb/ooxml/reader/listeners/feature_listeners.rb +7 -1
  20. data/lib/xlsxrb/ooxml/reader.rb +14 -17
  21. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +7 -1
  22. data/lib/xlsxrb/ooxml/styles_parser.rb +39 -9
  23. data/lib/xlsxrb/ooxml/utils.rb +34 -18
  24. data/lib/xlsxrb/ooxml/workbook_parser.rb +74 -16
  25. data/lib/xlsxrb/ooxml/workbook_writer.rb +82 -23
  26. data/lib/xlsxrb/ooxml/worksheet_parser.rb +319 -79
  27. data/lib/xlsxrb/ooxml/worksheet_writer.rb +263 -66
  28. data/lib/xlsxrb/ooxml/writer/features_xml.rb +5 -3
  29. data/lib/xlsxrb/ooxml/writer.rb +14 -0
  30. data/lib/xlsxrb/ooxml/xml_builder.rb +14 -2
  31. data/lib/xlsxrb/ooxml/zip_reader.rb +144 -28
  32. data/lib/xlsxrb/stream_row.rb +243 -16
  33. data/lib/xlsxrb/stream_sheet.rb +647 -16
  34. data/lib/xlsxrb/stream_writer.rb +375 -34
  35. data/lib/xlsxrb/style_builder.rb +308 -22
  36. data/lib/xlsxrb/utils.rb +211 -0
  37. data/lib/xlsxrb/version.rb +1 -1
  38. data/lib/xlsxrb/workbook_builder.rb +72 -11
  39. data/lib/xlsxrb/worksheet_builder.rb +738 -24
  40. data/lib/xlsxrb.rb +1073 -83
  41. data/sig/generated/xlsxrb/colors.rbs +40 -0
  42. data/sig/generated/xlsxrb/elements/cell.rbs +113 -8
  43. data/sig/generated/xlsxrb/elements/column.rbs +3 -2
  44. data/sig/generated/xlsxrb/elements/coordinate_access.rbs +18 -0
  45. data/sig/generated/xlsxrb/elements/image.rbs +82 -0
  46. data/sig/generated/xlsxrb/elements/row.rbs +83 -4
  47. data/sig/generated/xlsxrb/elements/styles.rbs +79 -0
  48. data/sig/generated/xlsxrb/elements/types.rbs +43 -0
  49. data/sig/generated/xlsxrb/elements/workbook.rbs +79 -2
  50. data/sig/generated/xlsxrb/elements/worksheet.rbs +282 -18
  51. data/sig/generated/xlsxrb/number_formatter.rbs +132 -0
  52. data/sig/generated/xlsxrb/ooxml/reader.rbs +6 -0
  53. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +2 -0
  54. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +5 -2
  55. data/sig/generated/xlsxrb/ooxml/utils.rbs +17 -8
  56. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +9 -3
  57. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +1 -1
  58. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +28 -6
  59. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +13 -3
  60. data/sig/generated/xlsxrb/ooxml/writer.rbs +2 -0
  61. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +3 -1
  62. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +13 -0
  63. data/sig/generated/xlsxrb/stream_row.rbs +100 -6
  64. data/sig/generated/xlsxrb/stream_sheet.rbs +358 -14
  65. data/sig/generated/xlsxrb/stream_writer.rbs +220 -30
  66. data/sig/generated/xlsxrb/style_builder.rbs +55 -18
  67. data/sig/generated/xlsxrb/utils.rbs +151 -0
  68. data/sig/generated/xlsxrb/workbook_builder.rbs +26 -0
  69. data/sig/generated/xlsxrb/worksheet_builder.rbs +356 -18
  70. data/sig/generated/xlsxrb.rbs +223 -27
  71. metadata +11 -1
@@ -26,14 +26,20 @@ module Xlsxrb
26
26
 
27
27
  INTEGER_STRINGS = (0..65_535).map(&:to_s).freeze
28
28
 
29
- def initialize(io)
29
+ def initialize(io, date1904: false)
30
30
  @io = io
31
+ @date1904 = date1904 ? true : false
31
32
  @builder = XmlBuilder.new(@io)
32
33
  @row_buffer = String.new(capacity: 65_536)
33
34
  @started = false
34
35
  @finished = false
35
36
  end
36
37
 
38
+ def set_column_style(col_index, style_id)
39
+ @column_styles ||= {}
40
+ @column_styles[col_index] = style_id
41
+ end
42
+
37
43
  # Write the worksheet header. Call once before writing rows.
38
44
  # Options for pre-sheetData elements:
39
45
  # sheet_properties: Hash of sheet-level properties (:tab_color, etc.)
@@ -41,15 +47,35 @@ module Xlsxrb
41
47
  # split_pane: { x_split:, y_split:, top_left_cell: }
42
48
  # selection: { active_cell:, sqref:, pane: }
43
49
  # sheet_view: Hash of sheet view properties
44
- def start(columns: [], sheet_properties: nil, freeze_pane: nil, split_pane: nil, selection: nil, sheet_view: nil)
50
+ def start(columns: [], sheet_properties: nil, freeze_pane: nil, split_pane: nil, selection: nil, sheet_view: nil, sheet_format: nil)
45
51
  return if @started
46
52
 
47
53
  @started = true
48
54
  @builder.declaration
49
55
  @builder.open_tag("worksheet", { xmlns: SSML_NS, "xmlns:r": DOC_REL_NS })
50
56
 
57
+ @column_styles ||= {}
58
+ if columns && !columns.empty?
59
+ columns.each do |col|
60
+ if col.is_a?(Elements::Column)
61
+ s = col.style_index
62
+ min = col.index
63
+ max = col.index
64
+ else
65
+ s = col[:style_index] || col[:style]
66
+ min = col[:index] || col[:min] || 0
67
+ max = col[:index] || col[:max] || col[:min] || 0
68
+ end
69
+ next unless s
70
+
71
+ (min..max).each { |i| @column_styles[i] = s }
72
+ end
73
+ end
74
+ @column_styles = nil if @column_styles.empty?
75
+
51
76
  write_sheet_properties(sheet_properties) if sheet_properties && !sheet_properties.empty?
52
77
  write_sheet_views(freeze_pane: freeze_pane, split_pane: split_pane, selection: selection, sheet_view: sheet_view) if freeze_pane || split_pane || selection || (sheet_view && !sheet_view.empty?)
78
+ write_sheet_format_pr(sheet_format) if sheet_format && !sheet_format.empty?
53
79
  write_columns(columns) unless columns.empty?
54
80
 
55
81
  @builder.open_tag("sheetData")
@@ -63,12 +89,14 @@ module Xlsxrb
63
89
  row_num_str = row_num.to_s
64
90
  buf = @row_buffer ||= String.new(capacity: 65_536)
65
91
  buf << '<row r="' << row_num_str << '"'
92
+ buf << ' s="' << attrs[:style_index].to_s << '" customFormat="1"' if attrs[:style_index].is_a?(Integer)
66
93
  if attrs[:height]
67
94
  buf << ' ht="' << attrs[:height].to_s << '" customHeight="1"'
68
95
  elsif attrs[:custom_height]
69
96
  buf << ' customHeight="1"'
70
97
  end
71
98
  buf << ' hidden="1"' if attrs[:hidden]
99
+ buf << ' collapsed="1"' if attrs[:collapsed]
72
100
  buf << ' outlineLevel="' << attrs[:outline_level].to_s << '"' if attrs[:outline_level]
73
101
  buf << ">"
74
102
 
@@ -77,25 +105,32 @@ module Xlsxrb
77
105
  value = cell.value
78
106
  style_id = cell.style_index
79
107
  col_ref = cell.ref || "#{column_letter(cell.column_index)}#{row_num_str}"
108
+ col_idx = cell.column_index
80
109
  formula = cell.formula
81
110
  formula_ca = false
82
111
  cell_type_val = nil
83
112
  elsif cell.is_a?(Hash)
84
113
  value = cell[:value]
85
114
  style_id = cell[:style_index]
86
- col_ref = cell[:ref] || "#{column_letter(cell[:column_index])}#{row_num_str}"
115
+ col_idx = cell[:column_index]
116
+ col_ref = cell[:ref] || (col_idx ? "#{column_letter(col_idx)}#{row_num_str}" : nil)
87
117
  formula = cell[:formula]
88
118
  formula_ca = cell[:formula_ca]
89
119
  cell_type_val = cell[:type]
90
120
  else
91
121
  value = cell
92
122
  style_id = nil
123
+ col_idx = nil
93
124
  col_ref = nil
94
125
  formula = nil
95
126
  formula_ca = false
96
127
  cell_type_val = nil
97
128
  end
98
129
 
130
+ col_style = @column_styles && col_idx ? @column_styles[col_idx] : nil
131
+ style_id ||= attrs[:style_index] || col_style
132
+ style_id = nil unless style_id.is_a?(Integer)
133
+
99
134
  # Fast path for common unstyled cells
100
135
  if !formula && !style_id && !cell_type_val
101
136
  case value
@@ -117,7 +152,7 @@ module Xlsxrb
117
152
  buf << '<c r="' << col_ref << '"/>'
118
153
  next
119
154
  when Date
120
- serial = Xlsxrb::Ooxml::Utils.date_to_serial(value)
155
+ serial = Xlsxrb::Ooxml::Utils.date_to_serial(value, date1904: @date1904)
121
156
  buf << '<c r="' << col_ref << '"><v>' << serial.to_s << "</v></c>"
122
157
  next
123
158
  end
@@ -137,13 +172,25 @@ module Xlsxrb
137
172
 
138
173
  xml_val = value
139
174
  type = cell_type_val
140
- formula_expr = nil
175
+ formula_type = nil
176
+ formula_ref = nil
177
+ formula_si = nil
141
178
 
142
179
  if formula
143
180
  if formula.is_a?(Xlsxrb::Elements::Formula)
144
181
  formula_expr = formula.expression
145
182
  formula_ca = formula.calculate_always
146
- xml_val = formula.cached_value || value || nil
183
+ formula_type = formula.type
184
+ formula_ref = formula.ref
185
+ formula_si = formula.shared_index
186
+ xml_val = formula.cached_value.nil? ? (value || nil) : formula.cached_value
187
+ elsif formula.is_a?(Hash)
188
+ formula_expr = formula[:formula] || formula[:expression]
189
+ formula_ca = formula[:calculate_always]
190
+ formula_type = formula[:type]
191
+ formula_ref = formula[:ref]
192
+ formula_si = formula[:si] || formula[:shared_index]
193
+ xml_val = formula[:value] || formula[:cached_value] || value || nil
147
194
  else
148
195
  formula_expr = formula
149
196
  xml_val = value || nil
@@ -175,9 +222,9 @@ module Xlsxrb
175
222
  xml_val = "0"
176
223
  type = "b"
177
224
  when Date
178
- xml_val = Xlsxrb::Ooxml::Utils.date_to_serial(value)
225
+ xml_val = Xlsxrb::Ooxml::Utils.date_to_serial(value, date1904: @date1904)
179
226
  when Time
180
- xml_val = Xlsxrb::Ooxml::Utils.datetime_to_serial(value)
227
+ xml_val = Xlsxrb::Ooxml::Utils.datetime_to_serial(value, date1904: @date1904)
181
228
  when BigDecimal
182
229
  xml_val = value.to_s("F")
183
230
  when Xlsxrb::Elements::CellError
@@ -229,16 +276,24 @@ module Xlsxrb
229
276
  buf << "><is><t>" << escape_xml(xml_val.to_s) << "</t></is></c>"
230
277
  end
231
278
  elsif formula_expr
232
- buf << if formula_ca
233
- '><f ca="1">'
234
- else
235
- "><f>"
236
- end
237
- buf << escape_xml(formula_expr) << "</f>"
279
+ f_attrs = +""
280
+ if formula_type
281
+ case formula_type.to_sym
282
+ when :array
283
+ f_attrs << ' t="array"'
284
+ f_attrs << %( ref="#{formula_ref}") if formula_ref
285
+ when :shared
286
+ f_attrs << ' t="shared"'
287
+ f_attrs << %( si="#{formula_si}") if formula_si
288
+ f_attrs << %( ref="#{formula_ref}") if formula_ref
289
+ end
290
+ end
291
+ f_attrs << ' ca="1"' if formula_ca
292
+ buf << "><f#{f_attrs}>" << escape_xml(formula_expr) << "</f>"
238
293
  if xml_val
239
294
  buf << "<v>" << xml_val.to_s << "</v></c>"
240
295
  else
241
- buf << "</c>"
296
+ buf << "<v>0</v></c>"
242
297
  end
243
298
  else
244
299
  buf << "><v>" << xml_val.to_s << "</v></c>"
@@ -262,7 +317,7 @@ module Xlsxrb
262
317
  row_num_str = row_num < 65_536 ? INTEGER_STRINGS[row_num] : row_num.to_s
263
318
  buf = @row_buffer ||= String.new(capacity: 65_536)
264
319
 
265
- if styles.nil? && attrs.nil?
320
+ if styles.nil? && attrs.nil? && @column_styles.nil?
266
321
  buf << "<row r=\"#{row_num_str}\">"
267
322
  col_index = 0
268
323
  max_len = values.length
@@ -292,7 +347,7 @@ module Xlsxrb
292
347
  next
293
348
  end
294
349
  formula_expr = value[1..]
295
- buf << "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f></c>"
350
+ buf << "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f><v>0</v></c>"
296
351
  when true
297
352
  buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"b\"><v>1</v></c>"
298
353
  when false
@@ -300,35 +355,54 @@ module Xlsxrb
300
355
  when Date
301
356
  date_style_id = style_map ? style_map["__xlsxrb_date"] : nil
302
357
  buf << if date_style_id
303
- "<c r=\"#{col_ref}#{row_num_str}\" s=\"#{date_style_id}\"><v>#{Xlsxrb::Ooxml::Utils.date_to_serial(value)}</v></c>"
358
+ "<c r=\"#{col_ref}#{row_num_str}\" s=\"#{date_style_id}\"><v>#{Xlsxrb::Ooxml::Utils.date_to_serial(value, date1904: @date1904)}</v></c>"
304
359
  else
305
- "<c r=\"#{col_ref}#{row_num_str}\"><v>#{Xlsxrb::Ooxml::Utils.date_to_serial(value)}</v></c>"
360
+ "<c r=\"#{col_ref}#{row_num_str}\"><v>#{Xlsxrb::Ooxml::Utils.date_to_serial(value, date1904: @date1904)}</v></c>"
306
361
  end
307
362
  when Time
308
363
  time_style_id = style_map ? style_map["__xlsxrb_time"] : nil
309
364
  buf << if time_style_id
310
- "<c r=\"#{col_ref}#{row_num_str}\" s=\"#{time_style_id}\"><v>#{Xlsxrb::Ooxml::Utils.datetime_to_serial(value)}</v></c>"
365
+ "<c r=\"#{col_ref}#{row_num_str}\" s=\"#{time_style_id}\"><v>#{Xlsxrb::Ooxml::Utils.datetime_to_serial(value, date1904: @date1904)}</v></c>"
311
366
  else
312
- "<c r=\"#{col_ref}#{row_num_str}\"><v>#{Xlsxrb::Ooxml::Utils.datetime_to_serial(value)}</v></c>"
367
+ "<c r=\"#{col_ref}#{row_num_str}\"><v>#{Xlsxrb::Ooxml::Utils.datetime_to_serial(value, date1904: @date1904)}</v></c>"
313
368
  end
314
369
  when Xlsxrb::Elements::Formula
315
370
  formula_expr = value.expression
316
371
  formula_expr = formula_expr[1..] if formula_expr.start_with?("=")
317
- buf << if value.cached_value
318
- "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f><v>#{value.cached_value}</v></c>"
319
- else
320
- "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f></c>"
321
- end
372
+ f_attrs = +""
373
+ if value.type
374
+ case value.type.to_sym
375
+ when :array
376
+ f_attrs << ' t="array"'
377
+ f_attrs << %( ref="#{value.ref}") if value.ref
378
+ when :shared
379
+ f_attrs << ' t="shared"'
380
+ f_attrs << %( si="#{value.shared_index}") if value.shared_index
381
+ f_attrs << %( ref="#{value.ref}") if value.ref
382
+ end
383
+ end
384
+ f_attrs << ' ca="1"' if value.calculate_always
385
+ v_val = value.cached_value.nil? ? 0 : value.cached_value
386
+ buf << "<c r=\"#{col_ref}#{row_num_str}\"><f#{f_attrs}>#{escape_xml(formula_expr)}</f><v>#{v_val}</v></c>"
322
387
  when Hash
323
388
  if value.key?(:formula)
324
- formula_expr = value[:formula]
389
+ formula_expr = value[:formula] || value[:expression]
325
390
  formula_expr = formula_expr[1..] if formula_expr.start_with?("=")
326
- xml_val = value[:value]
327
- buf << if xml_val
328
- "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f><v>#{xml_val}</v></c>"
329
- else
330
- "<c r=\"#{col_ref}#{row_num_str}\"><f>#{escape_xml(formula_expr)}</f></c>"
331
- end
391
+ f_attrs = +""
392
+ if value[:type]
393
+ case value[:type].to_sym
394
+ when :array
395
+ f_attrs << ' t="array"'
396
+ f_attrs << %( ref="#{value[:ref]}") if value[:ref]
397
+ when :shared
398
+ f_attrs << ' t="shared"'
399
+ f_attrs << %( si="#{value[:si] || value[:shared_index]}") if value[:si] || value[:shared_index]
400
+ f_attrs << %( ref="#{value[:ref]}") if value[:ref]
401
+ end
402
+ end
403
+ f_attrs << ' ca="1"' if value[:calculate_always]
404
+ xml_val = value[:value] || value[:cached_value] || 0
405
+ buf << "<c r=\"#{col_ref}#{row_num_str}\"><f#{f_attrs}>#{escape_xml(formula_expr)}</f><v>#{xml_val}</v></c>"
332
406
  end
333
407
  when Xlsxrb::Elements::CellError
334
408
  buf << "<c r=\"#{col_ref}#{row_num_str}\" t=\"e\"><v>#{value.code}</v></c>"
@@ -353,13 +427,13 @@ module Xlsxrb
353
427
  end
354
428
 
355
429
  is_styles_collection = styles && (styles.is_a?(Array) || styles.is_a?(Hash))
356
- single_style_id = nil
357
- single_style_id = style_map[styles] if styles && style_map && !is_styles_collection
430
+ single_style_id = resolve_style_id(styles, style_map) unless is_styles_collection
358
431
 
359
432
  buf << '<row r="' << row_num_str << '"'
360
433
  if attrs
361
434
  buf << ' ht="' << attrs[:height].to_s << '" customHeight="1"' if attrs[:height]
362
435
  buf << ' hidden="1"' if attrs[:hidden]
436
+ buf << ' collapsed="1"' if attrs[:collapsed]
363
437
  buf << ' outlineLevel="' << attrs[:outline_level].to_s << '"' if attrs[:outline_level]
364
438
  end
365
439
  buf << ">"
@@ -370,17 +444,26 @@ module Xlsxrb
370
444
  max_len = [max_len, styles_len].max
371
445
  end
372
446
 
447
+ row_style = attrs ? attrs[:style_index] : nil
448
+ row_style_id = resolve_style_id(row_style, style_map)
449
+
373
450
  col_index = 0
374
451
  while col_index < max_len
375
452
  value = col_index < values.length ? values[col_index] : nil
376
453
  style_id = single_style_id
377
- if is_styles_collection && style_map
454
+ if is_styles_collection
378
455
  style_name = if styles.is_a?(Array)
379
456
  col_index < styles.length ? styles[col_index] : nil
380
457
  else
381
458
  styles[col_index]
382
459
  end
383
- style_id = style_map[style_name] if style_name
460
+ style_id = resolve_style_id(style_name, style_map)
461
+ end
462
+
463
+ if style_id.nil?
464
+ col_style = @column_styles ? @column_styles[col_index] : nil
465
+ col_style_id = resolve_style_id(col_style, style_map)
466
+ style_id = row_style_id || col_style_id
384
467
  end
385
468
 
386
469
  col_ref = COLUMN_LETTERS[col_index] || column_letter(col_index)
@@ -425,11 +508,11 @@ module Xlsxrb
425
508
  col_index += 1
426
509
  next
427
510
  when Date
428
- buf << '<c r="' << col_ref << row_num_str << '"><v>' << Xlsxrb::Ooxml::Utils.date_to_serial(value).to_s << "</v></c>"
511
+ buf << '<c r="' << col_ref << row_num_str << '"><v>' << Xlsxrb::Ooxml::Utils.date_to_serial(value, date1904: @date1904).to_s << "</v></c>"
429
512
  col_index += 1
430
513
  next
431
514
  when Time
432
- buf << '<c r="' << col_ref << row_num_str << '"><v>' << Xlsxrb::Ooxml::Utils.datetime_to_serial(value).to_s << "</v></c>"
515
+ buf << '<c r="' << col_ref << row_num_str << '"><v>' << Xlsxrb::Ooxml::Utils.datetime_to_serial(value, date1904: @date1904).to_s << "</v></c>"
433
516
  col_index += 1
434
517
  next
435
518
  end
@@ -438,6 +521,9 @@ module Xlsxrb
438
521
  # General path: styled, formula, rich text, or complex cell
439
522
  formula_expr = nil
440
523
  formula_ca = false
524
+ formula_type = nil
525
+ formula_ref = nil
526
+ formula_si = nil
441
527
  xml_val = value
442
528
  type = nil
443
529
 
@@ -445,6 +531,9 @@ module Xlsxrb
445
531
  when Xlsxrb::Elements::Formula
446
532
  formula_expr = value.expression
447
533
  formula_ca = value.calculate_always
534
+ formula_type = value.type
535
+ formula_ref = value.ref
536
+ formula_si = value.shared_index
448
537
  xml_val = value.cached_value
449
538
  case value.cached_value
450
539
  when String
@@ -458,9 +547,12 @@ module Xlsxrb
458
547
  end
459
548
  when Hash
460
549
  if value.key?(:formula)
461
- formula_expr = value[:formula]
550
+ formula_expr = value[:formula] || value[:expression]
462
551
  formula_ca = value[:calculate_always] || false
463
- xml_val = value[:value]
552
+ formula_type = value[:type]
553
+ formula_ref = value[:ref]
554
+ formula_si = value[:si] || value[:shared_index]
555
+ xml_val = value[:value] || value[:cached_value]
464
556
  case xml_val
465
557
  when String
466
558
  type = "str"
@@ -476,7 +568,7 @@ module Xlsxrb
476
568
  if value.start_with?("=") && value.length > 1
477
569
  formula_expr = value
478
570
  xml_val = nil
479
- else
571
+ elsif sst_index
480
572
  idx = sst_index[value]
481
573
  unless idx
482
574
  sst << value
@@ -485,16 +577,24 @@ module Xlsxrb
485
577
  end
486
578
  xml_val = idx
487
579
  type = "s"
580
+ else
581
+ xml_val = value
582
+ type = "inlineStr"
488
583
  end
489
584
  when Xlsxrb::Elements::RichText
490
- idx = sst_index[value]
491
- unless idx
492
- sst << value
493
- idx = sst.size - 1
494
- sst_index[value] = idx
585
+ if sst_index
586
+ idx = sst_index[value]
587
+ unless idx
588
+ sst << value
589
+ idx = sst.size - 1
590
+ sst_index[value] = idx
591
+ end
592
+ xml_val = idx
593
+ type = "s"
594
+ else
595
+ xml_val = value
596
+ type = "inlineStr"
495
597
  end
496
- xml_val = idx
497
- type = "s"
498
598
  when true
499
599
  xml_val = "1"
500
600
  type = "b"
@@ -502,9 +602,9 @@ module Xlsxrb
502
602
  xml_val = "0"
503
603
  type = "b"
504
604
  when Date
505
- xml_val = Xlsxrb::Ooxml::Utils.date_to_serial(value)
605
+ xml_val = Xlsxrb::Ooxml::Utils.date_to_serial(value, date1904: @date1904)
506
606
  when Time
507
- xml_val = Xlsxrb::Ooxml::Utils.datetime_to_serial(value)
607
+ xml_val = Xlsxrb::Ooxml::Utils.datetime_to_serial(value, date1904: @date1904)
508
608
  when BigDecimal
509
609
  xml_val = value.to_s("F")
510
610
  when Xlsxrb::Elements::CellError
@@ -518,17 +618,27 @@ module Xlsxrb
518
618
  buf << ' s="' << style_id.to_s << '"' if style_id
519
619
  buf << ' t="' << type << '"' if type
520
620
  if formula_expr
521
- buf << if formula_ca
522
- '><f ca="1">'
523
- else
524
- "><f>"
525
- end
526
- buf << escape_xml(formula_expr) << "</f>"
621
+ f_attrs = +""
622
+ if formula_type
623
+ case formula_type.to_sym
624
+ when :array
625
+ f_attrs << ' t="array"'
626
+ f_attrs << %( ref="#{formula_ref}") if formula_ref
627
+ when :shared
628
+ f_attrs << ' t="shared"'
629
+ f_attrs << %( si="#{formula_si}") if formula_si
630
+ f_attrs << %( ref="#{formula_ref}") if formula_ref
631
+ end
632
+ end
633
+ f_attrs << ' ca="1"' if formula_ca
634
+ buf << "><f#{f_attrs}>" << escape_xml(formula_expr) << "</f>"
527
635
  if xml_val
528
636
  buf << "<v>" << xml_val.to_s << "</v></c>"
529
637
  else
530
- buf << "</c>"
638
+ buf << "<v>0</v></c>"
531
639
  end
640
+ elsif type == "inlineStr"
641
+ buf << "><is><t>" << escape_xml(xml_val.to_s) << "</t></is></c>"
532
642
  else
533
643
  buf << "><v>" << xml_val.to_s << "</v></c>"
534
644
  end
@@ -545,12 +655,12 @@ module Xlsxrb
545
655
 
546
656
  # Write the worksheet footer. Call once after all rows.
547
657
  # Options for post-sheetData elements (in OOXML order):
548
- def finish(drawing_rid: nil, sheet_protection: nil, auto_filter: nil,
658
+ def finish(drawing_rid: nil, sheet_protection: nil, protected_ranges: nil, auto_filter: nil,
549
659
  filter_columns: nil, sort_state: nil, merge_cells: nil,
550
660
  conditional_formats: nil, data_validations: nil,
551
661
  hyperlinks: nil, print_options: nil, page_margins: nil,
552
662
  page_setup: nil, header_footer: nil, row_breaks: nil,
553
- col_breaks: nil, tables: nil, table_start_rid: nil,
663
+ col_breaks: nil, cell_watches: nil, tables: nil, table_start_rid: nil,
554
664
  legacy_drawing_rid: nil, sparkline_groups: nil)
555
665
  return if @finished
556
666
 
@@ -564,6 +674,7 @@ module Xlsxrb
564
674
 
565
675
  # Elements must appear in OOXML specification order after sheetData
566
676
  write_sheet_protection(sheet_protection) if sheet_protection
677
+ write_protected_ranges(protected_ranges) if protected_ranges && !protected_ranges.empty?
567
678
  write_auto_filter(auto_filter, filter_columns, sort_state) if auto_filter
568
679
  write_merge_cells(merge_cells) if merge_cells && !merge_cells.empty?
569
680
  write_conditional_formatting(conditional_formats) if conditional_formats && !conditional_formats.empty?
@@ -575,6 +686,7 @@ module Xlsxrb
575
686
  write_header_footer(header_footer) if header_footer && !header_footer.empty?
576
687
  write_row_breaks(row_breaks) if row_breaks && !row_breaks.empty?
577
688
  write_col_breaks(col_breaks) if col_breaks && !col_breaks.empty?
689
+ write_cell_watches(cell_watches) if cell_watches && !cell_watches.empty?
578
690
  @builder.empty_tag("drawing", { "r:id": drawing_rid }) if drawing_rid
579
691
  @builder.empty_tag("legacyDrawing", { "r:id": legacy_drawing_rid }) if legacy_drawing_rid
580
692
  write_table_parts(tables, table_start_rid) if tables && !tables.empty?
@@ -584,6 +696,17 @@ module Xlsxrb
584
696
 
585
697
  private
586
698
 
699
+ def resolve_style_id(style, style_map)
700
+ return nil if style.nil?
701
+
702
+ if style_map
703
+ mapped = style_map[style] || style_map[style.to_s]
704
+ return mapped if mapped
705
+ end
706
+
707
+ style.is_a?(Integer) ? style : nil
708
+ end
709
+
587
710
  # --- Pre-sheetData elements ---
588
711
 
589
712
  def write_sheet_properties(props)
@@ -653,6 +776,22 @@ module Xlsxrb
653
776
  @builder.close_tag("sheetViews")
654
777
  end
655
778
 
779
+ def write_sheet_format_pr(fmt)
780
+ return if fmt.nil? || fmt.empty?
781
+
782
+ attrs = {}
783
+ attrs[:defaultRowHeight] = fmt[:default_row_height].to_s if fmt[:default_row_height]
784
+ attrs[:defaultColWidth] = fmt[:default_col_width].to_s if fmt[:default_col_width]
785
+ attrs[:baseColWidth] = fmt[:base_col_width].to_s if fmt[:base_col_width]
786
+ attrs[:outlineLevelRow] = fmt[:outline_level_row].to_s if fmt[:outline_level_row]
787
+ attrs[:outlineLevelCol] = fmt[:outline_level_col].to_s if fmt[:outline_level_col]
788
+ attrs[:customHeight] = "1" if fmt[:custom_height]
789
+ attrs[:zeroHeight] = "1" if fmt[:zero_height]
790
+ attrs[:thickTop] = "1" if fmt[:thick_top]
791
+ attrs[:thickBottom] = "1" if fmt[:thick_bottom]
792
+ @builder.empty_tag("sheetFormatPr", attrs) unless attrs.empty?
793
+ end
794
+
656
795
  # --- Post-sheetData elements (in OOXML order) ---
657
796
 
658
797
  def write_sheet_protection(opts)
@@ -681,6 +820,28 @@ module Xlsxrb
681
820
  @builder.empty_tag("sheetProtection", attrs)
682
821
  end
683
822
 
823
+ def write_protected_ranges(ranges)
824
+ @builder.open_tag("protectedRanges")
825
+ ranges.each do |pr|
826
+ attrs = { sqref: pr[:sqref].to_s, name: pr[:name].to_s }
827
+ attrs[:password] = pr[:password].to_s if pr[:password]
828
+ attrs[:algorithmName] = pr[:algorithm_name].to_s if pr[:algorithm_name]
829
+ attrs[:hashValue] = pr[:hash_value].to_s if pr[:hash_value]
830
+ attrs[:saltValue] = pr[:salt_value].to_s if pr[:salt_value]
831
+ attrs[:spinCount] = pr[:spin_count].to_s if pr[:spin_count]
832
+ attrs[:securityDescriptor] = pr[:security_descriptor].to_s if pr[:security_descriptor]
833
+ sds = pr[:security_descriptors] || []
834
+ if sds.empty?
835
+ @builder.empty_tag("protectedRange", attrs)
836
+ else
837
+ @builder.open_tag("protectedRange", attrs)
838
+ sds.each { |sd| @builder.tag("securityDescriptor") { @builder.text(sd.to_s) } }
839
+ @builder.close_tag("protectedRange")
840
+ end
841
+ end
842
+ @builder.close_tag("protectedRanges")
843
+ end
844
+
684
845
  def write_auto_filter(range, filter_columns, sort_state)
685
846
  if (filter_columns && !filter_columns.empty?) || sort_state
686
847
  @builder.open_tag("autoFilter", { ref: range })
@@ -945,7 +1106,8 @@ module Xlsxrb
945
1106
  def write_hyperlinks(links)
946
1107
  @builder.open_tag("hyperlinks")
947
1108
  links.each_with_index do |link, idx|
948
- h_attrs = { ref: link[:cell] }
1109
+ cell_ref = link[:cell] || link[:ref]
1110
+ h_attrs = { ref: cell_ref }
949
1111
  h_attrs[:"r:id"] = "rId#{link[:_rid] || (idx + 1)}" if link[:url]
950
1112
  h_attrs[:location] = link[:location] if link[:location]
951
1113
  h_attrs[:display] = link[:display] if link[:display]
@@ -1015,6 +1177,14 @@ module Xlsxrb
1015
1177
  @builder.close_tag("colBreaks")
1016
1178
  end
1017
1179
 
1180
+ def write_cell_watches(watches)
1181
+ @builder.open_tag("cellWatches")
1182
+ watches.each do |w|
1183
+ @builder.empty_tag("cellWatch", { r: w.to_s })
1184
+ end
1185
+ @builder.close_tag("cellWatches")
1186
+ end
1187
+
1018
1188
  def write_table_parts(tables, start_rid)
1019
1189
  @builder.open_tag("tableParts", { count: tables.size.to_s })
1020
1190
  tables.each_with_index do |_tbl, idx|
@@ -1106,6 +1276,8 @@ module Xlsxrb
1106
1276
  attrs[:hidden] = "1" if col[:hidden]
1107
1277
  attrs[:customWidth] = "1" if col[:custom_width] || col[:width]
1108
1278
  attrs[:outlineLevel] = col[:outline_level].to_s if col[:outline_level]
1279
+ style_id = col[:style_index] || col[:style]
1280
+ attrs[:style] = style_id.to_s if style_id
1109
1281
  @builder.empty_tag("col", attrs)
1110
1282
  end
1111
1283
  @builder.close_tag("cols")
@@ -1120,7 +1292,10 @@ module Xlsxrb
1120
1292
  # For formula cells with cached values, don't infer type from value
1121
1293
  type = formula ? cell[:type] : (cell[:type] || cell_type(value))
1122
1294
  attrs[:t] = type if type
1123
- attrs[:s] = cell[:style_index].to_s if cell[:style_index]
1295
+ col_idx = cell[:column_index]
1296
+ col_style = @column_styles && col_idx ? @column_styles[col_idx] : nil
1297
+ style_id = cell[:style_index] || col_style
1298
+ attrs[:s] = style_id.to_s if style_id.is_a?(Integer)
1124
1299
 
1125
1300
  if value.nil? && formula.nil?
1126
1301
  @builder.empty_tag("c", attrs)
@@ -1130,14 +1305,36 @@ module Xlsxrb
1130
1305
  @builder.open_tag("c", attrs)
1131
1306
  if formula
1132
1307
  f_attrs = {}
1133
- f_attrs[:ca] = "1" if cell[:formula_ca]
1308
+ formula_text = formula
1309
+ if formula.is_a?(Xlsxrb::Elements::Formula)
1310
+ formula_text = formula.expression
1311
+ formula_text = formula_text[1..] if formula_text.start_with?("=")
1312
+ f_attrs[:t] = formula.type.to_s if formula.type
1313
+ f_attrs[:ref] = formula.ref if formula.ref
1314
+ f_attrs[:si] = formula.shared_index.to_s if formula.shared_index
1315
+ f_attrs[:ca] = "1" if formula.calculate_always
1316
+ else
1317
+ formula_text = formula_text[1..] if formula_text.is_a?(String) && formula_text.start_with?("=")
1318
+ f_type = cell[:formula_type] || cell[:type]
1319
+ if f_type && %i[array shared].include?(f_type.to_sym)
1320
+ f_attrs[:t] = f_type.to_s
1321
+ f_attrs[:ref] = cell[:formula_ref] || cell[:ref] if cell[:formula_ref] || (f_type.to_s == "array" && cell[:ref])
1322
+ f_attrs[:si] = cell[:formula_si].to_s if cell[:formula_si]
1323
+ end
1324
+ f_attrs[:ca] = "1" if cell[:formula_ca]
1325
+ end
1134
1326
  if f_attrs.empty?
1135
- @builder.tag("f") { |b| b.text(formula) }
1327
+ @builder.tag("f") { |b| b.text(formula_text) }
1136
1328
  else
1137
- @builder.tag("f", f_attrs) { |b| b.text(formula) }
1329
+ @builder.tag("f", f_attrs) { |b| b.text(formula_text) }
1138
1330
  end
1139
1331
  end
1140
- @builder.tag("v") { |b| b.text(xml_cell_value(value, type)) } unless value.nil?
1332
+ v_val = if value.nil?
1333
+ formula ? "0" : nil
1334
+ else
1335
+ xml_cell_value(value, type)
1336
+ end
1337
+ @builder.tag("v") { |b| b.text(v_val) } unless v_val.nil?
1141
1338
  @builder.close_tag("c")
1142
1339
  end
1143
1340
 
@@ -393,11 +393,13 @@ module Xlsxrb
393
393
  when true, false
394
394
  %(<c r="#{cell_ref}" t="b"#{s_attr}#{ph_attr}><v>#{value ? 1 : 0}</v></c>)
395
395
  when Time
396
- serial = Xlsxrb::Ooxml::Utils.datetime_to_serial(value)
396
+ date1904 = @workbook_properties&.[](:date1904) ? true : false
397
+ serial = Xlsxrb::Ooxml::Utils.datetime_to_serial(value, date1904: date1904)
397
398
  dt_attr = s_attr.empty? && (dt_style = resolve_style_index(datetime_num_fmt_id)) ? %( s="#{dt_style}") : s_attr
398
399
  %(<c r="#{cell_ref}"#{dt_attr}#{ph_attr}><v>#{serial}</v></c>)
399
400
  when Date
400
- serial = Xlsxrb::Ooxml::Utils.date_to_serial(value)
401
+ date1904 = @workbook_properties&.[](:date1904) ? true : false
402
+ serial = Xlsxrb::Ooxml::Utils.date_to_serial(value, date1904: date1904)
401
403
  ds_attr = s_attr.empty? && (date_style = resolve_style_index(date_num_fmt_id)) ? %( s="#{date_style}") : s_attr
402
404
  %(<c r="#{cell_ref}"#{ds_attr}#{ph_attr}><v>#{serial}</v></c>)
403
405
  when Numeric
@@ -713,7 +715,7 @@ module Xlsxrb
713
715
  "<vt:i4>#{prop[:value]}</vt:i4>"
714
716
  when :float, :r8
715
717
  "<vt:r8>#{prop[:value]}</vt:r8>"
716
- when :bool
718
+ when :bool, :boolean
717
719
  "<vt:bool>#{prop[:value] ? "true" : "false"}</vt:bool>"
718
720
  when :date, :filetime
719
721
  "<vt:filetime>#{prop[:value]}</vt:filetime>"