xlsxrb 0.1.13 → 0.1.14

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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -0
  3. data/README.md +2 -0
  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 +85 -4
  13. data/lib/xlsxrb/elements/worksheet.rb +424 -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.rb +12 -14
  20. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +7 -1
  21. data/lib/xlsxrb/ooxml/styles_parser.rb +39 -9
  22. data/lib/xlsxrb/ooxml/utils.rb +34 -18
  23. data/lib/xlsxrb/ooxml/workbook_parser.rb +53 -15
  24. data/lib/xlsxrb/ooxml/workbook_writer.rb +65 -14
  25. data/lib/xlsxrb/ooxml/worksheet_parser.rb +319 -79
  26. data/lib/xlsxrb/ooxml/worksheet_writer.rb +206 -61
  27. data/lib/xlsxrb/ooxml/writer/features_xml.rb +4 -2
  28. data/lib/xlsxrb/ooxml/writer.rb +14 -0
  29. data/lib/xlsxrb/ooxml/zip_reader.rb +109 -25
  30. data/lib/xlsxrb/stream_row.rb +243 -16
  31. data/lib/xlsxrb/stream_sheet.rb +414 -15
  32. data/lib/xlsxrb/stream_writer.rb +213 -17
  33. data/lib/xlsxrb/style_builder.rb +308 -22
  34. data/lib/xlsxrb/utils.rb +158 -0
  35. data/lib/xlsxrb/version.rb +1 -1
  36. data/lib/xlsxrb/workbook_builder.rb +73 -3
  37. data/lib/xlsxrb/worksheet_builder.rb +227 -16
  38. data/lib/xlsxrb.rb +578 -79
  39. data/sig/generated/xlsxrb/colors.rbs +40 -0
  40. data/sig/generated/xlsxrb/elements/cell.rbs +113 -8
  41. data/sig/generated/xlsxrb/elements/column.rbs +3 -2
  42. data/sig/generated/xlsxrb/elements/coordinate_access.rbs +18 -0
  43. data/sig/generated/xlsxrb/elements/image.rbs +82 -0
  44. data/sig/generated/xlsxrb/elements/row.rbs +83 -4
  45. data/sig/generated/xlsxrb/elements/styles.rbs +79 -0
  46. data/sig/generated/xlsxrb/elements/types.rbs +43 -0
  47. data/sig/generated/xlsxrb/elements/workbook.rbs +51 -2
  48. data/sig/generated/xlsxrb/elements/worksheet.rbs +231 -18
  49. data/sig/generated/xlsxrb/number_formatter.rbs +132 -0
  50. data/sig/generated/xlsxrb/ooxml/reader.rbs +6 -0
  51. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +2 -0
  52. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +5 -2
  53. data/sig/generated/xlsxrb/ooxml/utils.rbs +17 -8
  54. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +9 -3
  55. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +28 -6
  56. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +3 -1
  57. data/sig/generated/xlsxrb/ooxml/writer.rbs +2 -0
  58. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +10 -0
  59. data/sig/generated/xlsxrb/stream_row.rbs +100 -6
  60. data/sig/generated/xlsxrb/stream_sheet.rbs +247 -13
  61. data/sig/generated/xlsxrb/stream_writer.rbs +120 -14
  62. data/sig/generated/xlsxrb/style_builder.rbs +55 -18
  63. data/sig/generated/xlsxrb/utils.rbs +119 -0
  64. data/sig/generated/xlsxrb/workbook_builder.rbs +26 -0
  65. data/sig/generated/xlsxrb/worksheet_builder.rbs +107 -11
  66. data/sig/generated/xlsxrb.rbs +186 -27
  67. metadata +11 -1
@@ -33,6 +33,16 @@ module Xlsxrb
33
33
  #: String
34
34
  attr_reader :name
35
35
 
36
+ # @return [Symbol] The sheet visibility state (:visible, :hidden, or :very_hidden).
37
+ # @api public
38
+ #: Symbol
39
+ attr_reader :state
40
+
41
+ # @return [Hash, nil] The parsed styles definition hash.
42
+ # @api public
43
+ #: Hash[untyped, untyped]?
44
+ attr_reader :styles
45
+
36
46
  # Initializes a streaming worksheet context.
37
47
  #
38
48
  # @param name [String] The sheet name.
@@ -41,15 +51,136 @@ module Xlsxrb
41
51
  # @param styles [Hash, nil] Optional parsed styles hash.
42
52
  # @param zip_reader [Ooxml::ZipReader, nil] Optional ZipReader context.
43
53
  # @param entry_name [String, nil] Archive entry name for this sheet.
44
- #: (String name, untyped sheet_source, Array[String] shared_strings, ?Hash[untyped, untyped]? styles, ?zip_reader: Ooxml::ZipReader?, ?entry_name: String?) -> void
45
- def initialize(name, sheet_source, shared_strings, styles = nil, zip_reader: nil, entry_name: nil)
54
+ # @param date1904 [Boolean] Whether the 1904 date system is active.
55
+ # @param dimension [String, nil] Sheet dimension reference string (e.g. "A1:Z50000").
56
+ # @param trim_empty_rows [Boolean] Whether to omit trailing empty rows during iteration.
57
+ # @param pad_empty_rows [Boolean] Whether to yield empty rows for skipped row numbers.
58
+ # @param pad_empty_cells [Boolean] Whether to pad missing cells within rows.
59
+ # @param images [Array<Elements::Image>, nil] Optional preloaded images array.
60
+ #: (String name, untyped sheet_source, Array[String] shared_strings, ?Hash[untyped, untyped]? styles, ?zip_reader: Ooxml::ZipReader?, ?entry_name: String?, ?state: Symbol, ?date1904: bool, ?dimension: String?, ?trim_empty_rows: bool, ?pad_empty_rows: bool, ?pad_empty_cells: bool, ?images: Array[Elements::Image]?) -> void
61
+ def initialize(name, sheet_source, shared_strings, styles = nil, zip_reader: nil, entry_name: nil, state: :visible, date1904: false, dimension: nil, trim_empty_rows: false, pad_empty_rows: false, pad_empty_cells: false, images: nil)
46
62
  @name = name
47
63
  @sheet_source = sheet_source
48
64
  @shared_strings = shared_strings
49
65
  @styles = styles
50
66
  @zip_reader = zip_reader
51
67
  @entry_name = entry_name
68
+ @state = state ? state.to_sym : :visible
69
+ @date1904 = date1904 ? true : false
70
+ @dimension = dimension
71
+ @dimension_checked = !dimension.nil?
52
72
  @sheet_xml = sheet_source if sheet_source.is_a?(String)
73
+ @trim_empty_rows = trim_empty_rows ? true : false
74
+ @pad_empty_rows = pad_empty_rows ? true : false
75
+ @pad_empty_cells = pad_empty_cells ? true : false
76
+ @images = images
77
+ end
78
+
79
+ # Returns whether trailing empty rows are omitted during iteration.
80
+ #
81
+ # @return [Boolean]
82
+ # @api public
83
+ #: () -> bool
84
+ def trim_empty_rows?
85
+ @trim_empty_rows ? true : false
86
+ end
87
+ alias trim_empty_rows trim_empty_rows?
88
+
89
+ # Returns whether skipped rows are padded during iteration.
90
+ #
91
+ # @return [Boolean]
92
+ # @api public
93
+ #: () -> bool
94
+ def pad_empty_rows?
95
+ @pad_empty_rows ? true : false
96
+ end
97
+ alias pad_empty_rows pad_empty_rows?
98
+
99
+ # Returns whether missing cells within rows are padded during iteration.
100
+ #
101
+ # @return [Boolean]
102
+ # @api public
103
+ #: () -> bool
104
+ def pad_empty_cells?
105
+ @pad_empty_cells ? true : false
106
+ end
107
+ alias pad_empty_cells pad_empty_cells?
108
+
109
+ # Returns the dimension reference string (e.g. "A1:Z50000"), or nil if not present.
110
+ #
111
+ # @return [String, nil]
112
+ # @api public
113
+ #: () -> String?
114
+ def dimension
115
+ return @dimension if @dimension_checked
116
+
117
+ @dimension_checked = true
118
+ @dimension = extract_dimension
119
+ end
120
+
121
+ # Returns the 1-based index of the first row defined in the sheet dimension, or nil.
122
+ #
123
+ # @return [Integer, nil]
124
+ # @api public
125
+ #: () -> Integer?
126
+ def first_row
127
+ dimension_bounds&.[](0)
128
+ end
129
+
130
+ # Returns the 1-based index of the first column defined in the sheet dimension, or nil.
131
+ #
132
+ # @return [Integer, nil]
133
+ # @api public
134
+ #: () -> Integer?
135
+ def first_column
136
+ dimension_bounds&.[](1)
137
+ end
138
+ alias first_col first_column
139
+
140
+ # Returns the 1-based index of the last row defined in the sheet dimension, or nil.
141
+ #
142
+ # @return [Integer, nil]
143
+ # @api public
144
+ #: () -> Integer?
145
+ def last_row
146
+ dimension_bounds&.[](2)
147
+ end
148
+
149
+ # Returns the 1-based index of the last column defined in the sheet dimension, or nil.
150
+ #
151
+ # @return [Integer, nil]
152
+ # @api public
153
+ #: () -> Integer?
154
+ def last_column
155
+ dimension_bounds&.[](3)
156
+ end
157
+ alias last_col last_column
158
+
159
+ # Returns whether the sheet uses the 1904 date system.
160
+ #
161
+ # @return [Boolean]
162
+ # @api public
163
+ #: () -> bool
164
+ def date1904?
165
+ @date1904
166
+ end
167
+
168
+ # Returns whether the sheet is hidden (:hidden or :very_hidden).
169
+ #
170
+ # @return [Boolean]
171
+ # @api public
172
+ #: () -> bool
173
+ def hidden?
174
+ @state == :hidden || @state == :very_hidden
175
+ end
176
+
177
+ # Returns whether the sheet is visible.
178
+ #
179
+ # @return [Boolean]
180
+ # @api public
181
+ #: () -> bool
182
+ def visible?
183
+ @state == :visible
53
184
  end
54
185
 
55
186
  # Iterates over rows in this streaming worksheet with O(1) memory.
@@ -57,16 +188,41 @@ module Xlsxrb
57
188
  # @overload each_row(&block)
58
189
  # @yield [row]
59
190
  # @yieldparam row [StreamRow, Elements::Row] The current row.
191
+ # @overload each_row(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
192
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows (defaults to sheet setting).
193
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers (defaults to sheet setting).
194
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows (defaults to sheet setting).
195
+ # @yield [row]
196
+ # @yieldparam row [StreamRow, Elements::Row] The current row.
60
197
  # @return [void]
61
198
  #
62
- # @overload each_row
199
+ # @overload each_row(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil)
200
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows (defaults to sheet setting).
201
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers (defaults to sheet setting).
202
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows (defaults to sheet setting).
63
203
  # @return [Enumerator<StreamRow | Elements::Row, void>]
64
204
  #
65
205
  # @api public
66
- #: () { (StreamRow | Elements::Row) -> void } -> void
67
- #: () -> Enumerator[StreamRow | Elements::Row, void]
68
- def each_row(&)
69
- return enum_for(:each_row) unless block_given?
206
+ #: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) { (StreamRow | Elements::Row) -> void } -> void
207
+ #: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) -> Enumerator[StreamRow | Elements::Row, void]
208
+ def each_row(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &)
209
+ return enum_for(:each_row, trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells) unless block_given?
210
+
211
+ trim = if trim_empty_rows.nil?
212
+ @trim_empty_rows
213
+ else
214
+ (trim_empty_rows ? true : false)
215
+ end
216
+ pad_rows = if pad_empty_rows.nil?
217
+ @pad_empty_rows
218
+ else
219
+ (pad_empty_rows ? true : false)
220
+ end
221
+ pad_cells = if pad_empty_cells.nil?
222
+ @pad_empty_cells
223
+ else
224
+ (pad_empty_cells ? true : false)
225
+ end
70
226
 
71
227
  source = if @sheet_source
72
228
  @sheet_source
@@ -74,7 +230,7 @@ module Xlsxrb
74
230
  ->(&blk) { @zip_reader.each_entry_chunk(@entry_name, &blk) }
75
231
  end
76
232
 
77
- Ooxml::WorksheetParser.each_row(source, shared_strings: @shared_strings, &)
233
+ Ooxml::WorksheetParser.each_row(source, shared_strings: @shared_strings, styles: @styles, date1904: @date1904, trim_empty_rows: trim, pad_empty_rows: pad_rows, pad_empty_cells: pad_cells, &)
78
234
  end
79
235
 
80
236
  # Iterates over all cells across all rows continuously with O(1) memory.
@@ -98,21 +254,56 @@ module Xlsxrb
98
254
  end
99
255
  end
100
256
 
257
+ # Iterates over row values directly as Arrays without wrapping cells in Elements::Cell objects.
258
+ #
259
+ # @overload each_row_values(type_cast: false, trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
260
+ # @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
261
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows (defaults to sheet setting).
262
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers (defaults to sheet setting).
263
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows (defaults to sheet setting).
264
+ # @yield [values]
265
+ # @yieldparam values [Array<Object>] Row values array.
266
+ # @return [void]
267
+ #
268
+ # @overload each_row_values(type_cast: false, trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil)
269
+ # @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
270
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows (defaults to sheet setting).
271
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers (defaults to sheet setting).
272
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows (defaults to sheet setting).
273
+ # @return [Enumerator<Array<Object>, void>]
274
+ #
275
+ # @api public
276
+ #: (?type_cast: bool, ?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) { (Array[untyped]) -> void } -> void
277
+ #: (?type_cast: bool, ?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) -> Enumerator[Array[untyped], void]
278
+ def each_row_values(type_cast: false, trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
279
+ return enum_for(:each_row_values, type_cast: type_cast, trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells) unless block
280
+
281
+ each_row(trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells) do |row|
282
+ block.call(row.values(type_cast: type_cast))
283
+ end
284
+ end
285
+
101
286
  # Default Enumerable iteration delegates to {#each_row}.
102
287
  #
103
- # @overload each(&block)
288
+ # @overload each(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
289
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows.
290
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers.
291
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows.
104
292
  # @yield [row]
105
293
  # @yieldparam row [StreamRow, Elements::Row]
106
294
  # @return [void]
107
295
  #
108
- # @overload each
296
+ # @overload each(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil)
297
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows.
298
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers.
299
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows.
109
300
  # @return [Enumerator<StreamRow | Elements::Row, void>]
110
301
  #
111
302
  # @api public
112
- #: () { (StreamRow | Elements::Row) -> void } -> void
113
- #: () -> Enumerator[StreamRow | Elements::Row, void]
114
- def each(&)
115
- each_row(&)
303
+ #: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) { (StreamRow | Elements::Row) -> void } -> void
304
+ #: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) -> Enumerator[StreamRow | Elements::Row, void]
305
+ def each(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &)
306
+ each_row(trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells, &)
116
307
  end
117
308
 
118
309
  # Loads this sheet completely into an in-memory {Elements::Worksheet},
@@ -123,10 +314,151 @@ module Xlsxrb
123
314
  # @api public
124
315
  #: () -> Elements::Worksheet
125
316
  def load
126
- Xlsxrb.send(:build_worksheet, @name, raw_sheet_xml, @shared_strings, @styles)
317
+ Xlsxrb.send(:build_worksheet, @name, raw_sheet_xml, @shared_strings, @styles, state: @state, zip_reader: @zip_reader, entry_name: @entry_name, hyperlinks: hyperlinks, comments: comments, images: images, date1904: @date1904, dimension: dimension, trim_empty_rows: @trim_empty_rows, pad_empty_rows: @pad_empty_rows, pad_empty_cells: @pad_empty_cells)
127
318
  end
128
319
  alias to_worksheet load
129
320
 
321
+ # Returns hyperlinks configured for this worksheet as a Hash of cell references to hyperlink hashes.
322
+ #
323
+ # @return [Hash<String, Hash[Symbol, untyped]>]
324
+ # @api public
325
+ #: () -> Hash[String, Hash[Symbol, untyped]]
326
+ def hyperlinks
327
+ @hyperlinks ||= Xlsxrb.send(:resolve_hyperlinks, raw_sheet_xml, zip_reader: @zip_reader, entry_name: @entry_name)
328
+ end
329
+
330
+ # Returns hyperlink metadata for a specific cell reference, or nil.
331
+ #
332
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
333
+ # @param col [Integer, nil] Optional 0-based column index.
334
+ # @return [Hash, nil]
335
+ # @api public
336
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Hash[Symbol, untyped]?
337
+ def hyperlink(ref_or_row, col = nil)
338
+ ref = if col
339
+ "#{Elements::Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
340
+ else
341
+ ref_or_row.to_s.upcase
342
+ end
343
+ hyperlinks[ref]
344
+ end
345
+
346
+ # Returns comments configured for this worksheet as an Array of comment hashes.
347
+ #
348
+ # @return [Array<Hash[Symbol, untyped]>]
349
+ # @api public
350
+ #: () -> Array[Hash[Symbol, untyped]]
351
+ def comments
352
+ @comments ||= Xlsxrb.send(:resolve_comments, zip_reader: @zip_reader, entry_name: @entry_name)
353
+ end
354
+
355
+ # Returns comment metadata for a specific cell reference, or nil.
356
+ #
357
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
358
+ # @param col [Integer, nil] Optional 0-based column index.
359
+ # @return [Hash, nil]
360
+ # @api public
361
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Hash[Symbol, untyped]?
362
+ def comment(ref_or_row, col = nil)
363
+ ref = if col
364
+ "#{Elements::Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
365
+ else
366
+ ref_or_row.to_s.upcase
367
+ end
368
+ comments_by_ref[ref]
369
+ end
370
+
371
+ # Returns comments indexed by cell reference.
372
+ #
373
+ # @return [Hash<String, Hash[Symbol, untyped]>]
374
+ # @api public
375
+ #: () -> Hash[String, Hash[Symbol, untyped]]
376
+ def comments_by_ref
377
+ @comments_by_ref ||= comments.each_with_object({}) do |c, acc|
378
+ r = c[:ref] || c[:cell]
379
+ acc[r.to_s.upcase] = c if r
380
+ end
381
+ end
382
+
383
+ # Returns all embedded images in this worksheet.
384
+ #
385
+ # @return [Array<Elements::Image>]
386
+ # @api public
387
+ #: () -> Array[Elements::Image]
388
+ def images
389
+ @images ||= Xlsxrb.send(:resolve_images, zip_reader: @zip_reader, entry_name: @entry_name)
390
+ end
391
+
392
+ # Returns all images anchored at a specific cell reference or 0-based coordinate.
393
+ #
394
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
395
+ # @param col [Integer, nil] Optional 0-based column index.
396
+ # @return [Array<Elements::Image>]
397
+ # @api public
398
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Array[Elements::Image]
399
+ def images_at(ref_or_row, col = nil)
400
+ ref = if col
401
+ "#{Elements::Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
402
+ else
403
+ ref_or_row.to_s.upcase
404
+ end
405
+ images.select { |img| img.cell_ref == ref }
406
+ end
407
+
408
+ # Loads and returns images for this worksheet, optionally yielding self for block syntax.
409
+ #
410
+ # @yield [sheet]
411
+ # @yieldparam sheet [StreamSheet]
412
+ # @return [StreamSheet, void]
413
+ # @api public
414
+ #: () { (StreamSheet) -> void } -> void
415
+ #: () -> StreamSheet
416
+ def with_images
417
+ images
418
+ if block_given?
419
+ yield self
420
+ nil
421
+ else
422
+ self
423
+ end
424
+ end
425
+
426
+ # Access a cell by reference or 0-based coordinates.
427
+ #
428
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
429
+ # @param col [Integer, nil] Optional 0-based column index.
430
+ # @return [Elements::Cell, nil]
431
+ # @api public
432
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Elements::Cell?
433
+ def cell(ref_or_row, col = nil)
434
+ target_row, target_col = if col
435
+ [ref_or_row.to_i, col.to_i]
436
+ else
437
+ parsed = Elements::Cell.parse_ref(ref_or_row.to_s)
438
+ return nil unless parsed
439
+
440
+ parsed
441
+ end
442
+
443
+ each_row do |row|
444
+ next if row.index < target_row
445
+ return row.cell_at(target_col) if row.index == target_row
446
+ break if row.index > target_row
447
+ end
448
+ nil
449
+ end
450
+
451
+ # Returns the formatted string representation of a cell's value.
452
+ #
453
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
454
+ # @param col [Integer, nil] Optional 0-based column index.
455
+ # @return [String, nil]
456
+ # @api public
457
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> String?
458
+ def formatted_value(ref_or_row, col = nil)
459
+ cell(ref_or_row, col)&.formatted_value
460
+ end
461
+
130
462
  # Returns merged cell ranges (e.g. ["A1:B2"]) for this worksheet.
131
463
  #
132
464
  # @return [Array<String>]
@@ -203,5 +535,72 @@ module Xlsxrb
203
535
  ""
204
536
  end
205
537
  end
538
+
539
+ # Returns 1-based bounds [first_row, first_col, last_row, last_col] from dimension ref.
540
+ #: () -> Array[Integer]?
541
+ def dimension_bounds
542
+ return @dimension_bounds if defined?(@dimension_bounds)
543
+
544
+ ref = dimension
545
+ return @dimension_bounds = nil unless ref
546
+
547
+ parts = ref.split(":", 2)
548
+ first_coords = Elements::Cell.parse_ref(parts[0])
549
+ return @dimension_bounds = nil unless first_coords
550
+
551
+ last_coords = parts[1] ? Elements::Cell.parse_ref(parts[1]) : first_coords
552
+ return @dimension_bounds = nil unless last_coords
553
+
554
+ @dimension_bounds = [
555
+ first_coords[0] + 1,
556
+ first_coords[1] + 1,
557
+ last_coords[0] + 1,
558
+ last_coords[1] + 1
559
+ ].freeze
560
+ end
561
+
562
+ # Extracts dimension reference from XML source lazily without parsing the entire file.
563
+ #: () -> String?
564
+ def extract_dimension
565
+ if @sheet_source.is_a?(String)
566
+ @sheet_source.slice(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/, 1)
567
+ elsif @zip_reader && @entry_name
568
+ stream = @zip_reader.open_entry_io(@entry_name)
569
+ buf = +""
570
+ dim = nil
571
+ begin
572
+ while (chunk = stream.read(65_536))
573
+ buf << chunk
574
+ if (m = buf.match(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/))
575
+ dim = m[1]
576
+ break
577
+ end
578
+ break if buf.include?("<sheetData") || buf.include?(":sheetData")
579
+ end
580
+ ensure
581
+ stream.close
582
+ end
583
+ dim
584
+ elsif @sheet_source.respond_to?(:call)
585
+ buf = +""
586
+ dim = nil
587
+ @sheet_source.call do |chunk|
588
+ buf << chunk
589
+ if (m = buf.match(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/))
590
+ dim = m[1]
591
+ break
592
+ end
593
+ break if buf.include?("<sheetData") || buf.include?(":sheetData")
594
+ end
595
+ dim
596
+ elsif @sheet_source.respond_to?(:seek) && @sheet_source.respond_to?(:pos)
597
+ pos = @sheet_source.pos
598
+ buf = @sheet_source.read(65_536) || ""
599
+ @sheet_source.seek(pos, IO::SEEK_SET)
600
+ buf.slice(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/, 1)
601
+ else
602
+ raw_sheet_xml.slice(/<(?:[a-zA-Z0-9_]+:)?dimension\b[^>]*\bref=["']([^"']+)["']/, 1)
603
+ end
604
+ end
206
605
  end
207
606
  end