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
@@ -19,7 +19,7 @@ module Xlsxrb
19
19
  class StreamRow
20
20
  [Enumerable].each { |m| include m }
21
21
 
22
- attr_reader :index, :height, :hidden, :custom_height, :outline_level
22
+ attr_reader :index, :height, :hidden, :custom_height, :collapsed, :outline_level, :style_index, :pad_empty_cells
23
23
 
24
24
  # @param index [Integer] 0-based row index.
25
25
  # @param xml_bytes [String] Raw ASCII-8BIT XML bytes.
@@ -30,10 +30,14 @@ module Xlsxrb
30
30
  # @param height [Float, Integer, nil] Row height in points.
31
31
  # @param hidden [Boolean] Whether the row is hidden.
32
32
  # @param custom_height [Boolean] Whether custom height is set.
33
+ # @param collapsed [Boolean] Whether the row is collapsed.
33
34
  # @param outline_level [Integer, nil] Grouping/outline level.
34
- #: (index: Integer, xml_bytes: String, from: Integer, to: Integer, shared_strings: Array[String], ?prefix: String, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?outline_level: Integer | nil) -> void
35
+ # @param style_index [Integer, nil] Style index.
36
+ # @param date1904 [Boolean] Whether the 1904 date system is active.
37
+ # @param pad_empty_cells [Boolean] Whether missing cells in this row are padded.
38
+ #: (index: Integer, xml_bytes: String, from: Integer, to: Integer, shared_strings: Array[String], ?prefix: String, ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?collapsed: bool, ?outline_level: Integer | nil, ?style_index: Integer | nil, ?styles: Hash[untyped, untyped]?, ?date1904: bool, ?pad_empty_cells: bool) -> void
35
39
  def initialize(index:, xml_bytes:, from:, to:, shared_strings:, prefix: "", height: nil, hidden: false,
36
- custom_height: false, outline_level: nil)
40
+ custom_height: false, collapsed: false, outline_level: nil, style_index: nil, styles: nil, date1904: false, pad_empty_cells: false)
37
41
  @index = index
38
42
  @xml = xml_bytes
39
43
  @from = from
@@ -43,12 +47,17 @@ module Xlsxrb
43
47
  @height = height
44
48
  @hidden = hidden
45
49
  @custom_height = custom_height
50
+ @collapsed = collapsed
46
51
  @outline_level = outline_level
52
+ @style_index = style_index
53
+ @styles = styles
54
+ @date1904 = date1904 ? true : false
55
+ @pad_empty_cells = pad_empty_cells ? true : false
47
56
  @cells = nil
48
57
  end
49
58
 
50
59
  # rubocop:disable Style/OptionalBooleanParameter
51
- def self.fast_create(index, xml_bytes, from, to, shared_strings, prefix = "", height = nil, hidden = false, custom_height = false, outline_level = nil)
60
+ def self.fast_create(index, xml_bytes, from, to, shared_strings, prefix = "", height = nil, hidden = false, custom_height = false, outline_level = nil, style_index = nil, styles = nil, date1904 = false, collapsed = false, pad_empty_cells = false)
52
61
  inst = allocate
53
62
  inst.instance_variable_set(:@index, index)
54
63
  inst.instance_variable_set(:@xml, xml_bytes)
@@ -59,12 +68,80 @@ module Xlsxrb
59
68
  inst.instance_variable_set(:@height, height)
60
69
  inst.instance_variable_set(:@hidden, hidden)
61
70
  inst.instance_variable_set(:@custom_height, custom_height)
71
+ inst.instance_variable_set(:@collapsed, collapsed)
62
72
  inst.instance_variable_set(:@outline_level, outline_level)
73
+ inst.instance_variable_set(:@style_index, style_index)
74
+ inst.instance_variable_set(:@styles, styles)
75
+ inst.instance_variable_set(:@date1904, date1904 ? true : false)
76
+ inst.instance_variable_set(:@pad_empty_cells, pad_empty_cells ? true : false)
63
77
  inst.instance_variable_set(:@cells, nil)
64
78
  inst
65
79
  end
66
80
  # rubocop:enable Style/OptionalBooleanParameter
67
81
 
82
+ # Returns whether the row is hidden.
83
+ #
84
+ # @return [Boolean]
85
+ # @api public
86
+ #: () -> bool
87
+ def hidden?
88
+ @hidden == true
89
+ end
90
+
91
+ # Returns whether the row is collapsed.
92
+ #
93
+ # @return [Boolean]
94
+ # @api public
95
+ #: () -> bool
96
+ def collapsed?
97
+ @collapsed == true
98
+ end
99
+
100
+ # Returns whether custom height is set.
101
+ #
102
+ # @return [Boolean]
103
+ # @api public
104
+ #: () -> bool
105
+ def custom_height?
106
+ @custom_height == true
107
+ end
108
+
109
+ # Returns a frozen Hash of row attributes.
110
+ #
111
+ # @return [Hash{Symbol => Object}]
112
+ # @api public
113
+ #: () -> Hash[Symbol, untyped]
114
+ def attributes
115
+ h = {
116
+ index: @index,
117
+ height: @height,
118
+ hidden: @hidden,
119
+ custom_height: @custom_height,
120
+ collapsed: @collapsed,
121
+ outline_level: @outline_level
122
+ }
123
+ h[:style_index] = @style_index if @style_index
124
+ h.freeze
125
+ end
126
+
127
+ # Returns whether the row uses the 1904 date system.
128
+ #
129
+ # @return [Boolean]
130
+ # @api public
131
+ #: () -> bool
132
+ def date1904?
133
+ @date1904 ? true : false
134
+ end
135
+
136
+ # Returns whether missing cells in this row are padded.
137
+ #
138
+ # @return [Boolean]
139
+ # @api public
140
+ #: () -> bool
141
+ def pad_empty_cells?
142
+ @pad_empty_cells ? true : false
143
+ end
144
+
68
145
  # Iterate over cells in this streaming row one by one.
69
146
  #
70
147
  # @yield [cell]
@@ -76,10 +153,10 @@ module Xlsxrb
76
153
  def each_cell(&block)
77
154
  return enum_for(:each_cell) unless block
78
155
 
79
- if @cells
80
- @cells.each(&block)
156
+ if @cells || @pad_empty_cells
157
+ cells.each(&block)
81
158
  else
82
- Ooxml::WorksheetParser.fast_scan_cells_direct(@xml, @from, @to, @shared_strings, @index, @prefix, &block)
159
+ Ooxml::WorksheetParser.fast_scan_cells_direct(@xml, @from, @to, @shared_strings, @index, @prefix, @styles, @date1904, &block)
83
160
  end
84
161
  end
85
162
 
@@ -103,9 +180,21 @@ module Xlsxrb
103
180
  def cells
104
181
  @cells ||= begin
105
182
  arr = []
106
- Ooxml::WorksheetParser.fast_scan_cells_direct(@xml, @from, @to, @shared_strings, @index, @prefix) do |c|
183
+ Ooxml::WorksheetParser.fast_scan_cells_direct(@xml, @from, @to, @shared_strings, @index, @prefix, @styles, @date1904) do |c|
107
184
  arr << c
108
185
  end
186
+ if @pad_empty_cells && !arr.empty?
187
+ max_col = arr.map(&:column_index).max || 0
188
+ if arr.size < max_col + 1
189
+ cell_map = {}
190
+ arr.each { |c| cell_map[c.column_index] = c }
191
+ padded = Array.new(max_col + 1)
192
+ (0..max_col).each do |c_idx|
193
+ padded[c_idx] = cell_map[c_idx] || Elements::Cell.fast_create(@index, c_idx, nil, nil, nil, nil, nil, @date1904)
194
+ end
195
+ arr = padded
196
+ end
197
+ end
109
198
  arr.freeze
110
199
  end
111
200
  end
@@ -121,12 +210,28 @@ module Xlsxrb
121
210
  when Symbol
122
211
  case col_index
123
212
  when :cells then cells
213
+ when :values then values
214
+ when :raw_values then raw_values
215
+ when :formatted_values then formatted_values
216
+ when :formulas then formulas
124
217
  when :index then index
125
218
  when :height then height
126
219
  when :hidden then hidden
127
220
  when :custom_height then custom_height
221
+ when :collapsed then collapsed
128
222
  when :outline_level then outline_level
129
- when :attrs then { height: height, hidden: hidden, custom_height: custom_height, outline_level: outline_level }
223
+ when :style_index then style_index
224
+ when :date1904 then date1904?
225
+ when :hidden? then hidden?
226
+ when :collapsed? then collapsed?
227
+ when :custom_height? then custom_height?
228
+ when :pad_empty_cells then @pad_empty_cells
229
+ when :pad_empty_cells? then pad_empty_cells?
230
+ when :attributes then attributes
231
+ when :attrs
232
+ h = { height: height, hidden: hidden, custom_height: custom_height, collapsed: collapsed, outline_level: outline_level }
233
+ h[:style_index] = style_index if style_index
234
+ h
130
235
  end
131
236
  else
132
237
  cells[col_index]
@@ -143,29 +248,107 @@ module Xlsxrb
143
248
  cells.find { |c| c.column_index == col_index }
144
249
  end
145
250
 
251
+ # Convert row cells to a coordinate-keyed Hash (e.g. {"A1" => "value", "B1" => 123}).
252
+ #
253
+ # @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
254
+ # @return [Hash{String => Object}]
255
+ # @api public
256
+ #: (?type_cast: bool) -> Hash[String, untyped]
257
+ def to_h(type_cast: false)
258
+ result = {}
259
+ each_cell do |c|
260
+ val = c.value
261
+ if type_cast && val.is_a?(Numeric)
262
+ fmt_type = if c.style_index && @styles
263
+ NumberFormatter.format_type_for(c.style_index, @styles)
264
+ elsif c.format_code
265
+ NumberFormatter.format_type(c.format_code)
266
+ end
267
+ begin
268
+ case fmt_type
269
+ when :date
270
+ val = Ooxml::Utils.serial_to_date(val, date1904: @date1904)
271
+ when :datetime, :time
272
+ val = Ooxml::Utils.serial_to_datetime(val, date1904: @date1904)
273
+ end
274
+ rescue StandardError
275
+ # Keep val numeric on failure
276
+ end
277
+ end
278
+ result[c.ref] = val
279
+ end
280
+ result
281
+ end
282
+
146
283
  # Convert row cells to an Array of raw values (sparse columns get nil).
147
284
  #
148
285
  # @return [Array<Object>]
149
286
  # @api public
150
287
  #: () -> Array[untyped]
151
288
  def to_a
289
+ values
290
+ end
291
+
292
+ # Returns cell values as an Array (sparse columns get nil).
293
+ #
294
+ # @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
295
+ # @return [Array<Object>]
296
+ # @api public
297
+ #: (?type_cast: bool) -> Array[untyped]
298
+ def values(type_cast: false)
299
+ if @cells
300
+ values_from_cells(type_cast)
301
+ else
302
+ Ooxml::WorksheetParser.fast_scan_row_values(@xml, @from, @to, @shared_strings, @styles, @date1904, type_cast: type_cast)
303
+ end
304
+ end
305
+
306
+ # Convert row cells to an Array of raw string values (sparse columns get nil).
307
+ #
308
+ # @return [Array<String, nil>]
309
+ # @api public
310
+ #: () -> Array[String?]
311
+ def raw_values
152
312
  return [] if cells.empty?
153
313
 
154
314
  max_col = cells.map(&:column_index).max || 0
155
315
  arr = Array.new(max_col + 1)
156
316
  cells.each do |cell|
157
- arr[cell.column_index] = cell.value
317
+ arr[cell.column_index] = cell.raw_value
158
318
  end
159
319
  arr
160
320
  end
161
321
 
162
- # Returns cell values as an Array.
322
+ # Convert row cells to an Array of formatted string representations (sparse columns get nil).
163
323
  #
164
- # @return [Array<Object>]
324
+ # @return [Array<String, nil>]
165
325
  # @api public
166
- #: () -> Array[untyped]
167
- def values
168
- to_a
326
+ #: () -> Array[String?]
327
+ def formatted_values
328
+ return [] if cells.empty?
329
+
330
+ max_col = cells.map(&:column_index).max || 0
331
+ arr = Array.new(max_col + 1)
332
+ cells.each do |cell|
333
+ arr[cell.column_index] = cell.formatted_value
334
+ end
335
+ arr
336
+ end
337
+
338
+ # Convert row cells to an Array of formula expressions without leading '=' (sparse columns get nil).
339
+ #
340
+ # @return [Array<String, nil>]
341
+ # @api public
342
+ #: () -> Array[String?]
343
+ def formulas
344
+ return [] if cells.empty?
345
+
346
+ max_col = cells.map(&:column_index).max || 0
347
+ arr = Array.new(max_col + 1)
348
+ cells.each do |cell|
349
+ arr[cell.column_index] = cell.formula_expression
350
+ end
351
+ arr
169
352
  end
170
353
 
171
354
  # Returns whether the row is valid according to OOXML specifications.
@@ -177,13 +360,25 @@ module Xlsxrb
177
360
  true
178
361
  end
179
362
 
363
+ # Returns whether the row contains no cell values (or all cell values are nil or empty strings).
364
+ #
365
+ # @return [Boolean]
366
+ # @api public
367
+ #: () -> bool
368
+ def empty?
369
+ vals = values
370
+ return true if vals.empty?
371
+
372
+ vals.all? { |v| v.nil? || (v.is_a?(String) && v.empty?) }
373
+ end
374
+
180
375
  # Unmapped metadata for compatibility with Elements::Row.
181
376
  #
182
377
  # @return [Hash]
183
378
  # @api public
184
379
  #: () -> Hash[untyped, untyped]
185
380
  def unmapped_data
186
- Elements::EMPTY_HASH
381
+ @style_index ? { style_index: @style_index } : Elements::EMPTY_HASH
187
382
  end
188
383
 
189
384
  # Validation errors for compatibility with Elements::Row.
@@ -203,5 +398,37 @@ module Xlsxrb
203
398
  def inspect
204
399
  "#<#{self.class.name} index=#{index} height=#{height.inspect} hidden=#{hidden}>"
205
400
  end
401
+
402
+ private
403
+
404
+ #: (bool) -> Array[untyped]
405
+ def values_from_cells(type_cast)
406
+ return [] if @cells.nil? || @cells.empty?
407
+
408
+ max_col = @cells.map(&:column_index).max || 0
409
+ arr = Array.new(max_col + 1)
410
+ @cells.each do |c|
411
+ val = c.value
412
+ if type_cast && val.is_a?(Numeric)
413
+ fmt_type = if c.style_index && @styles
414
+ NumberFormatter.format_type_for(c.style_index, @styles)
415
+ elsif c.format_code
416
+ NumberFormatter.format_type(c.format_code)
417
+ end
418
+ begin
419
+ case fmt_type
420
+ when :date
421
+ val = Ooxml::Utils.serial_to_date(val, date1904: @date1904)
422
+ when :datetime, :time
423
+ val = Ooxml::Utils.serial_to_datetime(val, date1904: @date1904)
424
+ end
425
+ rescue StandardError
426
+ # Keep val numeric on failure
427
+ end
428
+ end
429
+ arr[c.column_index] = val
430
+ end
431
+ arr
432
+ end
206
433
  end
207
434
  end