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
@@ -17,23 +17,139 @@ module Xlsxrb
17
17
  class Worksheet
18
18
  [Enumerable, CoordinateAccess].each { |m| include m }
19
19
 
20
- attr_reader :name, :rows, :columns, :charts, :unmapped_data, :errors
20
+ attr_reader :name, :rows, :columns, :charts, :conditional_formatting, :data_validations, :unmapped_data, :errors, :state, :hyperlinks, :comments, :images, :styles, :date1904, :print_area, :print_titles, :sparkline_groups, :merged_cells, :auto_filter, :tables, :shapes, :freeze_pane, :page_setup, :page_margins, :header_footer, :print_options, :sheet_protection, :sheet_view, :sheet_properties, :sheet_format, :row_breaks, :col_breaks, :protected_ranges, :cell_watches
21
+ attr_writer :dimension
22
+ alias conditional_formats conditional_formatting
21
23
 
22
24
  # @param name [String] The worksheet name (max 31 characters).
23
25
  # @param rows [Array<Elements::Row>] Rows in the sheet.
24
26
  # @param columns [Array<Elements::Column>] Column definitions.
25
27
  # @param charts [Array<Hash>] Charts in the sheet.
28
+ # @param conditional_formatting [Array<Hash>, nil] Conditional formatting rules.
29
+ # @param data_validations [Array<Hash>] Data validation rules.
26
30
  # @param unmapped_data [Hash] Additional metadata for round-tripping.
27
31
  # @param errors [Array<String>, nil] Validation errors.
28
- #: (name: String?, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
29
- def initialize(name:, rows: [], columns: [], charts: [], unmapped_data: {}, errors: nil)
32
+ # @param conditional_formats [Array<Hash>, nil] Alias for conditional_formatting.
33
+ # @param state [Symbol] Sheet visibility state (:visible, :hidden, or :very_hidden).
34
+ # @param hyperlinks [Hash{String => Hash}] Hyperlink mappings by cell reference.
35
+ # @param comments [Array<Hash>] Comments and notes in the sheet.
36
+ # @param images [Array<Elements::Image>] Embedded images in the sheet.
37
+ # @param styles [Hash, nil] Optional parsed styles hash.
38
+ # @param dimension [String, nil] Sheet dimension reference string (e.g. "A1:Z50000").
39
+ # @param trim_empty_rows [Boolean] Whether to omit trailing empty rows during iteration.
40
+ # @param pad_empty_rows [Boolean] Whether to yield empty rows for skipped row numbers.
41
+ # @param pad_empty_cells [Boolean] Whether to pad missing cells within rows.
42
+ # @param print_area [String, nil] Sheet print area range (e.g. "A1:H50").
43
+ # @param print_titles [Hash, nil] Repeating print title rows/cols (e.g. { rows: "1:2", cols: "A:B" }).
44
+ # @param sparkline_groups [Array<Hash>] Sparkline groups in the sheet.
45
+ # @param merged_cells [Array<String>] Merged cell ranges (e.g. ["A1:B2"]).
46
+ # @param auto_filter [String, nil] AutoFilter range (e.g. "A1:E10").
47
+ # @param tables [Array<Hash>] Table definitions in the sheet.
48
+ # @param shapes [Array<Hash>] Shape definitions in the sheet.
49
+ # @param freeze_pane [Hash, nil] Freeze pane settings (e.g. { row: 1, col: 0 }).
50
+ # @param page_setup [Hash] Page setup properties (e.g. orientation, paper size).
51
+ # @param page_margins [Hash, nil] Page margins settings.
52
+ # @param header_footer [Hash] Header and footer configurations.
53
+ # @param print_options [Hash] Print options (e.g. gridlines, headings).
54
+ # @param sheet_protection [Hash, nil] Sheet protection settings.
55
+ # @param sheet_view [Hash] Sheet view configurations (e.g. zoom, gridlines).
56
+ # @param sheet_properties [Hash] Sheet properties (e.g. tabColor, outlinePr).
57
+ # @param sheet_format [Hash] Sheet format properties (e.g. defaultRowHeight, defaultColWidth).
58
+ # @param row_breaks [Array<Integer | Hash>] Explicit page row breaks.
59
+ # @param col_breaks [Array<Integer | Hash>] Explicit page column breaks.
60
+ # @param protected_ranges [Array<Hash>] Protected range definitions.
61
+ # @param cell_watches [Array<String>] Watched cell references.
62
+ #: (name: String?, ?rows: Array[Elements::Row], ?columns: Array[Elements::Column], ?charts: Array[Hash[Symbol, untyped]], ?conditional_formatting: Array[Hash[Symbol, untyped]]?, ?data_validations: Array[Hash[Symbol, untyped]], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?, ?conditional_formats: Array[Hash[Symbol, untyped]]?, ?state: Symbol, ?hyperlinks: Hash[String, untyped], ?comments: Array[Hash[Symbol, untyped]], ?images: Array[Elements::Image], ?styles: Hash[untyped, untyped]?, ?date1904: bool, ?dimension: String?, ?trim_empty_rows: bool, ?pad_empty_rows: bool, ?pad_empty_cells: bool, ?print_area: String?, ?print_titles: Hash[Symbol, untyped]?, ?sparkline_groups: Array[Hash[Symbol, untyped]], ?merged_cells: Array[String], ?auto_filter: String?, ?tables: Array[Hash[Symbol, untyped]], ?shapes: Array[Hash[Symbol, untyped]], ?freeze_pane: Hash[Symbol, untyped]?, ?page_setup: Hash[Symbol, untyped], ?page_margins: Hash[Symbol, untyped]?, ?header_footer: Hash[Symbol, untyped], ?print_options: Hash[Symbol, untyped], ?sheet_protection: Hash[Symbol, untyped]?, ?sheet_view: Hash[Symbol, untyped], ?sheet_properties: Hash[Symbol, untyped], ?sheet_format: Hash[Symbol, untyped], ?row_breaks: Array[untyped], ?col_breaks: Array[untyped], ?protected_ranges: Array[Hash[Symbol, untyped]], ?cell_watches: Array[String]) -> void
63
+ def initialize(name:, rows: [], columns: [], charts: [], conditional_formatting: nil, data_validations: [], unmapped_data: {}, errors: nil, conditional_formats: nil, state: :visible, hyperlinks: {}, comments: [], images: [], styles: nil, date1904: false, dimension: nil, trim_empty_rows: false, pad_empty_rows: false, pad_empty_cells: false, print_area: nil, print_titles: nil, sparkline_groups: [], merged_cells: [], auto_filter: nil, tables: [], shapes: [], freeze_pane: nil, page_setup: {}, page_margins: nil, header_footer: {}, print_options: {}, sheet_protection: nil, sheet_view: {}, sheet_properties: {}, sheet_format: {}, row_breaks: [], col_breaks: [], protected_ranges: [], cell_watches: [])
30
64
  @name = name
31
65
  @rows = (rows || []).freeze
32
66
  @columns = (columns || []).freeze
33
67
  @charts = (charts || []).freeze
68
+ cf = conditional_formatting || conditional_formats || []
69
+ @conditional_formatting = cf.freeze
70
+ @data_validations = (data_validations || []).freeze
34
71
  @unmapped_data = (unmapped_data || {}).freeze
35
72
  computed_errors = errors || self.class.validate(@name, @rows)
36
73
  @errors = computed_errors.freeze
74
+ @state = state ? state.to_sym : :visible
75
+ @hyperlinks = (hyperlinks || {}).freeze
76
+ @comments = (comments || []).freeze
77
+ @images = (images || []).freeze
78
+ @styles = styles
79
+ @date1904 = date1904 ? true : false
80
+ @dimension = dimension
81
+ @print_area = print_area
82
+ @print_titles = print_titles&.freeze
83
+ @sparkline_groups = (sparkline_groups || []).freeze
84
+ @trim_empty_rows = trim_empty_rows ? true : false
85
+ @pad_empty_rows = pad_empty_rows ? true : false
86
+ @pad_empty_cells = pad_empty_cells ? true : false
87
+ @merged_cells = (merged_cells || []).freeze
88
+ @auto_filter = auto_filter
89
+ @tables = (tables || []).freeze
90
+ @shapes = (shapes || []).freeze
91
+ @freeze_pane = freeze_pane&.freeze
92
+ @page_setup = (page_setup || {}).freeze
93
+ @page_margins = page_margins&.freeze
94
+ @header_footer = (header_footer || {}).freeze
95
+ @print_options = (print_options || {}).freeze
96
+ @sheet_protection = sheet_protection&.freeze
97
+ @sheet_view = (sheet_view || {}).freeze
98
+ @sheet_properties = (sheet_properties || {}).freeze
99
+ @sheet_format = (sheet_format || unmapped_data[:sheet_format] || unmapped_data.dig(:facade, :sheet_format) || {}).freeze
100
+ @row_breaks = (row_breaks || []).freeze
101
+ @col_breaks = (col_breaks || []).freeze
102
+ @protected_ranges = (protected_ranges || unmapped_data[:protected_ranges] || unmapped_data.dig(:facade, :protected_ranges) || []).freeze
103
+ @cell_watches = (cell_watches || unmapped_data[:cell_watches] || unmapped_data.dig(:facade, :cell_watches) || []).freeze
104
+ end
105
+
106
+ # Returns whether trailing empty rows are omitted during iteration.
107
+ #
108
+ # @return [Boolean]
109
+ # @api public
110
+ #: () -> bool
111
+ def trim_empty_rows?
112
+ @trim_empty_rows ? true : false
113
+ end
114
+ alias trim_empty_rows trim_empty_rows?
115
+
116
+ # Returns whether skipped rows are padded during iteration.
117
+ #
118
+ # @return [Boolean]
119
+ # @api public
120
+ #: () -> bool
121
+ def pad_empty_rows?
122
+ @pad_empty_rows ? true : false
123
+ end
124
+ alias pad_empty_rows pad_empty_rows?
125
+
126
+ # Returns whether missing cells within rows are padded during iteration.
127
+ #
128
+ # @return [Boolean]
129
+ # @api public
130
+ #: () -> bool
131
+ def pad_empty_cells?
132
+ @pad_empty_cells ? true : false
133
+ end
134
+ alias pad_empty_cells pad_empty_cells?
135
+
136
+ # Returns whether the worksheet uses the 1904 date system.
137
+ #
138
+ # @return [Boolean]
139
+ # @api public
140
+ #: () -> bool
141
+ def date1904?
142
+ @date1904
143
+ end
144
+
145
+ # Returns the worksheet dimension reference string (e.g. "A1:Z50000"),
146
+ # or computes it dynamically from rows if not explicitly set.
147
+ #
148
+ # @return [String, nil]
149
+ # @api public
150
+ #: () -> String?
151
+ def dimension
152
+ @dimension ||= compute_dimension
37
153
  end
38
154
 
39
155
  # Iterate over rows in the worksheet.
@@ -43,16 +159,27 @@ module Xlsxrb
43
159
  # puts row.to_a.inspect
44
160
  # end
45
161
  #
46
- # @yield [row]
47
- # @yieldparam row [Elements::Row]
48
- # @return [Enumerator, void]
162
+ # @overload each(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
163
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows.
164
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers.
165
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows.
166
+ # @yield [row]
167
+ # @yieldparam row [Elements::Row]
168
+ # @return [void]
169
+ #
170
+ # @overload each(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil)
171
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows.
172
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers.
173
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows.
174
+ # @return [Enumerator<Elements::Row, void>]
175
+ #
49
176
  # @api public
50
- #: () { (Elements::Row) -> void } -> void
51
- #: () -> Enumerator[Elements::Row, void]
52
- def each(&)
53
- return to_enum(:each) unless block_given?
177
+ #: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) { (Elements::Row) -> void } -> void
178
+ #: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) -> Enumerator[Elements::Row, void]
179
+ def each(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &)
180
+ return to_enum(:each, trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells) unless block_given?
54
181
 
55
- rows.each(&)
182
+ each_row(trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells, &)
56
183
  end
57
184
 
58
185
  # Iterate over rows in the worksheet.
@@ -62,16 +189,104 @@ module Xlsxrb
62
189
  # puts "Row #{row.index}: #{row.to_a.inspect}"
63
190
  # end
64
191
  #
65
- # @yield [row]
66
- # @yieldparam row [Elements::Row]
67
- # @return [Enumerator, void]
192
+ # @overload each_row(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
193
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows.
194
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers.
195
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows.
196
+ # @yield [row]
197
+ # @yieldparam row [Elements::Row]
198
+ # @return [void]
199
+ #
200
+ # @overload each_row(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil)
201
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows.
202
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers.
203
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows.
204
+ # @return [Enumerator<Elements::Row, void>]
205
+ #
206
+ # @api public
207
+ #: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) { (Elements::Row) -> void } -> void
208
+ #: (?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) -> Enumerator[Elements::Row, void]
209
+ def each_row(trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
210
+ return to_enum(:each_row, trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells) unless block
211
+
212
+ trim = if trim_empty_rows.nil?
213
+ @trim_empty_rows
214
+ else
215
+ (trim_empty_rows ? true : false)
216
+ end
217
+ pad_rows = if pad_empty_rows.nil?
218
+ @pad_empty_rows
219
+ else
220
+ (pad_empty_rows ? true : false)
221
+ end
222
+ pad_cells = if pad_empty_cells.nil?
223
+ @pad_empty_cells
224
+ else
225
+ (pad_empty_cells ? true : false)
226
+ end
227
+
228
+ row_consumer = if trim
229
+ pending_empty = []
230
+ lambda do |row|
231
+ if row.empty?
232
+ pending_empty << row
233
+ else
234
+ pending_empty.each(&block)
235
+ pending_empty.clear
236
+ block.call(row)
237
+ end
238
+ end
239
+ else
240
+ block
241
+ end
242
+
243
+ if pad_rows
244
+ current_expected_idx = 0
245
+ inner_consumer = row_consumer
246
+ row_consumer = lambda do |row|
247
+ while current_expected_idx < row.index
248
+ empty_row = Row.new(index: current_expected_idx)
249
+ inner_consumer.call(pad_cells ? empty_row.pad_empty_cells : empty_row)
250
+ current_expected_idx += 1
251
+ end
252
+ inner_consumer.call(row)
253
+ current_expected_idx = row.index + 1
254
+ end
255
+ end
256
+
257
+ rows.each do |row|
258
+ row_to_yield = pad_cells ? row.pad_empty_cells : row
259
+ row_consumer.call(row_to_yield)
260
+ end
261
+ end
262
+
263
+ # Iterates over row values as Arrays.
264
+ #
265
+ # @overload each_row_values(type_cast: false, trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
266
+ # @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
267
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows.
268
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers.
269
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows.
270
+ # @yield [values]
271
+ # @yieldparam values [Array<Object>] Row values array.
272
+ # @return [void]
273
+ #
274
+ # @overload each_row_values(type_cast: false, trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil)
275
+ # @param type_cast [Boolean] Whether to coerce date/time serial numbers into Date/Time instances.
276
+ # @param trim_empty_rows [Boolean, nil] Whether to omit trailing empty rows.
277
+ # @param pad_empty_rows [Boolean, nil] Whether to yield empty rows for skipped row numbers.
278
+ # @param pad_empty_cells [Boolean, nil] Whether to pad missing cells within rows.
279
+ # @return [Enumerator<Array<Object>, void>]
280
+ #
68
281
  # @api public
69
- #: () { (Elements::Row) -> void } -> void
70
- #: () -> Enumerator[Elements::Row, void]
71
- def each_row(&)
72
- return to_enum(:each_row) unless block_given?
282
+ #: (?type_cast: bool, ?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) { (Array[untyped]) -> void } -> void
283
+ #: (?type_cast: bool, ?trim_empty_rows: bool?, ?pad_empty_rows: bool?, ?pad_empty_cells: bool?) -> Enumerator[Array[untyped], void]
284
+ def each_row_values(type_cast: false, trim_empty_rows: nil, pad_empty_rows: nil, pad_empty_cells: nil, &block)
285
+ return to_enum(: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
73
286
 
74
- rows.each(&)
287
+ each_row(trim_empty_rows: trim_empty_rows, pad_empty_rows: pad_empty_rows, pad_empty_cells: pad_empty_cells) do |row|
288
+ block.call(row.values(type_cast: type_cast))
289
+ end
75
290
  end
76
291
 
77
292
  # Iterate over all cells across rows.
@@ -88,6 +303,39 @@ module Xlsxrb
88
303
  cells.each(&)
89
304
  end
90
305
 
306
+ # Returns all images anchored at a specific cell reference or 0-based coordinate.
307
+ #
308
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
309
+ # @param col [Integer, nil] Optional 0-based column index.
310
+ # @return [Array<Elements::Image>]
311
+ # @api public
312
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Array[Elements::Image]
313
+ def images_at(ref_or_row, col = nil)
314
+ ref = if col
315
+ "#{Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
316
+ else
317
+ ref_or_row.to_s.upcase
318
+ end
319
+ images.select { |img| img.cell_ref == ref }
320
+ end
321
+
322
+ # Yields self with images loaded or returns self.
323
+ #
324
+ # @yield [sheet]
325
+ # @yieldparam sheet [Elements::Worksheet]
326
+ # @return [Elements::Worksheet, void]
327
+ # @api public
328
+ #: () { (Elements::Worksheet) -> void } -> void
329
+ #: () -> Elements::Worksheet
330
+ def with_images
331
+ if block_given?
332
+ yield self
333
+ nil
334
+ else
335
+ self
336
+ end
337
+ end
338
+
91
339
  # Returns whether the worksheet is valid according to OOXML specifications.
92
340
  #
93
341
  # @return [Boolean]
@@ -96,6 +344,95 @@ module Xlsxrb
96
344
  errors.empty?
97
345
  end
98
346
 
347
+ # Returns whether the worksheet is hidden (:hidden or :very_hidden).
348
+ #
349
+ # @return [Boolean]
350
+ # @api public
351
+ #: () -> bool
352
+ def hidden?
353
+ @state == :hidden || @state == :very_hidden
354
+ end
355
+
356
+ # Returns whether the worksheet is visible.
357
+ #
358
+ # @return [Boolean]
359
+ # @api public
360
+ #: () -> bool
361
+ def visible?
362
+ @state == :visible
363
+ end
364
+
365
+ # Returns hyperlink metadata for the given cell reference, or nil.
366
+ #
367
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
368
+ # @param col [Integer, nil] Optional 0-based column index if ref_or_row is a row index.
369
+ # @return [Hash, nil]
370
+ # @api public
371
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Hash[Symbol, untyped]?
372
+ def hyperlink(ref_or_row, col = nil)
373
+ ref = if col
374
+ "#{Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
375
+ else
376
+ ref_or_row.to_s.upcase
377
+ end
378
+ @hyperlinks[ref]
379
+ end
380
+
381
+ # Returns comment metadata for the given cell reference, or nil.
382
+ #
383
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
384
+ # @param col [Integer, nil] Optional 0-based column index if ref_or_row is a row index.
385
+ # @return [Hash, nil]
386
+ # @api public
387
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Hash[Symbol, untyped]?
388
+ def comment(ref_or_row, col = nil)
389
+ ref = if col
390
+ "#{Cell.column_letter(col)}#{ref_or_row.to_i + 1}"
391
+ else
392
+ ref_or_row.to_s.upcase
393
+ end
394
+ comments_by_ref[ref]
395
+ end
396
+
397
+ # Returns comments indexed by cell reference.
398
+ #
399
+ # @return [Hash<String, Hash[Symbol, untyped]>]
400
+ # @api public
401
+ #: () -> Hash[String, Hash[Symbol, untyped]]
402
+ def comments_by_ref
403
+ @comments_by_ref ||= @comments.each_with_object({}) do |c, acc|
404
+ r = c[:ref] || c[:cell]
405
+ acc[r.to_s.upcase] = c if r
406
+ end
407
+ end
408
+
409
+ # Access a cell by reference or 0-based coordinates.
410
+ #
411
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
412
+ # @param col [Integer, nil] Optional 0-based column index.
413
+ # @return [Elements::Cell, nil]
414
+ # @api public
415
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> Elements::Cell?
416
+ def cell(ref_or_row, col = nil)
417
+ if col
418
+ row = row_at(ref_or_row.to_i)
419
+ row&.cell_at(col.to_i)
420
+ else
421
+ self[ref_or_row]
422
+ end
423
+ end
424
+
425
+ # Returns the formatted string representation of a cell's value.
426
+ #
427
+ # @param ref_or_row [String, Symbol, Integer] Cell reference (e.g. "A1") or 0-based row index.
428
+ # @param col [Integer, nil] Optional 0-based column index.
429
+ # @return [String, nil]
430
+ # @api public
431
+ #: (String | Symbol | Integer ref_or_row, ?Integer? col) -> String?
432
+ def formatted_value(ref_or_row, col = nil)
433
+ cell(ref_or_row, col)&.formatted_value
434
+ end
435
+
99
436
  # Returns a new Worksheet with the specified cell updated.
100
437
  #
101
438
  # @example
@@ -105,10 +442,12 @@ module Xlsxrb
105
442
  # @param value [Object] The new cell value.
106
443
  # @param style_index [Integer, String, nil] Optional new style index.
107
444
  # @param formula [Elements::Formula, nil] Optional new formula.
445
+ # @param hyperlink [Hash, String, nil] Optional new hyperlink.
446
+ # @param comment [Hash, String, nil] Optional new comment.
108
447
  # @return [Worksheet] A new Worksheet instance.
109
448
  # @api public
110
- #: (String ref, ?value: untyped, ?style_index: Integer | String | nil, ?formula: Elements::Formula?) -> Elements::Worksheet
111
- def update_cell(ref, value: nil, style_index: nil, formula: nil)
449
+ #: (String ref, ?value: untyped, ?style_index: Integer | String | nil, ?formula: Elements::Formula?, ?hyperlink: (Hash[Symbol, untyped] | String)?, ?comment: (Hash[Symbol, untyped] | String)?) -> Elements::Worksheet
450
+ def update_cell(ref, value: nil, style_index: nil, formula: nil, hyperlink: nil, comment: nil)
112
451
  parsed = Cell.parse_ref(ref)
113
452
  raise ArgumentError, "invalid cell reference: #{ref}" unless parsed
114
453
 
@@ -121,10 +460,12 @@ module Xlsxrb
121
460
  existing_cell.with(
122
461
  value: value || existing_cell.value,
123
462
  style_index: style_index || existing_cell.style_index,
124
- formula: formula || existing_cell.formula
463
+ formula: formula || existing_cell.formula,
464
+ hyperlink: hyperlink || existing_cell.hyperlink,
465
+ comment: comment || existing_cell.comment
125
466
  )
126
467
  else
127
- Cell.new(row_index: row_idx, column_index: col_idx, value: value, style_index: style_index, formula: formula)
468
+ Cell.new(row_index: row_idx, column_index: col_idx, value: value, style_index: style_index, formula: formula, hyperlink: hyperlink, comment: comment)
128
469
  end
129
470
 
130
471
  # Replace cell in the existing row
@@ -136,11 +477,18 @@ module Xlsxrb
136
477
  new_rows = rows.map { |r| r.index == row_idx ? new_row : r }
137
478
  else
138
479
  # Row doesn't exist, create it
139
- new_cell = Cell.new(row_index: row_idx, column_index: col_idx, value: value, style_index: style_index, formula: formula)
480
+ new_cell = Cell.new(row_index: row_idx, column_index: col_idx, value: value, style_index: style_index, formula: formula, hyperlink: hyperlink, comment: comment)
140
481
  new_row = Row.new(index: row_idx, cells: [new_cell])
141
482
  new_rows = (rows + [new_row]).sort_by!(&:index)
142
483
  end
143
- with(rows: new_rows)
484
+ new_hyperlinks = hyperlink ? hyperlinks.merge(ref.upcase => hyperlink) : hyperlinks
485
+ new_comments = if comment
486
+ entry = comment.is_a?(Hash) ? comment.merge(ref: ref.upcase) : { ref: ref.upcase, text: comment }
487
+ @comments.reject { |c| (c[:ref] || c[:cell])&.to_s&.upcase == ref.upcase } + [entry]
488
+ else
489
+ @comments
490
+ end
491
+ with(rows: new_rows, hyperlinks: new_hyperlinks, comments: new_comments)
144
492
  end
145
493
 
146
494
  # Returns a new Worksheet with attributes replaced (Data-like behavior).
@@ -154,23 +502,122 @@ module Xlsxrb
154
502
  new_rows = changes.key?(:rows) ? changes[:rows] : rows
155
503
  new_cols = changes.key?(:columns) ? changes[:columns] : columns
156
504
  new_charts = changes.key?(:charts) ? changes[:charts] : charts
505
+ new_cf = if changes.key?(:conditional_formatting)
506
+ changes[:conditional_formatting]
507
+ elsif changes.key?(:conditional_formats)
508
+ changes[:conditional_formats]
509
+ else
510
+ conditional_formatting
511
+ end
512
+ new_dv = changes.key?(:data_validations) ? changes[:data_validations] : data_validations
157
513
  new_unmapped = changes.key?(:unmapped_data) ? changes[:unmapped_data] : unmapped_data
158
514
  new_errors = changes.key?(:errors) ? changes[:errors] : errors
515
+ new_state = changes.key?(:state) ? changes[:state] : state
516
+ new_hyperlinks = changes.key?(:hyperlinks) ? changes[:hyperlinks] : hyperlinks
517
+ new_comments = changes.key?(:comments) ? changes[:comments] : comments
518
+ new_styles = changes.key?(:styles) ? changes[:styles] : styles
519
+ new_date1904 = changes.key?(:date1904) ? changes[:date1904] : date1904
520
+ new_dim = changes.key?(:dimension) ? changes[:dimension] : @dimension
521
+ new_print_area = changes.key?(:print_area) ? changes[:print_area] : print_area
522
+ new_print_titles = changes.key?(:print_titles) ? changes[:print_titles] : print_titles
523
+ new_sparklines = changes.key?(:sparkline_groups) ? changes[:sparkline_groups] : sparkline_groups
524
+ new_trim = changes.key?(:trim_empty_rows) ? changes[:trim_empty_rows] : @trim_empty_rows
525
+ new_merged = changes.key?(:merged_cells) ? changes[:merged_cells] : merged_cells
526
+ new_auto_filter = changes.key?(:auto_filter) ? changes[:auto_filter] : auto_filter
527
+ new_tables = changes.key?(:tables) ? changes[:tables] : tables
528
+ new_shapes = changes.key?(:shapes) ? changes[:shapes] : shapes
529
+ new_freeze_pane = changes.key?(:freeze_pane) ? changes[:freeze_pane] : freeze_pane
530
+ new_page_setup = changes.key?(:page_setup) ? changes[:page_setup] : page_setup
531
+ new_page_margins = changes.key?(:page_margins) ? changes[:page_margins] : page_margins
532
+ new_header_footer = changes.key?(:header_footer) ? changes[:header_footer] : header_footer
533
+ new_print_options = changes.key?(:print_options) ? changes[:print_options] : print_options
534
+ new_sheet_protection = changes.key?(:sheet_protection) ? changes[:sheet_protection] : sheet_protection
535
+ new_sheet_view = changes.key?(:sheet_view) ? changes[:sheet_view] : sheet_view
536
+ new_sheet_properties = changes.key?(:sheet_properties) ? changes[:sheet_properties] : sheet_properties
537
+ new_sheet_format = changes.key?(:sheet_format) ? changes[:sheet_format] : sheet_format
538
+ new_row_breaks = changes.key?(:row_breaks) ? changes[:row_breaks] : row_breaks
539
+ new_col_breaks = changes.key?(:col_breaks) ? changes[:col_breaks] : col_breaks
540
+ new_protected_ranges = changes.key?(:protected_ranges) ? changes[:protected_ranges] : protected_ranges
541
+ new_cell_watches = changes.key?(:cell_watches) ? changes[:cell_watches] : cell_watches
159
542
 
160
543
  self.class.new(
161
544
  name: new_name,
162
545
  rows: new_rows,
163
546
  columns: new_cols,
164
547
  charts: new_charts,
548
+ conditional_formatting: new_cf,
549
+ data_validations: new_dv,
165
550
  unmapped_data: new_unmapped,
166
- errors: new_errors
551
+ errors: new_errors,
552
+ state: new_state,
553
+ hyperlinks: new_hyperlinks,
554
+ comments: new_comments,
555
+ styles: new_styles,
556
+ date1904: new_date1904,
557
+ dimension: new_dim,
558
+ print_area: new_print_area,
559
+ print_titles: new_print_titles,
560
+ sparkline_groups: new_sparklines,
561
+ trim_empty_rows: new_trim,
562
+ merged_cells: new_merged,
563
+ auto_filter: new_auto_filter,
564
+ tables: new_tables,
565
+ shapes: new_shapes,
566
+ freeze_pane: new_freeze_pane,
567
+ page_setup: new_page_setup,
568
+ page_margins: new_page_margins,
569
+ header_footer: new_header_footer,
570
+ print_options: new_print_options,
571
+ sheet_protection: new_sheet_protection,
572
+ sheet_view: new_sheet_view,
573
+ sheet_properties: new_sheet_properties,
574
+ sheet_format: new_sheet_format,
575
+ row_breaks: new_row_breaks,
576
+ col_breaks: new_col_breaks,
577
+ protected_ranges: new_protected_ranges,
578
+ cell_watches: new_cell_watches
167
579
  )
168
580
  end
169
581
 
170
582
  # Support pattern matching.
171
583
  #: (Array[Symbol]?) -> Hash[Symbol, untyped]
172
584
  def deconstruct_keys(_keys)
173
- { name: name, rows: rows, columns: columns, charts: charts, unmapped_data: unmapped_data, errors: errors }
585
+ {
586
+ name: name,
587
+ rows: rows,
588
+ columns: columns,
589
+ charts: charts,
590
+ conditional_formatting: conditional_formatting,
591
+ conditional_formats: conditional_formatting,
592
+ data_validations: data_validations,
593
+ unmapped_data: unmapped_data,
594
+ errors: errors,
595
+ state: state,
596
+ hyperlinks: hyperlinks,
597
+ comments: comments,
598
+ styles: styles,
599
+ date1904: date1904,
600
+ dimension: dimension,
601
+ print_area: print_area,
602
+ print_titles: print_titles,
603
+ sparkline_groups: sparkline_groups,
604
+ trim_empty_rows: @trim_empty_rows,
605
+ merged_cells: merged_cells,
606
+ auto_filter: auto_filter,
607
+ tables: tables,
608
+ shapes: shapes,
609
+ freeze_pane: freeze_pane,
610
+ page_setup: page_setup,
611
+ page_margins: page_margins,
612
+ header_footer: header_footer,
613
+ print_options: print_options,
614
+ sheet_protection: sheet_protection,
615
+ sheet_view: sheet_view,
616
+ sheet_properties: sheet_properties,
617
+ sheet_format: sheet_format,
618
+ row_breaks: row_breaks,
619
+ col_breaks: col_breaks
620
+ }
174
621
  end
175
622
 
176
623
  # Compare worksheets for equality.
@@ -178,13 +625,27 @@ module Xlsxrb
178
625
  def ==(other)
179
626
  return false unless other.is_a?(Worksheet)
180
627
 
181
- name == other.name && rows == other.rows && columns == other.columns && charts == other.charts
628
+ name == other.name && rows == other.rows && columns == other.columns && charts == other.charts &&
629
+ conditional_formatting == other.conditional_formatting && data_validations == other.data_validations &&
630
+ state == other.state && hyperlinks == other.hyperlinks && comments == other.comments &&
631
+ merged_cells == other.merged_cells && auto_filter == other.auto_filter &&
632
+ tables == other.tables && shapes == other.shapes && freeze_pane == other.freeze_pane &&
633
+ page_setup == other.page_setup && page_margins == other.page_margins &&
634
+ header_footer == other.header_footer && print_options == other.print_options &&
635
+ sheet_protection == other.sheet_protection && sheet_view == other.sheet_view &&
636
+ sheet_properties == other.sheet_properties && sheet_format == other.sheet_format &&
637
+ row_breaks == other.row_breaks && col_breaks == other.col_breaks
182
638
  end
183
639
  alias eql? ==
184
640
 
185
641
  #: () -> Integer
186
642
  def hash
187
- [self.class, name, rows, columns, charts].hash
643
+ [
644
+ self.class, name, rows, columns, charts, conditional_formatting, data_validations,
645
+ state, hyperlinks, comments, merged_cells, auto_filter, tables, shapes, freeze_pane,
646
+ page_setup, page_margins, header_footer, print_options, sheet_protection, sheet_view,
647
+ sheet_properties, sheet_format, row_breaks, col_breaks
648
+ ].hash
188
649
  end
189
650
 
190
651
  # Returns self when load is called on an already in-memory Worksheet.
@@ -237,6 +698,35 @@ module Xlsxrb
237
698
  end
238
699
  errs
239
700
  end
701
+
702
+ private
703
+
704
+ #: () -> String?
705
+ def compute_dimension
706
+ return nil if rows.empty?
707
+
708
+ min_r = nil
709
+ max_r = nil
710
+ min_c = nil
711
+ max_c = nil
712
+
713
+ rows.each do |r|
714
+ next if r.cells.empty?
715
+
716
+ min_r = r.index if min_r.nil? || r.index < min_r
717
+ max_r = r.index if max_r.nil? || r.index > max_r
718
+ r.cells.each do |c|
719
+ min_c = c.column_index if min_c.nil? || c.column_index < min_c
720
+ max_c = c.column_index if max_c.nil? || c.column_index > max_c
721
+ end
722
+ end
723
+
724
+ return nil if min_r.nil? || min_c.nil?
725
+
726
+ start_cell = "#{Elements::Cell.column_letter(min_c)}#{min_r + 1}"
727
+ end_cell = "#{Elements::Cell.column_letter(max_c)}#{max_r + 1}"
728
+ start_cell == end_cell ? start_cell : "#{start_cell}:#{end_cell}"
729
+ end
240
730
  end
241
731
  end
242
732
  end
@@ -9,6 +9,8 @@ require_relative "elements/column"
9
9
  require_relative "elements/coordinate_access"
10
10
  require_relative "elements/worksheet"
11
11
  require_relative "elements/workbook"
12
+ require_relative "elements/styles"
13
+ require_relative "elements/image"
12
14
 
13
15
  module Xlsxrb
14
16
  # High-level domain model layer.