xlsxrb 0.1.5 → 0.1.7

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.gem_rbs_collection/nokogiri/1.11/.rbs_meta.yaml +9 -0
  3. data/.gem_rbs_collection/nokogiri/1.11/nokogiri.rbs +2332 -0
  4. data/.gem_rbs_collection/nokogiri/1.11/patch.rbs +4 -0
  5. data/.gem_rbs_collection/rubyzip/3.2/.rbs_meta.yaml +9 -0
  6. data/.gem_rbs_collection/rubyzip/3.2/manifest.yaml +8 -0
  7. data/.gem_rbs_collection/rubyzip/3.2/zip/central_directory.rbs +42 -0
  8. data/.gem_rbs_collection/rubyzip/3.2/zip/compressor.rbs +5 -0
  9. data/.gem_rbs_collection/rubyzip/3.2/zip/constants.rbs +47 -0
  10. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/aes_encryption.rbs +30 -0
  11. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/decrypted_io.rbs +9 -0
  12. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/encryption.rbs +7 -0
  13. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/null_encryption.rbs +19 -0
  14. data/.gem_rbs_collection/rubyzip/3.2/zip/crypto/traditional_encryption.rbs +31 -0
  15. data/.gem_rbs_collection/rubyzip/3.2/zip/decompressor.rbs +18 -0
  16. data/.gem_rbs_collection/rubyzip/3.2/zip/deflater.rbs +12 -0
  17. data/.gem_rbs_collection/rubyzip/3.2/zip/dirtyable.rbs +11 -0
  18. data/.gem_rbs_collection/rubyzip/3.2/zip/dos_time.rbs +13 -0
  19. data/.gem_rbs_collection/rubyzip/3.2/zip/entry.rbs +95 -0
  20. data/.gem_rbs_collection/rubyzip/3.2/zip/entry_set.rbs +31 -0
  21. data/.gem_rbs_collection/rubyzip/3.2/zip/errors.rbs +58 -0
  22. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/aes.rbs +24 -0
  23. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/generic.rbs +17 -0
  24. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/ntfs.rbs +23 -0
  25. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/old_unix.rbs +22 -0
  26. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/universal_time.rbs +30 -0
  27. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unix.rbs +20 -0
  28. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/unknown.rbs +15 -0
  29. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field/zip64.rbs +26 -0
  30. data/.gem_rbs_collection/rubyzip/3.2/zip/extra_field.rbs +21 -0
  31. data/.gem_rbs_collection/rubyzip/3.2/zip/file.rbs +131 -0
  32. data/.gem_rbs_collection/rubyzip/3.2/zip/file_split.rbs +14 -0
  33. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/dir.rbs +33 -0
  34. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/directory_iterator.rbs +21 -0
  35. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file.rbs +63 -0
  36. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/file_stat.rbs +55 -0
  37. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem/zip_file_name_mapper.rbs +35 -0
  38. data/.gem_rbs_collection/rubyzip/3.2/zip/filesystem.rbs +7 -0
  39. data/.gem_rbs_collection/rubyzip/3.2/zip/inflater.rbs +10 -0
  40. data/.gem_rbs_collection/rubyzip/3.2/zip/input_stream.rbs +22 -0
  41. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_input_stream.rbs +29 -0
  42. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras/abstract_output_stream.rbs +17 -0
  43. data/.gem_rbs_collection/rubyzip/3.2/zip/ioextras.rbs +13 -0
  44. data/.gem_rbs_collection/rubyzip/3.2/zip/null_compressor.rbs +10 -0
  45. data/.gem_rbs_collection/rubyzip/3.2/zip/null_decompressor.rbs +8 -0
  46. data/.gem_rbs_collection/rubyzip/3.2/zip/null_input_stream.rbs +6 -0
  47. data/.gem_rbs_collection/rubyzip/3.2/zip/output_stream.rbs +30 -0
  48. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_compressor.rbs +10 -0
  49. data/.gem_rbs_collection/rubyzip/3.2/zip/pass_thru_decompressor.rbs +10 -0
  50. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_directory.rbs +5 -0
  51. data/.gem_rbs_collection/rubyzip/3.2/zip/streamable_stream.rbs +15 -0
  52. data/.gem_rbs_collection/rubyzip/3.2/zip/version.rbs +3 -0
  53. data/.gem_rbs_collection/rubyzip/3.2/zip.rbs +40 -0
  54. data/CHANGELOG.md +28 -0
  55. data/README.md +66 -44
  56. data/Rakefile +2 -1
  57. data/Steepfile +1 -2
  58. data/benchmark.rb +376 -0
  59. data/docs/ARCHITECTURE.md +12 -5
  60. data/docs/visual/VisualGallery.md +8 -0
  61. data/docs/wasm/ruby.wasm +0 -0
  62. data/lib/ruby_lsp/xlsxrb/addon.rb +43 -0
  63. data/lib/ruby_lsp/xlsxrb/completion_listener.rb +769 -0
  64. data/lib/xlsxrb/elements/cell.rb +137 -11
  65. data/lib/xlsxrb/elements/column.rb +22 -0
  66. data/lib/xlsxrb/elements/row.rb +98 -5
  67. data/lib/xlsxrb/elements/types.rb +40 -5
  68. data/lib/xlsxrb/elements/workbook.rb +73 -5
  69. data/lib/xlsxrb/elements/worksheet.rb +110 -1
  70. data/lib/xlsxrb/ooxml/shared_strings_parser.rb +104 -40
  71. data/lib/xlsxrb/ooxml/worksheet_parser.rb +144 -23
  72. data/lib/xlsxrb/ooxml/worksheet_writer.rb +160 -145
  73. data/lib/xlsxrb/ooxml/xml_builder.rb +16 -10
  74. data/lib/xlsxrb/ooxml/zip_reader.rb +104 -18
  75. data/lib/xlsxrb/ooxml/zip_writer.rb +63 -25
  76. data/lib/xlsxrb/style_builder.rb +158 -30
  77. data/lib/xlsxrb/version.rb +1 -1
  78. data/lib/xlsxrb.rb +892 -336
  79. data/rbs_collection.lock.yaml +28 -0
  80. data/sig/generated/xlsxrb/elements/cell.rbs +39 -0
  81. data/sig/generated/xlsxrb/elements/column.rbs +35 -0
  82. data/sig/generated/xlsxrb/elements/row.rbs +39 -0
  83. data/sig/generated/xlsxrb/elements/types.rbs +82 -0
  84. data/sig/generated/xlsxrb/elements/workbook.rbs +32 -0
  85. data/sig/generated/xlsxrb/elements/worksheet.rbs +34 -0
  86. data/sig/generated/xlsxrb/ooxml/reader.rbs +1412 -0
  87. data/sig/generated/xlsxrb/ooxml/shared_strings_parser.rbs +21 -0
  88. data/sig/generated/xlsxrb/ooxml/styles_parser.rbs +44 -0
  89. data/sig/generated/xlsxrb/ooxml/utils.rbs +40 -0
  90. data/sig/generated/xlsxrb/ooxml/workbook_parser.rbs +46 -0
  91. data/sig/generated/xlsxrb/ooxml/workbook_writer.rbs +66 -0
  92. data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +67 -0
  93. data/sig/generated/xlsxrb/ooxml/worksheet_writer.rbs +92 -0
  94. data/sig/generated/xlsxrb/ooxml/writer.rbs +880 -0
  95. data/sig/generated/xlsxrb/ooxml/xml_builder.rbs +45 -0
  96. data/sig/generated/xlsxrb/ooxml/xml_parser.rbs +30 -0
  97. data/sig/generated/xlsxrb/ooxml/zip_generator.rbs +38 -0
  98. data/sig/generated/xlsxrb/ooxml/zip_reader.rbs +45 -0
  99. data/sig/generated/xlsxrb/ooxml/zip_writer.rbs +45 -0
  100. data/sig/generated/xlsxrb/style_builder.rbs +212 -50
  101. data/sig/generated/xlsxrb.rbs +1646 -0
  102. data/sig/rexml.rbs +4 -0
  103. metadata +78 -1
@@ -5,40 +5,112 @@
5
5
  module Xlsxrb
6
6
  module Elements
7
7
  # Represents a single cell in a worksheet.
8
- # All indices are 0-based.
8
+ # All row and column indices are 0-based.
9
+ #
10
+ # @example Access cell properties
11
+ # cell = sheet["A1"]
12
+ # cell.value # raw value
13
+ # cell.ref # "A1"
14
+ # cell.to_i # integer value
15
+ # cell.to_date # Date value
16
+ #
17
+ # @api public
9
18
  Cell = Data.define(:row_index, :column_index, :value, :formula, :style_index, :unmapped_data, :errors) do
10
- def initialize(row_index:, column_index:, value: nil, formula: nil, style_index: nil, unmapped_data: {}, errors: nil)
19
+ # @param row_index [Integer] 0-based row index.
20
+ # @param column_index [Integer] 0-based column index.
21
+ # @param value [Object, nil] The cell's value.
22
+ # @param formula [Elements::Formula, nil] Optional formula.
23
+ # @param style_index [Integer, String, nil] Style identifier.
24
+ # @param unmapped_data [Hash] Additional metadata.
25
+ # @param errors [Array<String>, nil] Validation errors.
26
+ #: (row_index: Integer, column_index: Integer, ?value: untyped, ?formula: Elements::Formula?, ?style_index: Integer | String | nil, ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
27
+ def initialize(row_index:, column_index:, value: nil, formula: nil, style_index: nil, unmapped_data: EMPTY_HASH, errors: nil)
11
28
  computed_errors = errors || self.class.validate(row_index, column_index, value)
12
29
  computed_errors = computed_errors.freeze unless computed_errors.frozen?
13
30
  super(row_index: row_index, column_index: column_index, value: value, formula: formula,
14
31
  style_index: style_index, unmapped_data: unmapped_data, errors: computed_errors)
15
32
  end
16
33
 
34
+ # Returns whether the cell is valid according to OOXML specifications.
35
+ #
36
+ # @return [Boolean]
37
+ #: () -> bool
17
38
  def valid?
18
39
  errors.empty?
19
40
  end
20
41
 
21
- # Excel-style reference (e.g. "A1").
42
+ # Returns the Excel-style reference (e.g. "A1", "B2").
43
+ #
44
+ # @return [String]
45
+ # @api public
46
+ #: () -> String
22
47
  def ref
23
48
  "#{self.class.column_letter(column_index)}#{row_index + 1}"
24
49
  end
25
50
 
51
+ # Access cell attributes by Symbol key.
52
+ #
53
+ # @param key [Symbol] Attribute key (:value, :formula, :style_index, :ref, :column_index, :row_index, :type).
54
+ # @return [Object, nil]
55
+ # @api public
56
+ #: (Symbol key) -> untyped
57
+ def [](key)
58
+ case key
59
+ when :value then value
60
+ when :formula then formula
61
+ when :style_index then style_index
62
+ when :ref then ref
63
+ when :column_index then column_index
64
+ when :row_index then row_index
65
+ when :type
66
+ case value
67
+ when String then "s"
68
+ when true, false then "b"
69
+ end
70
+ end
71
+ end
72
+
73
+ # Returns the cell value.
74
+ #
75
+ # @return [Object, nil]
76
+ # @api public
77
+ #: () -> untyped
26
78
  def content
27
79
  value
28
80
  end
29
81
 
82
+ # Returns the string representation of the cell value.
83
+ #
84
+ # @return [String]
85
+ # @api public
86
+ #: () -> String
30
87
  def to_s
31
88
  value.to_s
32
89
  end
33
90
 
91
+ # Converts the cell value to Integer.
92
+ #
93
+ # @return [Integer]
94
+ # @api public
95
+ #: () -> Integer
34
96
  def to_i
35
97
  value.to_i
36
98
  end
37
99
 
100
+ # Converts the cell value to Float.
101
+ #
102
+ # @return [Float]
103
+ # @api public
104
+ #: () -> Float
38
105
  def to_f
39
106
  value.to_f
40
107
  end
41
108
 
109
+ # Converts the cell value (numeric serial date or date string) to Date.
110
+ #
111
+ # @return [Date, nil]
112
+ # @api public
113
+ #: () -> Date?
42
114
  def to_date
43
115
  return value if value.is_a?(Date)
44
116
 
@@ -54,6 +126,11 @@ module Xlsxrb
54
126
  end
55
127
  end
56
128
 
129
+ # Converts the cell value (numeric serial datetime or datetime string) to Time.
130
+ #
131
+ # @return [Time, nil]
132
+ # @api public
133
+ #: () -> Time?
57
134
  def to_time
58
135
  return value if value.is_a?(Time)
59
136
 
@@ -82,7 +159,16 @@ module Xlsxrb
82
159
  result.freeze
83
160
  end.freeze
84
161
 
85
- # Converts a 0-based column index to a letter (0 -> "A", 25 -> "Z", 26 -> "AA").
162
+ # Converts a 0-based column index to an Excel letter (0 -> "A", 25 -> "Z", 26 -> "AA").
163
+ #
164
+ # @example
165
+ # Cell.column_letter(0) #=> "A"
166
+ # Cell.column_letter(26) #=> "AA"
167
+ #
168
+ # @param index [Integer] 0-based column index.
169
+ # @return [String] Excel column letter.
170
+ # @api public
171
+ #: (Integer index) -> String
86
172
  def self.column_letter(index)
87
173
  raise ArgumentError, "Column index must be a non-negative Integer, got #{index.inspect}" unless index.is_a?(Integer) && index >= 0
88
174
 
@@ -99,7 +185,15 @@ module Xlsxrb
99
185
  end
100
186
 
101
187
  # Converts a column letter (e.g. "A", :AA) to a 0-based column index.
102
- # If passed an integer or string/symbol representing an integer, it validates and returns the integer.
188
+ #
189
+ # @example
190
+ # Cell.column_index("A") #=> 0
191
+ # Cell.column_index(:AA) #=> 26
192
+ #
193
+ # @param letter [String, Symbol, Integer] Column letter or integer index.
194
+ # @return [Integer] 0-based column index.
195
+ # @api public
196
+ #: (String | Symbol | Integer letter) -> Integer
103
197
  def self.column_index(letter)
104
198
  if letter.is_a?(Integer)
105
199
  raise ArgumentError, "Column index must be >= 0, got #{letter}" if letter.negative?
@@ -121,20 +215,52 @@ module Xlsxrb
121
215
  end
122
216
 
123
217
  # Parses an Excel-style reference to [row_index, col_index] (both 0-based).
218
+ #
219
+ # @example
220
+ # Cell.parse_ref("A1") #=> [0, 0]
221
+ # Cell.parse_ref("C10") #=> [9, 2]
222
+ #
223
+ # @param ref [String, nil] Excel cell reference.
224
+ # @return [Array(Integer, Integer), nil] 0-based [row_index, col_index].
225
+ # @api public
226
+ #: (String? ref) -> [Integer, Integer]?
124
227
  def self.parse_ref(ref)
125
- match = ref.match(/\A([A-Z]+)(\d+)\z/)
126
- return nil unless match
228
+ return nil unless ref
229
+
230
+ bytes = ref.b
231
+ len = bytes.bytesize
232
+ col = 0
233
+ i = 0
234
+ while i < len
235
+ b = bytes.getbyte(i)
236
+ if b.between?(65, 90)
237
+ col = (col * 26) + (b - 64)
238
+ i += 1
239
+ elsif b.between?(97, 122)
240
+ col = (col * 26) + (b - 96)
241
+ i += 1
242
+ else
243
+ break
244
+ end
245
+ end
246
+ return nil if i.zero? || i == len
127
247
 
128
- col = match[1].chars.reduce(0) { |acc, c| (acc * 26) + (c.ord - "A".ord + 1) } - 1
129
- row = match[2].to_i - 1
130
- [row, col]
248
+ row = bytes.byteslice(i, len - i).to_i - 1
249
+ [row, col - 1]
131
250
  end
132
251
 
252
+ # Validates cell coordinates and value type against OOXML specifications.
253
+ #
254
+ # @param row_index [Integer]
255
+ # @param column_index [Integer]
256
+ # @param value [Object]
257
+ # @return [Array<String>] List of errors.
258
+ #: (untyped row_index, untyped column_index, untyped value) -> Array[String]
133
259
  def self.validate(row_index, column_index, value)
134
260
  if row_index.is_a?(Integer) && row_index >= 0 && row_index < 1_048_576 &&
135
261
  column_index.is_a?(Integer) && column_index >= 0 && column_index < 16_384 &&
136
262
  (value.nil? || value.is_a?(String) || value.is_a?(Numeric) || value == true || value == false || value.is_a?(Date) || value.is_a?(Time) || value.is_a?(Formula) || (value.is_a?(Hash) && value.key?(:formula)) || value.is_a?(RichText) || value.is_a?(CellError))
137
- return [].freeze
263
+ return EMPTY_ERRORS
138
264
  end
139
265
 
140
266
  errs = []
@@ -6,7 +6,20 @@ module Xlsxrb
6
6
  module Elements
7
7
  # Represents column formatting in a worksheet.
8
8
  # index is 0-based.
9
+ #
10
+ # @example
11
+ # col = Elements::Column.new(index: 0, width: 25.0)
12
+ #
13
+ # @api public
9
14
  Column = Data.define(:index, :width, :hidden, :custom_width, :outline_level, :unmapped_data, :errors) do
15
+ # @param index [Integer] 0-based column index.
16
+ # @param width [Float, Integer, nil] Column width in characters.
17
+ # @param hidden [Boolean] Whether the column is hidden.
18
+ # @param custom_width [Boolean] Whether custom width flag is set.
19
+ # @param outline_level [Integer, nil] Grouping/outline level.
20
+ # @param unmapped_data [Hash] Additional metadata.
21
+ # @param errors [Array<String>, nil] Validation errors.
22
+ #: (index: Integer, ?width: Float | Integer | nil, ?hidden: bool, ?custom_width: bool, ?outline_level: Integer | nil, ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
10
23
  def initialize(index:, width: nil, hidden: false, custom_width: false, outline_level: nil,
11
24
  unmapped_data: {}, errors: nil)
12
25
  computed_errors = errors || self.class.validate(index)
@@ -15,10 +28,19 @@ module Xlsxrb
15
28
  errors: computed_errors.freeze)
16
29
  end
17
30
 
31
+ # Returns whether the column definition is valid according to OOXML specifications.
32
+ #
33
+ # @return [Boolean]
34
+ #: () -> bool
18
35
  def valid?
19
36
  errors.empty?
20
37
  end
21
38
 
39
+ # Validates column index against OOXML limits.
40
+ #
41
+ # @param index [Integer]
42
+ # @return [Array<String>] List of errors.
43
+ #: (untyped index) -> Array[String]
22
44
  def self.validate(index)
23
45
  errs = []
24
46
  errs << "index must be a non-negative Integer (got #{index.inspect})" if !index.is_a?(Integer) || index.negative?
@@ -5,12 +5,28 @@
5
5
  module Xlsxrb
6
6
  module Elements
7
7
  # Represents a single row in a worksheet.
8
- # index is 0-based.
8
+ # All row and column indices are 0-based.
9
+ #
10
+ # @example Access cell by index or symbol
11
+ # row = sheet.row_at(0)
12
+ # cell = row[0] # cell at column 0
13
+ # row.to_a # array of cell values
14
+ #
15
+ # @api public
9
16
  Row = Data.define(:index, :cells, :height, :hidden, :custom_height, :outline_level, :unmapped_data, :errors) do
10
17
  include Enumerable
11
18
 
12
- def initialize(index:, cells: [], height: nil, hidden: false, custom_height: false, outline_level: nil,
13
- unmapped_data: {}, errors: nil)
19
+ # @param index [Integer] 0-based row index.
20
+ # @param cells [Array<Elements::Cell>] Cells in this row.
21
+ # @param height [Float, Integer, nil] Row height in points.
22
+ # @param hidden [Boolean] Whether the row is hidden.
23
+ # @param custom_height [Boolean] Whether custom height is set.
24
+ # @param outline_level [Integer, nil] Grouping/outline level.
25
+ # @param unmapped_data [Hash] Additional metadata.
26
+ # @param errors [Array<String>, nil] Validation errors.
27
+ #: (index: Integer, ?cells: Array[Elements::Cell], ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?outline_level: Integer | nil, ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
28
+ def initialize(index:, cells: EMPTY_CELLS, height: nil, hidden: false, custom_height: false, outline_level: nil,
29
+ unmapped_data: EMPTY_HASH, errors: nil)
14
30
  computed_errors = errors || self.class.validate(index, cells)
15
31
  computed_errors = computed_errors.freeze unless computed_errors.frozen?
16
32
  cells = cells.freeze unless cells.frozen?
@@ -19,22 +35,80 @@ module Xlsxrb
19
35
  unmapped_data: unmapped_data, errors: computed_errors)
20
36
  end
21
37
 
38
+ # Access a cell by 0-based column index, or access row attributes via Symbol.
39
+ #
40
+ # @example
41
+ # row[0] #=> Cell at column 0
42
+ # row[:height] #=> 25.0
43
+ # row[:cells] #=> [Cell, Cell, ...]
44
+ #
45
+ # @param col_index [Integer, Symbol] Column index or attribute symbol.
46
+ # @return [Elements::Cell, Object, nil]
47
+ # @api public
48
+ #: (Integer | Symbol col_index) -> untyped
22
49
  def [](col_index)
23
- cells[col_index]
50
+ case col_index
51
+ when Symbol
52
+ case col_index
53
+ when :cells then cells
54
+ when :index then index
55
+ when :height then height
56
+ when :hidden then hidden
57
+ when :custom_height then custom_height
58
+ when :outline_level then outline_level
59
+ when :attrs then { height: height, hidden: hidden, custom_height: custom_height, outline_level: outline_level }
60
+ end
61
+ else
62
+ cells[col_index]
63
+ end
24
64
  end
25
65
 
66
+ # Iterate over cells in this row.
67
+ #
68
+ # @example
69
+ # row.each do |cell|
70
+ # puts cell.value
71
+ # end
72
+ #
73
+ # @yield [cell]
74
+ # @yieldparam cell [Elements::Cell]
75
+ # @return [Enumerator, void]
76
+ # @api public
77
+ #: () { (Elements::Cell) -> void } -> void
78
+ #: | () -> Enumerator[Elements::Cell, void]
26
79
  def each(&)
27
80
  return to_enum(:each) unless block_given?
28
81
 
29
82
  cells.each(&)
30
83
  end
31
84
 
85
+ # Iterate over cells in this row.
86
+ #
87
+ # @example
88
+ # row.each_cell do |cell|
89
+ # puts "#{cell.ref}: #{cell.value}"
90
+ # end
91
+ #
92
+ # @yield [cell]
93
+ # @yieldparam cell [Elements::Cell]
94
+ # @return [Enumerator, void]
95
+ # @api public
96
+ #: () { (Elements::Cell) -> void } -> void
97
+ #: | () -> Enumerator[Elements::Cell, void]
32
98
  def each_cell(&)
33
99
  return to_enum(:each_cell) unless block_given?
34
100
 
35
101
  cells.each(&)
36
102
  end
37
103
 
104
+ # Convert row cells to an Array of raw values.
105
+ #
106
+ # @example
107
+ # row.to_a #=> ["ID", "Name", "Total"]
108
+ #
109
+ # @return [Array<Object>]
110
+ # @api public
111
+ #: () -> Array[untyped]
38
112
  def to_a
39
113
  return [] if cells.empty?
40
114
 
@@ -46,16 +120,29 @@ module Xlsxrb
46
120
  arr
47
121
  end
48
122
 
123
+ # Returns whether the row is valid according to OOXML specifications.
124
+ #
125
+ # @return [Boolean]
126
+ #: () -> bool
49
127
  def valid?
50
128
  errors.empty?
51
129
  end
52
130
 
53
131
  # Returns the cell at the given 0-based column index, or nil.
132
+ #
133
+ # @param column_index [Integer] 0-based column index.
134
+ # @return [Elements::Cell, nil]
135
+ # @api public
136
+ #: (Integer column_index) -> Elements::Cell?
54
137
  def cell_at(column_index)
55
138
  cells.find { |c| c.column_index == column_index }
56
139
  end
57
140
 
58
141
  # Returns cell values as an Array (sparse columns get nil).
142
+ #
143
+ # @return [Array<Object>]
144
+ # @api public
145
+ #: () -> Array[untyped]
59
146
  def values
60
147
  return [] if cells.empty?
61
148
 
@@ -65,8 +152,14 @@ module Xlsxrb
65
152
  result
66
153
  end
67
154
 
155
+ # Validates row index and cells against OOXML limits.
156
+ #
157
+ # @param index [Integer]
158
+ # @param cells [Array<Elements::Cell>]
159
+ # @return [Array<String>] List of errors.
160
+ #: (untyped index, untyped cells) -> Array[String]
68
161
  def self.validate(index, cells)
69
- return [].freeze if index.is_a?(Integer) && index >= 0 && index < 1_048_576 && cells.is_a?(Array)
162
+ return EMPTY_ERRORS if index.is_a?(Integer) && index >= 0 && index < 1_048_576 && cells.is_a?(Array)
70
163
 
71
164
  errs = []
72
165
  if !index.is_a?(Integer) || index.negative?
@@ -4,32 +4,67 @@
4
4
 
5
5
  module Xlsxrb
6
6
  module Elements
7
- # Represents a formula with an optional cached value.
8
- # Optional: type (:shared, :array), ref (range), shared_index (si for shared formulas)
7
+ EMPTY_ERRORS = [].freeze
8
+ EMPTY_HASH = {}.freeze
9
+ EMPTY_CELLS = [].freeze
10
+
11
+ # Represents an Excel formula with an optional cached value and calculation properties.
12
+ #
13
+ # @example Create a formula
14
+ # formula = Elements::Formula.new(expression: "SUM(A1:A10)")
15
+ #
16
+ # @api public
9
17
  Formula = Data.define(:expression, :cached_value, :type, :ref, :shared_index, :calculate_always, :aca, :bx, :dt2d, :dtr, :r1, :r2) do
18
+ # @param expression [String] Excel formula expression without leading '=' (e.g. "SUM(A1:A10)").
19
+ # @param cached_value [Object, nil] Optional precomputed value.
20
+ # @param type [Symbol, String, nil] Formula type (:shared, :array, etc.).
21
+ # @param ref [String, nil] Target cell or range reference.
22
+ # @param shared_index [Integer, nil] Shared formula index.
23
+ # @param calculate_always [Boolean, nil] Force Excel to recalculate on open.
24
+ # @param aca [Boolean, nil] Always calculate array attribute.
25
+ # @param bx [Boolean, nil] Assigns to array formula.
26
+ # @param dt2d [Boolean, nil] 2D data table reference.
27
+ # @param dtr [Boolean, nil] 1D data table reference.
28
+ # @param r1 [String, nil] First table reference.
29
+ # @param r2 [String, nil] Second table reference.
30
+ #: (expression: String, ?cached_value: untyped, ?type: untyped, ?ref: String?, ?shared_index: Integer?, ?calculate_always: bool?, ?aca: bool?, ?bx: bool?, ?dt2d: bool?, ?dtr: bool?, ?r1: String?, ?r2: String?) -> void
10
31
  def initialize(expression:, cached_value: nil, type: nil, ref: nil, shared_index: nil, calculate_always: nil, aca: nil, bx: nil, dt2d: nil, dtr: nil, r1: nil, r2: nil) # rubocop:disable Naming/MethodParameterName
11
32
  super
12
33
  end
13
34
  end
14
35
 
15
36
  # Represents a cell error value (e.g. #N/A, #REF!, #DIV/0!).
37
+ # @api public
16
38
  VALID_ERROR_CODES = %w[#NULL! #DIV/0! #VALUE! #REF! #NAME? #NUM! #N/A #GETTING_DATA].freeze
17
39
  CellError = Data.define(:code) do
40
+ # @param code [String] Valid error code string (e.g. "#N/A").
41
+ #: (code: String) -> void
18
42
  def initialize(code:)
19
43
  raise ArgumentError, "invalid error code: #{code.inspect} (must be one of #{VALID_ERROR_CODES.join(", ")})" unless VALID_ERROR_CODES.include?(code)
20
44
 
21
45
  super
22
46
  end
23
47
 
48
+ # @return [String]
49
+ # @api public
50
+ #: () -> String
24
51
  def to_s
25
52
  code
26
53
  end
27
54
  end
28
55
 
29
- # Represents a rich text string with formatting runs.
30
- # runs: array of hashes, each with :text and optional :font (hash of font properties).
31
- # Font properties: :bold, :italic, :underline, :sz, :color, :name
56
+ # Represents a rich text string with multiple formatting runs.
57
+ #
58
+ # @example
59
+ # rt = Elements::RichText.new(runs: [{ text: "Hello ", font: { bold: true } }, { text: "World" }])
60
+ #
61
+ # @api public
32
62
  RichText = Data.define(:runs) do
63
+ # Returns concatenated plain text of all runs.
64
+ #
65
+ # @return [String]
66
+ # @api public
67
+ #: () -> String
33
68
  def to_s
34
69
  runs.map { |r| r[:text] }.join
35
70
  end
@@ -4,25 +4,63 @@
4
4
 
5
5
  module Xlsxrb
6
6
  module Elements
7
- # Represents an entire XLSX workbook.
7
+ # Represents an entire in-memory XLSX workbook.
8
+ #
9
+ # @example Access sheets
10
+ # workbook = Xlsxrb.read("report.xlsx")
11
+ # sheet = workbook.sheet(0) # or workbook["Sheet1"]
12
+ # workbook.each { |s| puts s.name }
13
+ #
14
+ # @api public
8
15
  Workbook = Data.define(:sheets, :shared_strings, :styles, :unmapped_data, :errors) do
9
16
  include Enumerable
10
17
 
18
+ # @param sheets [Array<Elements::Worksheet>] Worksheets in the workbook.
19
+ # @param shared_strings [Array<String>] Shared strings table.
20
+ # @param styles [Hash] Styles definition.
21
+ # @param unmapped_data [Hash] Additional metadata for round-tripping.
22
+ # @param errors [Array<String>, nil] Validation errors.
23
+ #: (?sheets: Array[Elements::Worksheet], ?shared_strings: Array[String], ?styles: Hash[untyped, untyped], ?unmapped_data: Hash[untyped, untyped], ?errors: Array[String]?) -> void
11
24
  def initialize(sheets: [], shared_strings: [], styles: {}, unmapped_data: {}, errors: nil)
12
25
  computed_errors = errors || self.class.validate(sheets)
13
26
  super(sheets: sheets.freeze, shared_strings: shared_strings.freeze, styles: styles,
14
27
  unmapped_data: unmapped_data, errors: computed_errors.freeze)
15
28
  end
16
29
 
30
+ # Iterate over worksheets.
31
+ #
32
+ # @example
33
+ # workbook.each do |sheet|
34
+ # puts sheet.name
35
+ # end
36
+ #
37
+ # @yield [sheet]
38
+ # @yieldparam sheet [Elements::Worksheet]
39
+ # @return [Enumerator, void]
40
+ #: () { (Elements::Worksheet) -> void } -> void
41
+ #: | () -> Enumerator[Elements::Worksheet, void]
17
42
  def each(&)
18
43
  sheets.each(&)
19
44
  end
20
45
 
46
+ # Returns whether the workbook is valid according to ECMA-376 rules.
47
+ #
48
+ # @return [Boolean]
49
+ #: () -> bool
21
50
  def valid?
22
51
  errors.empty?
23
52
  end
24
53
 
25
- # Returns the sheet at the given 0-based index or by name.
54
+ # Returns the worksheet at the given 0-based index or by name.
55
+ #
56
+ # @example
57
+ # wb.sheet(0)
58
+ # wb.sheet("Sales")
59
+ #
60
+ # @param identifier [Integer, String] 0-based index or sheet name.
61
+ # @return [Elements::Worksheet, nil]
62
+ # @api public
63
+ #: (?Integer | String identifier) -> Elements::Worksheet?
26
64
  def sheet(identifier = 0)
27
65
  case identifier
28
66
  when Integer
@@ -34,7 +72,20 @@ module Xlsxrb
34
72
  alias_method :[], :sheet
35
73
 
36
74
  # Returns a new Workbook with the specified sheet updated.
37
- # If a block is given, it yields the matched sheet and expects a new Worksheet back.
75
+ # Yields the matched worksheet to the block, which must return a new Worksheet.
76
+ #
77
+ # @example
78
+ # new_wb = wb.update_sheet("Sheet1") do |sheet|
79
+ # sheet.update_cell("A1", value: "New Title")
80
+ # end
81
+ #
82
+ # @param identifier [Integer, String] 0-based index or sheet name.
83
+ # @yield [sheet]
84
+ # @yieldparam sheet [Elements::Worksheet] The worksheet to update.
85
+ # @yieldreturn [Elements::Worksheet] The modified worksheet.
86
+ # @return [Elements::Workbook] A new Workbook instance.
87
+ # @api public
88
+ #: (Integer | String identifier) { (Elements::Worksheet) -> Elements::Worksheet } -> Elements::Workbook
38
89
  def update_sheet(identifier)
39
90
  raise ArgumentError, "block is required" unless block_given?
40
91
 
@@ -48,16 +99,33 @@ module Xlsxrb
48
99
  with(sheets: new_sheets)
49
100
  end
50
101
 
51
- # Returns sheet names.
102
+ # Returns an Array of all worksheet names.
103
+ #
104
+ # @return [Array<String>]
105
+ # @api public
106
+ #: () -> Array[String]
52
107
  def sheet_names
53
108
  sheets.map(&:name)
54
109
  end
55
110
 
56
- # Save the workbook to a file.
111
+ # Save the workbook to an XLSX file.
112
+ #
113
+ # @example
114
+ # wb.save("output.xlsx")
115
+ #
116
+ # @param filepath [String, IO] Destination file path or IO stream.
117
+ # @return [void]
118
+ # @api public
119
+ #: (untyped filepath) -> void
57
120
  def save(filepath)
58
121
  Xlsxrb.write(filepath, self)
59
122
  end
60
123
 
124
+ # Validates workbook structure according to OOXML specifications.
125
+ #
126
+ # @param sheets [Array<Elements::Worksheet>]
127
+ # @return [Array<String>] List of error messages.
128
+ #: (untyped sheets) -> Array[String]
61
129
  def self.validate(sheets)
62
130
  errs = []
63
131
  errs << "sheets must be an Array (got #{sheets.class})" unless sheets.is_a?(Array)