xlsxrb 0.1.6 → 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.
@@ -5,8 +5,25 @@
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
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
10
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?
@@ -14,15 +31,29 @@ module Xlsxrb
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
26
57
  def [](key)
27
58
  case key
28
59
  when :value then value
@@ -39,22 +70,47 @@ module Xlsxrb
39
70
  end
40
71
  end
41
72
 
73
+ # Returns the cell value.
74
+ #
75
+ # @return [Object, nil]
76
+ # @api public
77
+ #: () -> untyped
42
78
  def content
43
79
  value
44
80
  end
45
81
 
82
+ # Returns the string representation of the cell value.
83
+ #
84
+ # @return [String]
85
+ # @api public
86
+ #: () -> String
46
87
  def to_s
47
88
  value.to_s
48
89
  end
49
90
 
91
+ # Converts the cell value to Integer.
92
+ #
93
+ # @return [Integer]
94
+ # @api public
95
+ #: () -> Integer
50
96
  def to_i
51
97
  value.to_i
52
98
  end
53
99
 
100
+ # Converts the cell value to Float.
101
+ #
102
+ # @return [Float]
103
+ # @api public
104
+ #: () -> Float
54
105
  def to_f
55
106
  value.to_f
56
107
  end
57
108
 
109
+ # Converts the cell value (numeric serial date or date string) to Date.
110
+ #
111
+ # @return [Date, nil]
112
+ # @api public
113
+ #: () -> Date?
58
114
  def to_date
59
115
  return value if value.is_a?(Date)
60
116
 
@@ -70,6 +126,11 @@ module Xlsxrb
70
126
  end
71
127
  end
72
128
 
129
+ # Converts the cell value (numeric serial datetime or datetime string) to Time.
130
+ #
131
+ # @return [Time, nil]
132
+ # @api public
133
+ #: () -> Time?
73
134
  def to_time
74
135
  return value if value.is_a?(Time)
75
136
 
@@ -98,7 +159,16 @@ module Xlsxrb
98
159
  result.freeze
99
160
  end.freeze
100
161
 
101
- # 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
102
172
  def self.column_letter(index)
103
173
  raise ArgumentError, "Column index must be a non-negative Integer, got #{index.inspect}" unless index.is_a?(Integer) && index >= 0
104
174
 
@@ -115,7 +185,15 @@ module Xlsxrb
115
185
  end
116
186
 
117
187
  # Converts a column letter (e.g. "A", :AA) to a 0-based column index.
118
- # 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
119
197
  def self.column_index(letter)
120
198
  if letter.is_a?(Integer)
121
199
  raise ArgumentError, "Column index must be >= 0, got #{letter}" if letter.negative?
@@ -137,6 +215,15 @@ module Xlsxrb
137
215
  end
138
216
 
139
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]?
140
227
  def self.parse_ref(ref)
141
228
  return nil unless ref
142
229
 
@@ -162,6 +249,13 @@ module Xlsxrb
162
249
  [row, col - 1]
163
250
  end
164
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]
165
259
  def self.validate(row_index, column_index, value)
166
260
  if row_index.is_a?(Integer) && row_index >= 0 && row_index < 1_048_576 &&
167
261
  column_index.is_a?(Integer) && column_index >= 0 && column_index < 16_384 &&
@@ -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,10 +5,26 @@
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
 
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
12
28
  def initialize(index:, cells: EMPTY_CELLS, height: nil, hidden: false, custom_height: false, outline_level: nil,
13
29
  unmapped_data: EMPTY_HASH, errors: nil)
14
30
  computed_errors = errors || self.class.validate(index, cells)
@@ -19,6 +35,17 @@ 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
50
  case col_index
24
51
  when Symbol
@@ -36,18 +63,52 @@ module Xlsxrb
36
63
  end
37
64
  end
38
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]
39
79
  def each(&)
40
80
  return to_enum(:each) unless block_given?
41
81
 
42
82
  cells.each(&)
43
83
  end
44
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]
45
98
  def each_cell(&)
46
99
  return to_enum(:each_cell) unless block_given?
47
100
 
48
101
  cells.each(&)
49
102
  end
50
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]
51
112
  def to_a
52
113
  return [] if cells.empty?
53
114
 
@@ -59,16 +120,29 @@ module Xlsxrb
59
120
  arr
60
121
  end
61
122
 
123
+ # Returns whether the row is valid according to OOXML specifications.
124
+ #
125
+ # @return [Boolean]
126
+ #: () -> bool
62
127
  def valid?
63
128
  errors.empty?
64
129
  end
65
130
 
66
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?
67
137
  def cell_at(column_index)
68
138
  cells.find { |c| c.column_index == column_index }
69
139
  end
70
140
 
71
141
  # Returns cell values as an Array (sparse columns get nil).
142
+ #
143
+ # @return [Array<Object>]
144
+ # @api public
145
+ #: () -> Array[untyped]
72
146
  def values
73
147
  return [] if cells.empty?
74
148
 
@@ -78,6 +152,12 @@ module Xlsxrb
78
152
  result
79
153
  end
80
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]
81
161
  def self.validate(index, cells)
82
162
  return EMPTY_ERRORS if index.is_a?(Integer) && index >= 0 && index < 1_048_576 && cells.is_a?(Array)
83
163
 
@@ -8,32 +8,63 @@ module Xlsxrb
8
8
  EMPTY_HASH = {}.freeze
9
9
  EMPTY_CELLS = [].freeze
10
10
 
11
- # Represents a formula with an optional cached value.
12
- # Optional: type (:shared, :array), ref (range), shared_index (si for shared formulas)
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
13
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
14
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
15
32
  super
16
33
  end
17
34
  end
18
35
 
19
36
  # Represents a cell error value (e.g. #N/A, #REF!, #DIV/0!).
37
+ # @api public
20
38
  VALID_ERROR_CODES = %w[#NULL! #DIV/0! #VALUE! #REF! #NAME? #NUM! #N/A #GETTING_DATA].freeze
21
39
  CellError = Data.define(:code) do
40
+ # @param code [String] Valid error code string (e.g. "#N/A").
41
+ #: (code: String) -> void
22
42
  def initialize(code:)
23
43
  raise ArgumentError, "invalid error code: #{code.inspect} (must be one of #{VALID_ERROR_CODES.join(", ")})" unless VALID_ERROR_CODES.include?(code)
24
44
 
25
45
  super
26
46
  end
27
47
 
48
+ # @return [String]
49
+ # @api public
50
+ #: () -> String
28
51
  def to_s
29
52
  code
30
53
  end
31
54
  end
32
55
 
33
- # Represents a rich text string with formatting runs.
34
- # runs: array of hashes, each with :text and optional :font (hash of font properties).
35
- # 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
36
62
  RichText = Data.define(:runs) do
63
+ # Returns concatenated plain text of all runs.
64
+ #
65
+ # @return [String]
66
+ # @api public
67
+ #: () -> String
37
68
  def to_s
38
69
  runs.map { |r| r[:text] }.join
39
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)