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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +87 -35
- data/Rakefile +16 -5
- data/benchmark.rb +32 -4
- data/docs/ARCHITECTURE.md +32 -30
- data/docs/visual/VisualGallery.md +157 -157
- data/docs/wasm/ruby.wasm +0 -0
- data/lib/ruby_lsp/xlsxrb/addon.rb +1 -1
- data/lib/ruby_lsp/xlsxrb/completion_listener.rb +11 -13
- data/lib/xlsxrb/elements/coordinate_access.rb +101 -0
- data/lib/xlsxrb/elements/workbook.rb +19 -1
- data/lib/xlsxrb/elements/worksheet.rb +92 -122
- data/lib/xlsxrb/elements.rb +1 -0
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +21 -15
- data/lib/xlsxrb/stream_row.rb +181 -0
- data/lib/xlsxrb/version.rb +1 -1
- data/lib/xlsxrb.rb +164 -106
- data/sig/generated/xlsxrb/elements/coordinate_access.rbs +70 -0
- data/sig/generated/xlsxrb/elements/worksheet.rbs +116 -14
- data/sig/generated/xlsxrb/ooxml/worksheet_parser.rbs +2 -0
- data/sig/generated/xlsxrb/stream_row.rbs +125 -0
- data/sig/generated/xlsxrb.rbs +110 -68
- metadata +5 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
module Xlsxrb
|
|
6
|
+
# Streaming row implementation that parses cells on-demand / lazily.
|
|
7
|
+
# Provides O(1) memory consumption even for rows with tens of thousands of columns.
|
|
8
|
+
#
|
|
9
|
+
# @example Streaming cells one-by-one (O(1) memory)
|
|
10
|
+
# row.each_cell do |cell|
|
|
11
|
+
# puts "#{cell.ref}: #{cell.value}"
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# @example Random access or array conversion (cached on-demand)
|
|
15
|
+
# cell = row[0]
|
|
16
|
+
# values = row.to_a
|
|
17
|
+
#
|
|
18
|
+
# @api public
|
|
19
|
+
class StreamRow
|
|
20
|
+
[Enumerable].each { |m| include m }
|
|
21
|
+
|
|
22
|
+
attr_reader :index, :height, :hidden, :custom_height, :outline_level
|
|
23
|
+
|
|
24
|
+
# @param index [Integer] 0-based row index.
|
|
25
|
+
# @param xml_bytes [String] Raw ASCII-8BIT XML bytes.
|
|
26
|
+
# @param from [Integer] Byte offset where cells start.
|
|
27
|
+
# @param to [Integer] Byte offset where cells end.
|
|
28
|
+
# @param shared_strings [Array<String>] Shared strings table.
|
|
29
|
+
# @param height [Float, Integer, nil] Row height in points.
|
|
30
|
+
# @param hidden [Boolean] Whether the row is hidden.
|
|
31
|
+
# @param custom_height [Boolean] Whether custom height is set.
|
|
32
|
+
# @param outline_level [Integer, nil] Grouping/outline level.
|
|
33
|
+
#: (index: Integer, xml_bytes: String, from: Integer, to: Integer, shared_strings: Array[String], ?height: Float | Integer | nil, ?hidden: bool, ?custom_height: bool, ?outline_level: Integer | nil) -> void
|
|
34
|
+
def initialize(index:, xml_bytes:, from:, to:, shared_strings:, height: nil, hidden: false,
|
|
35
|
+
custom_height: false, outline_level: nil)
|
|
36
|
+
@index = index
|
|
37
|
+
@xml = xml_bytes
|
|
38
|
+
@from = from
|
|
39
|
+
@to = to
|
|
40
|
+
@shared_strings = shared_strings
|
|
41
|
+
@height = height
|
|
42
|
+
@hidden = hidden
|
|
43
|
+
@custom_height = custom_height
|
|
44
|
+
@outline_level = outline_level
|
|
45
|
+
@cells_cache = nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Iterate over cells in this streaming row one by one.
|
|
49
|
+
#
|
|
50
|
+
# @yield [cell]
|
|
51
|
+
# @yieldparam cell [Elements::Cell]
|
|
52
|
+
# @return [Enumerator, void]
|
|
53
|
+
# @api public
|
|
54
|
+
#: () { (Elements::Cell) -> void } -> void
|
|
55
|
+
#: | () -> Enumerator[Elements::Cell, void]
|
|
56
|
+
def each_cell(&)
|
|
57
|
+
return enum_for(:each_cell) unless block_given?
|
|
58
|
+
|
|
59
|
+
if @cells
|
|
60
|
+
@cells.each(&)
|
|
61
|
+
else
|
|
62
|
+
Ooxml::WorksheetParser.fast_scan_cells_direct(@xml, @from, @to, @shared_strings, { row: @index }, &)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Iterate over cells in this streaming row.
|
|
67
|
+
#
|
|
68
|
+
# @yield [cell]
|
|
69
|
+
# @yieldparam cell [Elements::Cell]
|
|
70
|
+
# @return [Enumerator, void]
|
|
71
|
+
# @api public
|
|
72
|
+
#: () { (Elements::Cell) -> void } -> void
|
|
73
|
+
#: | () -> Enumerator[Elements::Cell, void]
|
|
74
|
+
def each(&)
|
|
75
|
+
each_cell(&)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns all cells as an Array. Cached on first access.
|
|
79
|
+
#
|
|
80
|
+
# @return [Array<Elements::Cell>]
|
|
81
|
+
# @api public
|
|
82
|
+
#: () -> Array[Elements::Cell]
|
|
83
|
+
def cells
|
|
84
|
+
@cells ||= each_cell.to_a.freeze
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Access a cell by 0-based column index, or access row attributes via Symbol.
|
|
88
|
+
#
|
|
89
|
+
# @param col_index [Integer, Symbol] Column index or attribute symbol.
|
|
90
|
+
# @return [Elements::Cell, Object, nil]
|
|
91
|
+
# @api public
|
|
92
|
+
#: (Integer | Symbol col_index) -> untyped
|
|
93
|
+
def [](col_index)
|
|
94
|
+
case col_index
|
|
95
|
+
when Symbol
|
|
96
|
+
case col_index
|
|
97
|
+
when :cells then cells
|
|
98
|
+
when :index then index
|
|
99
|
+
when :height then height
|
|
100
|
+
when :hidden then hidden
|
|
101
|
+
when :custom_height then custom_height
|
|
102
|
+
when :outline_level then outline_level
|
|
103
|
+
when :attrs then { height: height, hidden: hidden, custom_height: custom_height, outline_level: outline_level }
|
|
104
|
+
end
|
|
105
|
+
else
|
|
106
|
+
cells[col_index]
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Access a cell by 0-based column index.
|
|
111
|
+
#
|
|
112
|
+
# @param col_index [Integer] 0-based column index.
|
|
113
|
+
# @return [Elements::Cell, nil]
|
|
114
|
+
# @api public
|
|
115
|
+
#: (Integer col_index) -> Elements::Cell?
|
|
116
|
+
def cell_at(col_index)
|
|
117
|
+
cells.find { |c| c.column_index == col_index }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Convert row cells to an Array of raw values (sparse columns get nil).
|
|
121
|
+
#
|
|
122
|
+
# @return [Array<Object>]
|
|
123
|
+
# @api public
|
|
124
|
+
#: () -> Array[untyped]
|
|
125
|
+
def to_a
|
|
126
|
+
return [] if cells.empty?
|
|
127
|
+
|
|
128
|
+
max_col = cells.map(&:column_index).max || 0
|
|
129
|
+
arr = Array.new(max_col + 1)
|
|
130
|
+
cells.each do |cell|
|
|
131
|
+
arr[cell.column_index] = cell.value
|
|
132
|
+
end
|
|
133
|
+
arr
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Returns cell values as an Array.
|
|
137
|
+
#
|
|
138
|
+
# @return [Array<Object>]
|
|
139
|
+
# @api public
|
|
140
|
+
#: () -> Array[untyped]
|
|
141
|
+
def values
|
|
142
|
+
to_a
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Returns whether the row is valid according to OOXML specifications.
|
|
146
|
+
#
|
|
147
|
+
# @return [Boolean]
|
|
148
|
+
# @api public
|
|
149
|
+
#: () -> bool
|
|
150
|
+
def valid?
|
|
151
|
+
true
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Unmapped metadata for compatibility with Elements::Row.
|
|
155
|
+
#
|
|
156
|
+
# @return [Hash]
|
|
157
|
+
# @api public
|
|
158
|
+
#: () -> Hash[untyped, untyped]
|
|
159
|
+
def unmapped_data
|
|
160
|
+
Elements::EMPTY_HASH
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Validation errors for compatibility with Elements::Row.
|
|
164
|
+
#
|
|
165
|
+
# @return [Array<String>]
|
|
166
|
+
# @api public
|
|
167
|
+
#: () -> Array[String]
|
|
168
|
+
def errors
|
|
169
|
+
Elements::EMPTY_ERRORS
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Human-readable representation.
|
|
173
|
+
#
|
|
174
|
+
# @return [String]
|
|
175
|
+
# @api public
|
|
176
|
+
#: () -> String
|
|
177
|
+
def inspect
|
|
178
|
+
"#<#{self.class.name} index=#{index} height=#{height.inspect} hidden=#{hidden}>"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
data/lib/xlsxrb/version.rb
CHANGED
data/lib/xlsxrb.rb
CHANGED
|
@@ -24,6 +24,7 @@ require_relative "xlsxrb/ooxml/writer"
|
|
|
24
24
|
require_relative "xlsxrb/ooxml/reader"
|
|
25
25
|
require_relative "xlsxrb/ooxml"
|
|
26
26
|
require_relative "xlsxrb/elements"
|
|
27
|
+
require_relative "xlsxrb/stream_row"
|
|
27
28
|
require_relative "xlsxrb/style_builder"
|
|
28
29
|
|
|
29
30
|
# Ruby XLSX read/write library.
|
|
@@ -360,28 +361,46 @@ module Xlsxrb
|
|
|
360
361
|
)
|
|
361
362
|
end
|
|
362
363
|
|
|
363
|
-
# Reads an XLSX file
|
|
364
|
+
# Reads an XLSX file (streaming / lazy-loaded by default) from a file path, IO stream, or binary String.
|
|
364
365
|
#
|
|
365
|
-
#
|
|
366
|
-
#
|
|
367
|
-
# sheet = workbook["Sheet1"]
|
|
368
|
-
# puts sheet["A1"].value
|
|
366
|
+
# Sheets and rows are streamed lazily with O(1) constant memory. If a block is given,
|
|
367
|
+
# yields each StreamSheet sequentially.
|
|
369
368
|
#
|
|
370
|
-
#
|
|
371
|
-
#
|
|
369
|
+
# Call #load on the returned Workbook or Sheet to convert to an in-memory representation
|
|
370
|
+
# for coordinate random access (e.g. sheet["A1"]).
|
|
372
371
|
#
|
|
373
|
-
# @
|
|
374
|
-
#
|
|
372
|
+
# @example Streaming read across sheets and rows (O(1) memory)
|
|
373
|
+
# Xlsxrb.read("large.xlsx") do |sheet|
|
|
374
|
+
# puts "Sheet: #{sheet.name}"
|
|
375
|
+
# sheet.each_row do |row|
|
|
376
|
+
# row.each_cell { |cell| puts "#{cell.ref}: #{cell.value}" }
|
|
377
|
+
# end
|
|
378
|
+
# end
|
|
379
|
+
#
|
|
380
|
+
# @example Lazy workbook access and explicit in-memory loading
|
|
381
|
+
# wb = Xlsxrb.read("data.xlsx")
|
|
382
|
+
# sheet = wb.sheets.first
|
|
383
|
+
# sheet.each_row { |row| ... } # streams with O(1) memory
|
|
384
|
+
# doc_sheet = sheet.load # explicitly load into memory
|
|
385
|
+
# puts doc_sheet["A1"].value # coordinate random access
|
|
386
|
+
#
|
|
387
|
+
# @param source [String, IO] File path, binary content string (starting with PK..), or IO object.
|
|
388
|
+
# @yield [sheet] Yields each streaming sheet.
|
|
389
|
+
# @yieldparam sheet [StreamSheet] The streaming worksheet object.
|
|
390
|
+
# @return [Elements::Workbook, void] Returns Elements::Workbook when no block is given.
|
|
375
391
|
# @api public
|
|
376
|
-
#: (
|
|
377
|
-
|
|
392
|
+
#: (String | IO source) { (StreamSheet) -> void } -> void
|
|
393
|
+
#: (String | IO source) -> Elements::Workbook
|
|
394
|
+
def self.read(source, &)
|
|
395
|
+
source = StringIO.new(source) if source.is_a?(String) && (source.start_with?("PK\x03\x04") || source.include?("\x00"))
|
|
396
|
+
|
|
378
397
|
attributes = source.is_a?(String) ? { "filepath" => source } : {}
|
|
379
398
|
Xlsxrb.in_span("Xlsxrb.read", attributes: attributes) do
|
|
380
399
|
entries = Ooxml::ZipReader.open(source, &:read_all)
|
|
381
400
|
shared_strings = Ooxml::SharedStringsParser.parse(entries["xl/sharedStrings.xml"])
|
|
382
|
-
styles = Ooxml::StylesParser.parse(entries["xl/styles.xml"])
|
|
383
401
|
workbook_sheets = Ooxml::WorkbookParser.parse(entries["xl/workbook.xml"])
|
|
384
402
|
rels = Ooxml::RelationshipsParser.parse(entries["xl/_rels/workbook.xml.rels"])
|
|
403
|
+
styles = Ooxml::StylesParser.parse(entries["xl/styles.xml"])
|
|
385
404
|
|
|
386
405
|
sheets = workbook_sheets.map do |sheet_info|
|
|
387
406
|
target = rels[sheet_info[:r_id]]
|
|
@@ -389,24 +408,92 @@ module Xlsxrb
|
|
|
389
408
|
|
|
390
409
|
sheet_path = target.start_with?("/") ? target.delete_prefix("/") : "xl/#{target}"
|
|
391
410
|
sheet_xml = entries[sheet_path]
|
|
392
|
-
|
|
411
|
+
next nil if sheet_xml.nil? || sheet_xml.empty?
|
|
412
|
+
|
|
413
|
+
StreamSheet.new(
|
|
414
|
+
sheet_info[:name],
|
|
415
|
+
sheet_xml,
|
|
416
|
+
shared_strings,
|
|
417
|
+
styles
|
|
418
|
+
)
|
|
393
419
|
end.compact
|
|
394
420
|
|
|
395
|
-
Elements::Workbook.new(sheets: sheets, shared_strings: shared_strings, styles: styles)
|
|
421
|
+
wb = Elements::Workbook.new(sheets: sheets, shared_strings: shared_strings, styles: styles)
|
|
422
|
+
|
|
423
|
+
if block_given?
|
|
424
|
+
sheets.each(&)
|
|
425
|
+
nil
|
|
426
|
+
else
|
|
427
|
+
wb
|
|
428
|
+
end
|
|
396
429
|
end
|
|
397
430
|
end
|
|
398
431
|
|
|
399
|
-
# Writes an
|
|
432
|
+
# Writes an XLSX file or IO stream (streaming or in-memory), or returns a binary string.
|
|
400
433
|
#
|
|
401
|
-
# @
|
|
434
|
+
# @overload write(target, strict_excel_mode: true, &block)
|
|
435
|
+
# Streaming write: yields a StreamWriter context for high-speed, zero-allocation XLSX generation.
|
|
436
|
+
# @param target [String, IO] Destination file path or writable IO object.
|
|
437
|
+
# @param strict_excel_mode [Boolean] Whether to enforce Excel specifications.
|
|
438
|
+
# @yield [stream_writer]
|
|
439
|
+
# @yieldparam stream_writer [Xlsxrb::StreamWriter]
|
|
440
|
+
# @return [void]
|
|
441
|
+
#
|
|
442
|
+
# @overload write(workbook)
|
|
443
|
+
# In-memory write: exports the workbook to an in-memory binary String.
|
|
444
|
+
# @param workbook [Elements::Workbook] The workbook to write.
|
|
445
|
+
# @return [String] Binary data representing the XLSX file.
|
|
446
|
+
#
|
|
447
|
+
# @overload write(target, workbook)
|
|
448
|
+
# In-memory write: writes the workbook to a file path or IO stream.
|
|
449
|
+
# @param target [String, IO] Destination file path or writable IO object.
|
|
450
|
+
# @param workbook [Elements::Workbook] The workbook to write.
|
|
451
|
+
# @return [void]
|
|
452
|
+
#
|
|
453
|
+
# @example Streaming write to file
|
|
454
|
+
# Xlsxrb.write("output.xlsx") do |writer|
|
|
455
|
+
# writer.sheet("Sheet1") { |s| s.row(["Hello", "World"]) }
|
|
456
|
+
# end
|
|
457
|
+
#
|
|
458
|
+
# @example In-memory export to binary string
|
|
459
|
+
# binary_data = Xlsxrb.write(workbook)
|
|
460
|
+
#
|
|
461
|
+
# @example In-memory write to file
|
|
402
462
|
# Xlsxrb.write("output.xlsx", workbook)
|
|
403
463
|
#
|
|
404
|
-
# @param target [String, IO] File path or IO object.
|
|
405
|
-
# @param workbook [Elements::Workbook] The workbook to write.
|
|
406
|
-
# @return [void]
|
|
407
464
|
# @api public
|
|
408
|
-
#: (
|
|
409
|
-
|
|
465
|
+
#: (Elements::Workbook workbook) -> String
|
|
466
|
+
#: (String | IO target, Elements::Workbook workbook) -> void
|
|
467
|
+
#: (String | IO target, ?strict_excel_mode: bool) ?{ (StreamWriter) -> void } -> void
|
|
468
|
+
def self.write(target_or_workbook, workbook_or_nil = nil, strict_excel_mode: true, &block)
|
|
469
|
+
if block_given?
|
|
470
|
+
target = target_or_workbook
|
|
471
|
+
raise Error, "target is required" if target.nil?
|
|
472
|
+
|
|
473
|
+
attributes = target.is_a?(String) ? { "filepath" => target } : {}
|
|
474
|
+
return Xlsxrb.in_span("Xlsxrb.write", attributes: attributes) do
|
|
475
|
+
stream_writer = StreamWriter.new(target, strict_excel_mode: strict_excel_mode)
|
|
476
|
+
begin
|
|
477
|
+
yield stream_writer
|
|
478
|
+
stream_writer.close
|
|
479
|
+
ensure
|
|
480
|
+
stream_writer.cleanup!
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
if workbook_or_nil.nil?
|
|
486
|
+
wb = target_or_workbook
|
|
487
|
+
raise Error, "workbook must be an Elements::Workbook" unless wb.is_a?(Elements::Workbook)
|
|
488
|
+
|
|
489
|
+
io = StringIO.new
|
|
490
|
+
io.binmode
|
|
491
|
+
write(io, wb)
|
|
492
|
+
return io.string.b
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
target = target_or_workbook
|
|
496
|
+
workbook = workbook_or_nil
|
|
410
497
|
raise Error, "target is required" if target.nil?
|
|
411
498
|
raise Error, "workbook must be an Elements::Workbook" unless workbook.is_a?(Elements::Workbook)
|
|
412
499
|
|
|
@@ -416,7 +503,8 @@ module Xlsxrb
|
|
|
416
503
|
sst_index = {}
|
|
417
504
|
|
|
418
505
|
# Collect shared strings and build index without allocating new Hashes
|
|
419
|
-
sheet_data = workbook.sheets.map do |
|
|
506
|
+
sheet_data = workbook.sheets.map do |raw_ws|
|
|
507
|
+
ws = raw_ws.respond_to?(:load) ? raw_ws.load : raw_ws
|
|
420
508
|
ws.rows.each do |row|
|
|
421
509
|
row.cells.each do |cell|
|
|
422
510
|
val = cell.value
|
|
@@ -485,7 +573,7 @@ module Xlsxrb
|
|
|
485
573
|
raise Error, "source is required" if source.nil?
|
|
486
574
|
raise Error, "block is required" unless block_given?
|
|
487
575
|
|
|
488
|
-
workbook = read(source)
|
|
576
|
+
workbook = read(source).load
|
|
489
577
|
result_workbook = yield workbook
|
|
490
578
|
result_workbook = workbook unless result_workbook.is_a?(Elements::Workbook)
|
|
491
579
|
|
|
@@ -494,15 +582,26 @@ module Xlsxrb
|
|
|
494
582
|
end
|
|
495
583
|
|
|
496
584
|
# Represents a sheet being streamed sequentially from an XLSX file.
|
|
585
|
+
# Provides O(1) constant-memory streaming over rows and cells.
|
|
586
|
+
#
|
|
587
|
+
# Call #load (or #to_worksheet) to convert this streaming sheet into an
|
|
588
|
+
# in-memory Elements::Worksheet supporting coordinate random access (sheet["A1"]).
|
|
497
589
|
#
|
|
498
|
-
# @example Iterate rows in streaming mode
|
|
499
|
-
# Xlsxrb.
|
|
590
|
+
# @example Iterate rows and cells in streaming mode (O(1) memory)
|
|
591
|
+
# Xlsxrb.read("large_data.xlsx") do |sheet|
|
|
500
592
|
# puts "Processing sheet: #{sheet.name}"
|
|
501
593
|
# sheet.each_row do |row|
|
|
502
|
-
#
|
|
594
|
+
# row.each_cell do |cell|
|
|
595
|
+
# puts "#{cell.ref}: #{cell.value}"
|
|
596
|
+
# end
|
|
503
597
|
# end
|
|
504
598
|
# end
|
|
505
599
|
#
|
|
600
|
+
# @example Load into an in-memory Worksheet for coordinate random access
|
|
601
|
+
# wb = Xlsxrb.read("data.xlsx")
|
|
602
|
+
# doc_sheet = wb.sheet(0).load
|
|
603
|
+
# puts doc_sheet["A1"].value
|
|
604
|
+
#
|
|
506
605
|
# @api public
|
|
507
606
|
class StreamSheet
|
|
508
607
|
[Enumerable].each { |m| include m }
|
|
@@ -512,26 +611,28 @@ module Xlsxrb
|
|
|
512
611
|
# @param name [String] The sheet name.
|
|
513
612
|
# @param sheet_xml [String] Raw XML content of the sheet.
|
|
514
613
|
# @param shared_strings [Array<String>] Shared strings table.
|
|
515
|
-
|
|
516
|
-
|
|
614
|
+
# @param styles [Hash, nil] Styles table.
|
|
615
|
+
#: (String name, String sheet_xml, Array[String] shared_strings, ?Hash[untyped, untyped]? styles) -> void
|
|
616
|
+
def initialize(name, sheet_xml, shared_strings, styles = nil)
|
|
517
617
|
@name = name
|
|
518
618
|
@sheet_xml = sheet_xml
|
|
519
619
|
@shared_strings = shared_strings
|
|
620
|
+
@styles = styles
|
|
520
621
|
end
|
|
521
622
|
|
|
522
|
-
# Iterate over rows in this streaming sheet.
|
|
623
|
+
# Iterate over rows in this streaming sheet (O(1) memory).
|
|
523
624
|
#
|
|
524
625
|
# @yield [row]
|
|
525
|
-
# @yieldparam row [Elements::Row]
|
|
626
|
+
# @yieldparam row [StreamRow, Elements::Row]
|
|
526
627
|
# @return [Enumerator, void]
|
|
527
628
|
# @api public
|
|
528
|
-
#: () { (Elements::Row) -> void } -> void
|
|
529
|
-
#: | () -> Enumerator[Elements::Row, void]
|
|
629
|
+
#: () { (StreamRow | Elements::Row) -> void } -> void
|
|
630
|
+
#: | () -> Enumerator[StreamRow | Elements::Row, void]
|
|
530
631
|
def each_row
|
|
531
632
|
return enum_for(:each_row) unless block_given?
|
|
532
633
|
|
|
533
634
|
Ooxml::WorksheetParser.each_row(@sheet_xml, shared_strings: @shared_strings) do |row|
|
|
534
|
-
if row.is_a?(Elements::Row)
|
|
635
|
+
if row.is_a?(Elements::Row) || row.is_a?(StreamRow)
|
|
535
636
|
yield row
|
|
536
637
|
else
|
|
537
638
|
yield Xlsxrb.send(:build_row_from_raw, row)
|
|
@@ -539,88 +640,45 @@ module Xlsxrb
|
|
|
539
640
|
end
|
|
540
641
|
end
|
|
541
642
|
|
|
542
|
-
# Iterate over rows
|
|
643
|
+
# Iterate over all cells across rows continuously (O(1) memory).
|
|
543
644
|
#
|
|
544
|
-
# @yield [
|
|
545
|
-
# @yieldparam
|
|
645
|
+
# @yield [cell]
|
|
646
|
+
# @yieldparam cell [Elements::Cell]
|
|
546
647
|
# @return [Enumerator, void]
|
|
547
648
|
# @api public
|
|
548
|
-
#: () { (Elements::
|
|
549
|
-
#: | () -> Enumerator[Elements::
|
|
550
|
-
def
|
|
551
|
-
|
|
552
|
-
end
|
|
553
|
-
end
|
|
554
|
-
|
|
555
|
-
# Streaming read: yields StreamSheet objects one at a time for each sheet in the workbook.
|
|
556
|
-
# Keeps memory usage minimal even for multi-gigabyte XLSX files.
|
|
557
|
-
#
|
|
558
|
-
# @example
|
|
559
|
-
# Xlsxrb.foreach("large.xlsx") do |sheet|
|
|
560
|
-
# puts "Sheet: #{sheet.name}"
|
|
561
|
-
# sheet.each_row { |row| process(row) }
|
|
562
|
-
# end
|
|
563
|
-
#
|
|
564
|
-
# @param source [String, IO] File path or IO object.
|
|
565
|
-
# @yield [sheet] Yields each sheet.
|
|
566
|
-
# @yieldparam sheet [StreamSheet] The streaming sheet object.
|
|
567
|
-
# @return [Enumerator, void]
|
|
568
|
-
# @api public
|
|
569
|
-
#: (untyped source) ?{ (StreamSheet) -> void } -> untyped
|
|
570
|
-
def self.foreach(source)
|
|
571
|
-
return enum_for(:foreach, source) unless block_given?
|
|
649
|
+
#: () { (Elements::Cell) -> void } -> void
|
|
650
|
+
#: | () -> Enumerator[Elements::Cell, void]
|
|
651
|
+
def each_cell(&)
|
|
652
|
+
return enum_for(:each_cell) unless block_given?
|
|
572
653
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
entries = Ooxml::ZipReader.open(source, &:read_all)
|
|
576
|
-
shared_strings = Ooxml::SharedStringsParser.parse(entries["xl/sharedStrings.xml"])
|
|
577
|
-
workbook_sheets = Ooxml::WorkbookParser.parse(entries["xl/workbook.xml"])
|
|
578
|
-
rels = Ooxml::RelationshipsParser.parse(entries["xl/_rels/workbook.xml.rels"])
|
|
579
|
-
|
|
580
|
-
workbook_sheets.each do |sheet_info|
|
|
581
|
-
target = rels[sheet_info[:r_id]]
|
|
582
|
-
next unless target
|
|
583
|
-
|
|
584
|
-
sheet_path = target.start_with?("/") ? target.delete_prefix("/") : "xl/#{target}"
|
|
585
|
-
sheet_xml = entries[sheet_path]
|
|
586
|
-
next if sheet_xml.nil? || sheet_xml.empty?
|
|
587
|
-
|
|
588
|
-
yield StreamSheet.new(sheet_info[:name], sheet_xml, shared_strings)
|
|
654
|
+
each_row do |row|
|
|
655
|
+
row.each_cell(&)
|
|
589
656
|
end
|
|
590
657
|
end
|
|
591
|
-
end
|
|
592
658
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
# @param strict_excel_mode [Boolean] Whether to enforce Excel specifications (max rows/cols/length).
|
|
605
|
-
# @yield [stream_writer]
|
|
606
|
-
# @yieldparam stream_writer [Xlsxrb::StreamWriter]
|
|
607
|
-
# @return [void]
|
|
608
|
-
# @api public
|
|
609
|
-
#: (untyped target, ?strict_excel_mode: bool) ?{ (StreamWriter) -> void } -> void
|
|
610
|
-
def self.generate(target, strict_excel_mode: true)
|
|
611
|
-
raise Error, "target is required" if target.nil?
|
|
612
|
-
raise Error, "block is required" unless block_given?
|
|
659
|
+
# Default Enumerable iteration iterates rows in the streaming sheet.
|
|
660
|
+
#
|
|
661
|
+
# @yield [row]
|
|
662
|
+
# @yieldparam row [StreamRow, Elements::Row]
|
|
663
|
+
# @return [Enumerator, void]
|
|
664
|
+
# @api public
|
|
665
|
+
#: () { (StreamRow | Elements::Row) -> void } -> void
|
|
666
|
+
#: | () -> Enumerator[StreamRow | Elements::Row, void]
|
|
667
|
+
def each(&)
|
|
668
|
+
each_row(&)
|
|
669
|
+
end
|
|
613
670
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
671
|
+
# Loads this sheet completely into an in-memory Elements::Worksheet,
|
|
672
|
+
# enabling coordinate random access (sheet["A1"]), row lookups (row_at),
|
|
673
|
+
# and immutable cell updates (update_cell).
|
|
674
|
+
#
|
|
675
|
+
# @return [Elements::Worksheet] The fully parsed in-memory worksheet.
|
|
676
|
+
# @api public
|
|
677
|
+
#: () -> Elements::Worksheet
|
|
678
|
+
def load
|
|
679
|
+
Xlsxrb.send(:build_worksheet, @name, @sheet_xml, @shared_strings, @styles)
|
|
623
680
|
end
|
|
681
|
+
alias to_worksheet load
|
|
624
682
|
end
|
|
625
683
|
|
|
626
684
|
# Builds an in-memory Elements::Workbook using a declarative DSL.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Generated from lib/xlsxrb/elements/coordinate_access.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Xlsxrb
|
|
4
|
+
module Elements
|
|
5
|
+
# Mixin providing coordinate-based and random-access cell/row lookups
|
|
6
|
+
# for in-memory worksheet structures.
|
|
7
|
+
#
|
|
8
|
+
# Expects the including class to provide a `#rows` method returning an `Array<Elements::Row>`.
|
|
9
|
+
#
|
|
10
|
+
# @api public
|
|
11
|
+
module CoordinateAccess
|
|
12
|
+
# Returns a Hash mapping Excel cell references (e.g. "A1") to Cell objects.
|
|
13
|
+
#
|
|
14
|
+
# @return [Hash<String, Elements::Cell>]
|
|
15
|
+
# : () -> Hash[String, Elements::Cell]
|
|
16
|
+
def cells_hash: () -> Hash[String, Elements::Cell]
|
|
17
|
+
|
|
18
|
+
# Returns all cells ordered by row and column index.
|
|
19
|
+
#
|
|
20
|
+
# @return [Array<Elements::Cell>]
|
|
21
|
+
# @api public
|
|
22
|
+
# : () -> Array[Elements::Cell]
|
|
23
|
+
def cells: () -> Array[Elements::Cell]
|
|
24
|
+
|
|
25
|
+
# Access a cell by its Excel-style reference (e.g. "A1").
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# sheet["A1"] #=> #<Elements::Cell value="Hello">
|
|
29
|
+
#
|
|
30
|
+
# @param ref [String, Symbol] Cell reference (e.g. "A1" or :A1).
|
|
31
|
+
# @return [Elements::Cell, nil]
|
|
32
|
+
# @api public
|
|
33
|
+
# : (String | Symbol ref) -> Elements::Cell?
|
|
34
|
+
def []: (String | Symbol ref) -> Elements::Cell?
|
|
35
|
+
|
|
36
|
+
# Returns the row at the given 0-based index, or nil.
|
|
37
|
+
#
|
|
38
|
+
# @param index [Integer] 0-based row index.
|
|
39
|
+
# @return [Elements::Row, nil]
|
|
40
|
+
# @api public
|
|
41
|
+
# : (Integer index) -> Elements::Row?
|
|
42
|
+
def row_at: (Integer index) -> Elements::Row?
|
|
43
|
+
|
|
44
|
+
# Returns the first row in the sheet, or nil.
|
|
45
|
+
#
|
|
46
|
+
# @return [Elements::Row, nil]
|
|
47
|
+
# @api public
|
|
48
|
+
# : () -> Elements::Row?
|
|
49
|
+
def first_row: () -> Elements::Row?
|
|
50
|
+
|
|
51
|
+
# Returns the last row in the sheet, or nil.
|
|
52
|
+
#
|
|
53
|
+
# @return [Elements::Row, nil]
|
|
54
|
+
# @api public
|
|
55
|
+
# : () -> Elements::Row?
|
|
56
|
+
def last_row: () -> Elements::Row?
|
|
57
|
+
|
|
58
|
+
# Returns the raw cell value at the given Excel-style reference (e.g. "A1").
|
|
59
|
+
#
|
|
60
|
+
# @example
|
|
61
|
+
# sheet.cell_value("A1") #=> "Sales Report"
|
|
62
|
+
#
|
|
63
|
+
# @param ref [String] Cell reference (e.g. "A1").
|
|
64
|
+
# @return [Object, nil]
|
|
65
|
+
# @api public
|
|
66
|
+
# : (String ref) -> untyped
|
|
67
|
+
def cell_value: (String ref) -> untyped
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|