xlsxrb 0.1.7 → 0.1.8

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.
data/docs/wasm/ruby.wasm CHANGED
Binary file
@@ -12,7 +12,7 @@ module RubyLsp
12
12
  # This add-on serves as a bridge/polyfill for current Ruby LSP environments.
13
13
  # While xlsxrb ships with complete RBS signatures (`sig/generated/`), Ruby LSP's
14
14
  # type inferrer does not yet perform automatic static type inference from method
15
- # block signatures to block parameters (e.g., `Xlsxrb.generate do |wb|`).
15
+ # block signatures to block parameters (e.g., `Xlsxrb.write do |wb|`).
16
16
  #
17
17
  # This add-on enables immediate out-of-the-box autocompletion and rich Markdown
18
18
  # documentation across all public block arguments.
@@ -693,9 +693,9 @@ module RubyLsp
693
693
 
694
694
  if caller_receiver_is_xlsxrb?(caller_receiver)
695
695
  case method_name
696
- when :generate then return :stream_writer
696
+ when :write then return :stream_writer
697
+ when :read then return :stream_sheet
697
698
  when :build then return :workbook_builder
698
- when :foreach then return :stream_sheet
699
699
  when :modify then return :workbook
700
700
  end
701
701
  end
@@ -718,25 +718,23 @@ module RubyLsp
718
718
  def infer_each_block_target(caller_receiver)
719
719
  if receiver_matches?(caller_receiver, %i[workbook wb])
720
720
  :worksheet
721
- elsif receiver_matches?(caller_receiver, %i[row r])
721
+ elsif receiver_matches?(caller_receiver, %i[row r stream_row])
722
722
  :cell
723
723
  else
724
724
  :row
725
725
  end
726
726
  end
727
727
 
728
- def infer_from_variable_name(receiver_name)
729
- case receiver_name
730
- when :wb, :stream_writer, :w
731
- :stream_writer
732
- when :workbook
733
- :workbook
734
- when :s, :ws, :sheet
728
+ def infer_from_variable_name(name)
729
+ case name
730
+ when :wb, :workbook
731
+ :workbook_builder
732
+ when :s, :sheet, :ws, :worksheet
735
733
  :worksheet_proxy
736
- when :worksheet
737
- :worksheet
738
- when :stream_sheet
734
+ when :stream_sheet, :ss
739
735
  :stream_sheet
736
+ when :stream_writer, :sw
737
+ :stream_writer
740
738
  when :r, :row
741
739
  :row
742
740
  when :c, :cell
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rbs_inline: enabled
4
+
5
+ module Xlsxrb
6
+ module Elements
7
+ # Mixin providing coordinate-based and random-access cell/row lookups
8
+ # for in-memory worksheet structures.
9
+ #
10
+ # Expects the including class to provide a `#rows` method returning an `Array<Elements::Row>`.
11
+ #
12
+ # @api public
13
+ module CoordinateAccess
14
+ # Returns a Hash mapping Excel cell references (e.g. "A1") to Cell objects.
15
+ #
16
+ # @return [Hash<String, Elements::Cell>]
17
+ #: () -> Hash[String, Elements::Cell]
18
+ def cells_hash
19
+ h = {}
20
+ rows.each do |r|
21
+ r.cells.each do |c|
22
+ ref = "#{Cell.column_letter(c.column_index)}#{c.row_index + 1}"
23
+ h[ref] = c
24
+ end
25
+ end
26
+ h
27
+ end
28
+
29
+ # Returns all cells ordered by row and column index.
30
+ #
31
+ # @return [Array<Elements::Cell>]
32
+ # @api public
33
+ #: () -> Array[Elements::Cell]
34
+ def cells
35
+ cells_hash.values.sort_by { |c| [c.row_index, c.column_index] }
36
+ end
37
+
38
+ # Access a cell by its Excel-style reference (e.g. "A1").
39
+ #
40
+ # @example
41
+ # sheet["A1"] #=> #<Elements::Cell value="Hello">
42
+ #
43
+ # @param ref [String, Symbol] Cell reference (e.g. "A1" or :A1).
44
+ # @return [Elements::Cell, nil]
45
+ # @api public
46
+ #: (String | Symbol ref) -> Elements::Cell?
47
+ def [](ref)
48
+ cells_hash[ref.to_s.upcase]
49
+ end
50
+
51
+ # Returns the row at the given 0-based index, or nil.
52
+ #
53
+ # @param index [Integer] 0-based row index.
54
+ # @return [Elements::Row, nil]
55
+ # @api public
56
+ #: (Integer index) -> Elements::Row?
57
+ def row_at(index)
58
+ rows.find { |r| r.index == index }
59
+ end
60
+
61
+ # Returns the first row in the sheet, or nil.
62
+ #
63
+ # @return [Elements::Row, nil]
64
+ # @api public
65
+ #: () -> Elements::Row?
66
+ def first_row
67
+ rows.min_by(&:index)
68
+ end
69
+
70
+ # Returns the last row in the sheet, or nil.
71
+ #
72
+ # @return [Elements::Row, nil]
73
+ # @api public
74
+ #: () -> Elements::Row?
75
+ def last_row
76
+ rows.max_by(&:index)
77
+ end
78
+
79
+ # Returns the raw cell value at the given Excel-style reference (e.g. "A1").
80
+ #
81
+ # @example
82
+ # sheet.cell_value("A1") #=> "Sales Report"
83
+ #
84
+ # @param ref [String] Cell reference (e.g. "A1").
85
+ # @return [Object, nil]
86
+ # @api public
87
+ #: (String ref) -> untyped
88
+ def cell_value(ref)
89
+ parsed = Cell.parse_ref(ref)
90
+ return nil unless parsed
91
+
92
+ row_idx, col_idx = parsed
93
+ row = row_at(row_idx)
94
+ return nil unless row
95
+
96
+ cell = row.cell_at(col_idx)
97
+ cell&.value
98
+ end
99
+ end
100
+ end
101
+ end
@@ -42,6 +42,7 @@ module Xlsxrb
42
42
  def each(&)
43
43
  sheets.each(&)
44
44
  end
45
+ alias_method :each_sheet, :each
45
46
 
46
47
  # Returns whether the workbook is valid according to ECMA-376 rules.
47
48
  #
@@ -71,6 +72,22 @@ module Xlsxrb
71
72
  end
72
73
  alias_method :[], :sheet
73
74
 
75
+ # Loads all sheets into memory, returning an Elements::Workbook where every
76
+ # worksheet is a fully-parsed Elements::Worksheet supporting coordinate random access.
77
+ #
78
+ # @example
79
+ # wb = Xlsxrb.read("file.xlsx").load
80
+ # puts wb["Sheet1"]["A1"].value
81
+ #
82
+ # @return [Elements::Workbook]
83
+ # @api public
84
+ #: () -> Elements::Workbook
85
+ def load
86
+ loaded_sheets = sheets.map { |s| s.respond_to?(:load) ? s.load : s }
87
+ with(sheets: loaded_sheets)
88
+ end
89
+ alias_method :to_workbook, :load
90
+
74
91
  # Returns a new Workbook with the specified sheet updated.
75
92
  # Yields the matched worksheet to the block, which must return a new Worksheet.
76
93
  #
@@ -92,10 +109,11 @@ module Xlsxrb
92
109
  sheet_to_update = sheet(identifier)
93
110
  raise ArgumentError, "sheet not found: #{identifier}" unless sheet_to_update
94
111
 
112
+ sheet_to_update = sheet_to_update.load if sheet_to_update.respond_to?(:load)
95
113
  new_sheet = yield sheet_to_update
96
114
  raise TypeError, "block must return a Worksheet" unless new_sheet.is_a?(Worksheet)
97
115
 
98
- new_sheets = sheets.map { |s| s == sheet_to_update ? new_sheet : s }
116
+ new_sheets = sheets.map { |s| s.name == sheet_to_update.name ? new_sheet : s }
99
117
  with(sheets: new_sheets)
100
118
  end
101
119
 
@@ -4,16 +4,20 @@
4
4
 
5
5
  module Xlsxrb
6
6
  module Elements
7
- # Represents a single worksheet in a workbook.
7
+ # Represents a single fully parsed, in-memory worksheet in a workbook.
8
+ # Provides coordinate random access (sheet["A1"]), row lookups (row_at),
9
+ # and immutable cell updates (update_cell).
8
10
  #
9
11
  # @example Access cells and rows
10
- # sheet = workbook.sheet(0)
12
+ # sheet = workbook.sheet(0).load
11
13
  # cell = sheet["A1"]
12
- # sheet.each_row { |row| puts row.to_a.inspect }
14
+ # row = sheet.row_at(0)
13
15
  #
14
16
  # @api public
15
- Worksheet = Data.define(:name, :rows, :columns, :charts, :unmapped_data, :errors) do
16
- include Enumerable
17
+ class Worksheet
18
+ [Enumerable, CoordinateAccess].each { |m| include m }
19
+
20
+ attr_reader :name, :rows, :columns, :charts, :unmapped_data, :errors
17
21
 
18
22
  # @param name [String] The worksheet name (max 31 characters).
19
23
  # @param rows [Array<Elements::Row>] Rows in the sheet.
@@ -23,85 +27,32 @@ module Xlsxrb
23
27
  # @param errors [Array<String>, nil] Validation errors.
24
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
25
29
  def initialize(name:, rows: [], columns: [], charts: [], unmapped_data: {}, errors: nil)
26
- computed_errors = errors || self.class.validate(name, rows)
27
- super(name: name, rows: rows.freeze, columns: columns.freeze, charts: charts.freeze,
28
- unmapped_data: unmapped_data, errors: computed_errors.freeze)
29
- end
30
-
31
- # Returns a Hash mapping Excel cell references (e.g. "A1") to Cell objects.
32
- #
33
- # @return [Hash<String, Elements::Cell>]
34
- #: () -> Hash[String, Elements::Cell]
35
- def cells_hash
36
- h = {}
37
- rows.each do |r|
38
- r.cells.each do |c|
39
- ref = "#{Cell.column_letter(c.column_index)}#{c.row_index + 1}"
40
- h[ref] = c
41
- end
42
- end
43
- h
44
- end
45
-
46
- # Returns all cells ordered by row and column index.
47
- #
48
- # @return [Array<Elements::Cell>]
49
- # @api public
50
- #: () -> Array[Elements::Cell]
51
- def cells
52
- # Ensure ordered traversal
53
- cells_hash.values.sort_by { |c| [c.row_index, c.column_index] }
54
- end
55
-
56
- # Access a cell by its Excel-style reference (e.g. "A1").
57
- #
58
- # @example
59
- # sheet["A1"] #=> #<Elements::Cell value="Hello">
60
- #
61
- # @param ref [String, Symbol] Cell reference (e.g. "A1" or :A1).
62
- # @return [Elements::Cell, nil]
63
- # @api public
64
- #: (String | Symbol ref) -> Elements::Cell?
65
- def [](ref)
66
- cells_hash[ref.to_s.upcase]
30
+ @name = name
31
+ @rows = (rows || []).freeze
32
+ @columns = (columns || []).freeze
33
+ @charts = (charts || []).freeze
34
+ @unmapped_data = (unmapped_data || {}).freeze
35
+ computed_errors = errors || self.class.validate(@name, @rows)
36
+ @errors = computed_errors.freeze
67
37
  end
68
38
 
69
- # Iterate over cells in the worksheet.
39
+ # Iterate over rows in the worksheet.
70
40
  #
71
41
  # @example
72
- # sheet.each do |cell|
73
- # puts cell.value
42
+ # sheet.each do |row|
43
+ # puts row.to_a.inspect
74
44
  # end
75
45
  #
76
- # @yield [cell]
77
- # @yieldparam cell [Elements::Cell]
46
+ # @yield [row]
47
+ # @yieldparam row [Elements::Row]
78
48
  # @return [Enumerator, void]
79
49
  # @api public
80
- #: () { (Elements::Cell) -> void } -> void
81
- #: | () -> Enumerator[Elements::Cell, void]
50
+ #: () { (Elements::Row) -> void } -> void
51
+ #: | () -> Enumerator[Elements::Row, void]
82
52
  def each(&)
83
53
  return to_enum(:each) unless block_given?
84
54
 
85
- cells.each(&)
86
- end
87
-
88
- # Iterate over cells in the worksheet.
89
- #
90
- # @example
91
- # sheet.each_cell do |cell|
92
- # puts "#{cell.ref}: #{cell.value}"
93
- # end
94
- #
95
- # @yield [cell]
96
- # @yieldparam cell [Elements::Cell]
97
- # @return [Enumerator, void]
98
- # @api public
99
- #: () { (Elements::Cell) -> void } -> void
100
- #: | () -> Enumerator[Elements::Cell, void]
101
- def each_cell(&)
102
- return to_enum(:each_cell) unless block_given?
103
-
104
- cells.each(&)
55
+ rows.each(&)
105
56
  end
106
57
 
107
58
  # Iterate over rows in the worksheet.
@@ -123,61 +74,26 @@ module Xlsxrb
123
74
  rows.each(&)
124
75
  end
125
76
 
126
- # Returns whether the worksheet is valid according to OOXML specifications.
77
+ # Iterate over all cells across rows.
127
78
  #
128
- # @return [Boolean]
129
- #: () -> bool
130
- def valid?
131
- errors.empty?
132
- end
133
-
134
- # Returns the row at the given 0-based index, or nil.
135
- #
136
- # @param index [Integer] 0-based row index.
137
- # @return [Elements::Row, nil]
138
- # @api public
139
- #: (Integer index) -> Elements::Row?
140
- def row_at(index)
141
- rows.find { |r| r.index == index }
142
- end
143
-
144
- # Returns the first row in the sheet, or nil.
145
- #
146
- # @return [Elements::Row, nil]
79
+ # @yield [cell]
80
+ # @yieldparam cell [Elements::Cell]
81
+ # @return [Enumerator, void]
147
82
  # @api public
148
- #: () -> Elements::Row?
149
- def first_row
150
- rows.min_by(&:index)
151
- end
83
+ #: () { (Elements::Cell) -> void } -> void
84
+ #: | () -> Enumerator[Elements::Cell, void]
85
+ def each_cell(&)
86
+ return to_enum(:each_cell) unless block_given?
152
87
 
153
- # Returns the last row in the sheet, or nil.
154
- #
155
- # @return [Elements::Row, nil]
156
- # @api public
157
- #: () -> Elements::Row?
158
- def last_row
159
- rows.max_by(&:index)
88
+ cells.each(&)
160
89
  end
161
90
 
162
- # Returns the raw cell value at the given Excel-style reference (e.g. "A1").
163
- #
164
- # @example
165
- # sheet.cell_value("A1") #=> "Sales Report"
91
+ # Returns whether the worksheet is valid according to OOXML specifications.
166
92
  #
167
- # @param ref [String] Cell reference (e.g. "A1").
168
- # @return [Object, nil]
169
- # @api public
170
- #: (String ref) -> untyped
171
- def cell_value(ref)
172
- parsed = Cell.parse_ref(ref)
173
- return nil unless parsed
174
-
175
- row_idx, col_idx = parsed
176
- row = row_at(row_idx)
177
- return nil unless row
178
-
179
- cell = row.cell_at(col_idx)
180
- cell&.value
93
+ # @return [Boolean]
94
+ #: () -> bool
95
+ def valid?
96
+ errors.empty?
181
97
  end
182
98
 
183
99
  # Returns a new Worksheet with the specified cell updated.
@@ -227,6 +143,60 @@ module Xlsxrb
227
143
  with(rows: new_rows)
228
144
  end
229
145
 
146
+ # Returns a new Worksheet with attributes replaced (Data-like behavior).
147
+ #
148
+ # @param changes [Hash]
149
+ # @return [Worksheet]
150
+ # @api public
151
+ #: (**untyped) -> Elements::Worksheet
152
+ def with(**changes)
153
+ new_name = changes.key?(:name) ? changes[:name] : name
154
+ new_rows = changes.key?(:rows) ? changes[:rows] : rows
155
+ new_cols = changes.key?(:columns) ? changes[:columns] : columns
156
+ new_charts = changes.key?(:charts) ? changes[:charts] : charts
157
+ new_unmapped = changes.key?(:unmapped_data) ? changes[:unmapped_data] : unmapped_data
158
+ new_errors = changes.key?(:errors) ? changes[:errors] : errors
159
+
160
+ self.class.new(
161
+ name: new_name,
162
+ rows: new_rows,
163
+ columns: new_cols,
164
+ charts: new_charts,
165
+ unmapped_data: new_unmapped,
166
+ errors: new_errors
167
+ )
168
+ end
169
+
170
+ # Support pattern matching.
171
+ #: (Array[Symbol]?) -> Hash[Symbol, untyped]
172
+ def deconstruct_keys(_keys)
173
+ { name: name, rows: rows, columns: columns, charts: charts, unmapped_data: unmapped_data, errors: errors }
174
+ end
175
+
176
+ # Compare worksheets for equality.
177
+ #: (untyped other) -> bool
178
+ def ==(other)
179
+ return false unless other.is_a?(Worksheet)
180
+
181
+ name == other.name && rows == other.rows && columns == other.columns && charts == other.charts
182
+ end
183
+ alias eql? ==
184
+
185
+ #: () -> Integer
186
+ def hash
187
+ [self.class, name, rows, columns, charts].hash
188
+ end
189
+
190
+ # Returns self when load is called on an already in-memory Worksheet.
191
+ #
192
+ # @return [Elements::Worksheet]
193
+ # @api public
194
+ #: () -> Elements::Worksheet
195
+ def load
196
+ self
197
+ end
198
+ alias to_worksheet load
199
+
230
200
  # Validates worksheet name and rows against OOXML limits.
231
201
  #
232
202
  # @param name [String]
@@ -6,6 +6,7 @@ require_relative "elements/types"
6
6
  require_relative "elements/cell"
7
7
  require_relative "elements/row"
8
8
  require_relative "elements/column"
9
+ require_relative "elements/coordinate_access"
9
10
  require_relative "elements/worksheet"
10
11
  require_relative "elements/workbook"
11
12
 
@@ -515,17 +515,17 @@ module Xlsxrb
515
515
  row_end = xml.index("</row>", tag_end + 1)
516
516
  break unless row_end
517
517
 
518
- row_source = { part: part_name, row: row_index }
519
- cells = fast_parse_cells_direct(xml, tag_end + 1, row_end, shared_strings, row_source)
520
-
521
- row_obj = Elements::Row.new(
518
+ { part: part_name, row: row_index }
519
+ row_obj = StreamRow.new(
522
520
  index: row_index,
523
- cells: cells,
521
+ xml_bytes: xml,
522
+ from: tag_end + 1,
523
+ to: row_end,
524
+ shared_strings: shared_strings,
524
525
  height: attrs[:height],
525
526
  hidden: attrs[:hidden] || false,
526
527
  custom_height: attrs[:custom_height] || false,
527
- outline_level: attrs[:outline_level],
528
- errors: Elements::EMPTY_ERRORS
528
+ outline_level: attrs[:outline_level]
529
529
  )
530
530
  block.call(row_obj)
531
531
 
@@ -537,7 +537,13 @@ module Xlsxrb
537
537
 
538
538
  def self.fast_parse_cells_direct(xml, from, to, shared_strings, row_source)
539
539
  cells = []
540
+ fast_scan_cells_direct(xml, from, to, shared_strings, row_source) { |c| cells << c }
541
+ cells
542
+ end
543
+
544
+ def self.fast_scan_cells_direct(xml, from, to, shared_strings, row_source, &block)
540
545
  pos = from
546
+ cell_count = 0
541
547
 
542
548
  while pos < to
543
549
  c_start = xml.index("<c", pos)
@@ -589,7 +595,6 @@ module Xlsxrb
589
595
 
590
596
  row_idx = (row_idx * 10) + (cb - 48)
591
597
  ai += 1
592
-
593
598
  end
594
599
  row_idx -= 1
595
600
  ai += 1 if xml.getbyte(ai) == 34
@@ -619,7 +624,6 @@ module Xlsxrb
619
624
 
620
625
  style_index = (style_index * 10) + (cb - 48)
621
626
  ai += 1
622
-
623
627
  end
624
628
  ai += 1 if xml.getbyte(ai) == 34
625
629
  else
@@ -629,13 +633,15 @@ module Xlsxrb
629
633
 
630
634
  # Self-closing <c ... />
631
635
  if xml.getbyte(c_tag_end - 1) == 47
632
- cells << Elements::Cell.new(
636
+ cell_obj = Elements::Cell.new(
633
637
  row_index: row_idx || row_source[:row],
634
- column_index: col_idx || cells.size,
638
+ column_index: col_idx || cell_count,
635
639
  value: nil,
636
640
  style_index: style_index,
637
641
  errors: Elements::EMPTY_ERRORS
638
642
  )
643
+ cell_count += 1
644
+ block.call(cell_obj)
639
645
  pos = c_tag_end + 1
640
646
  next
641
647
  end
@@ -721,19 +727,19 @@ module Xlsxrb
721
727
  end
722
728
 
723
729
  val_to_use = inline_str || value
724
- cells << Elements::Cell.new(
730
+ cell_obj = Elements::Cell.new(
725
731
  row_index: row_idx || row_source[:row],
726
- column_index: col_idx || cells.size,
732
+ column_index: col_idx || cell_count,
727
733
  value: val_to_use,
728
734
  formula: formula,
729
735
  style_index: style_index,
730
736
  errors: Elements::EMPTY_ERRORS
731
737
  )
738
+ cell_count += 1
739
+ block.call(cell_obj)
732
740
 
733
741
  pos = c_end + 4
734
742
  end
735
-
736
- cells
737
743
  end
738
744
 
739
745
  private_class_method :fast_parse_cells_direct