roo-oxide 0.0.2-aarch64-linux

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.
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roo
4
+ module Oxide
5
+ # Prepended to Roo::Excelx. roo extracts every worksheet to its temporary
6
+ # directory when it opens a workbook. With the native reader on, a
7
+ # workbook leaves its worksheets in the zip instead:
8
+ # the native reader inflates the one it reads, and roo's own code finds
9
+ # the file extracted the first time it reads it (DeferredSheetDoc). roo
10
+ # reads the workbook's file only while it opens it, so it opens a snapshot
11
+ # in memory for small ZIPs or in its temporary directory for large ones.
12
+ # The workbook reads the same after its file
13
+ # is changed, moved, or deleted. The native extension also writes the
14
+ # files roo does extract, such as sharedStrings.xml: rubyzip's inflating
15
+ # leaves several times the file's size in short-lived Strings, which
16
+ # Ruby's GC frees only after they have grown the process.
17
+ module DeferredSheets
18
+ # roo versions whose workbook extraction these methods reproduce.
19
+ ROO_VERSIONS = %w[4.0.0].freeze
20
+ COMPRESSION_METHODS = [::Zip::Entry::STORED, ::Zip::Entry::DEFLATED].freeze
21
+ METADATA_FILES = %w[roo_workbook.xml roo_workbook.xml.rels roo_styles.xml].freeze
22
+ # Nokogiri's memory parser passes the byte length as a signed C int.
23
+ MAX_METADATA_SIZE = (2**31) - 1
24
+ MEMORY_ZIP_SIZE = 256 * 1024
25
+ private_constant :ROO_VERSIONS, :COMPRESSION_METHODS, :METADATA_FILES, :MAX_METADATA_SIZE, :MEMORY_ZIP_SIZE
26
+
27
+ # `path` as the native extension takes it. On Windows, Ruby opens a
28
+ # binary path's bytes as UTF-8, where magnus refuses a binary string.
29
+ # The extension raises EncodingError for a path it cannot take.
30
+ def self.native_path(path)
31
+ path = ::File.path(path)
32
+ return path unless Gem.win_platform? && path.encoding == Encoding::BINARY
33
+
34
+ path.dup.force_encoding(Encoding::UTF_8)
35
+ end
36
+
37
+ # Extended onto each Zip::Entry of the copy that the native extension
38
+ # can read: `oxide_entry` is its Native::ZipEntry, which writes the
39
+ # file when roo extracts the entry. When the native extraction fails,
40
+ # or cannot take the path, rubyzip extracts it as roo does, and raises
41
+ # or warns as roo does.
42
+ module NativeEntry
43
+ attr_accessor :oxide_entry, :oxide_metadata
44
+
45
+ def extract(entry_path, destination_directory:)
46
+ path = ::File.join(destination_directory, entry_path)
47
+ if oxide_metadata && METADATA_FILES.include?(entry_path) &&
48
+ size <= MAX_METADATA_SIZE && !::File.exist?(path)
49
+ oxide_metadata[entry_path] = Native.metadata_bytes(oxide_entry)
50
+ return self
51
+ end
52
+ oxide_entry.extract(DeferredSheets.native_path(path))
53
+ self
54
+ rescue IOError, EncodingError
55
+ super
56
+ end
57
+ end
58
+
59
+ # A worksheet left in the zip: `zip_entry` is rubyzip's Zip::Entry for
60
+ # it, a NativeEntry.
61
+ class Sheet
62
+ def initialize(zip_entry)
63
+ @zip_entry = zip_entry
64
+ @lock = Mutex.new
65
+ @extracted = false
66
+ end
67
+
68
+ # Its Native::ZipEntry.
69
+ def entry
70
+ @zip_entry.oxide_entry
71
+ end
72
+
73
+ # Extracts the worksheet where roo would have, once; another thread
74
+ # waits until the file is complete.
75
+ def extract(path)
76
+ @lock.synchronize do
77
+ next if @extracted
78
+
79
+ @zip_entry.extract(::File.basename(path), destination_directory: ::File.dirname(path))
80
+ @extracted = true
81
+ end
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def process_zipfile(zipfilename_or_stream)
88
+ @oxide_copy = oxide_copy(zipfilename_or_stream)
89
+ @oxide_copy ? super(@oxide_copy) : super
90
+ end
91
+
92
+ # The snapshot roo opens: a stream, a copy in roo's temporary
93
+ # directory, or the file roo wrote there itself for a URL or
94
+ # `packed: :zip`. An empty file is left to roo: rubyzip's error names
95
+ # the file it opened.
96
+ def oxide_copy(source)
97
+ return unless Oxide.enabled? && ROO_VERSIONS.include?(Roo::VERSION)
98
+
99
+ if is_stream?(source)
100
+ source.seek(0)
101
+ elsif ::File.dirname(source) == @tmpdir
102
+ return source
103
+ end
104
+
105
+ write_oxide_copy(source)
106
+ end
107
+
108
+ def memory_copy(source)
109
+ size = is_stream?(source) ? (source.size if source.respond_to?(:size)) : ::File.size(source)
110
+ ::StringIO.new(is_stream?(source) ? source.read : ::File.binread(source)) if size&.between?(1, MEMORY_ZIP_SIZE)
111
+ end
112
+
113
+ def write_oxide_copy(source)
114
+ copy = memory_copy(source)
115
+ return copy if copy
116
+
117
+ path = "#{@tmpdir}/roo_oxide.xlsx"
118
+ # StringIO#read shares its full String; IO.copy_stream's partial reads copy its buffer.
119
+ if source.is_a?(::StringIO)
120
+ ::File.binwrite(path, source.read)
121
+ else
122
+ IO.copy_stream(source, path)
123
+ end
124
+ path if ::File.size?(path)
125
+ end
126
+
127
+ def process_zipfile_entries(entries)
128
+ if @oxide_copy
129
+ @shared.extend(MetadataShared)
130
+ @shared.oxide_metadata = {}
131
+ copy = @oxide_copy
132
+ zip = copy.is_a?(::StringIO) ? Native::ZipBuffer.new(copy.string) : DeferredSheets.native_path(copy)
133
+ native_entries(entries, zip)
134
+ end
135
+ super
136
+ end
137
+
138
+ # Makes each entry the native extension can read a NativeEntry. roo
139
+ # raises or warns while extracting an entry that is not a file,
140
+ # encrypted, or compressed by another method; rubyzip still extracts it.
141
+ def native_entries(entries, zip)
142
+ metadata = metadata_entries(entries)
143
+ entries.each do |entry|
144
+ next unless entry.file? && !entry.encrypted? && COMPRESSION_METHODS.include?(entry.compression_method)
145
+
146
+ entry.extend(NativeEntry).oxide_entry =
147
+ Native::ZipEntry.new(zip, entry.local_header_offset, entry.compressed_size, entry.size,
148
+ entry.compression_method)
149
+ entry.oxide_metadata = @shared.oxide_metadata if metadata.include?(entry)
150
+ end
151
+ rescue EncodingError
152
+ nil
153
+ end
154
+
155
+ def metadata_entries(entries)
156
+ metadata = [entries.find { |entry| entry.name[/workbook.xml$/] },
157
+ entries.find { |entry| entry.name[/workbook.xml.rels$/] }]
158
+ styles = entries.select { |entry| entry.name.downcase[/styles.xml$/] && !entry.name.downcase[/richdata/] }
159
+ metadata + (styles.one? ? styles : [])
160
+ end
161
+
162
+ def extract_worksheet_ids(entries, path)
163
+ return super unless @oxide_copy
164
+
165
+ entry = entries.find { |item| item.name[/workbook.xml$/] }
166
+ return super unless entry.is_a?(NativeEntry)
167
+
168
+ entry.extract(::File.basename(path), destination_directory: ::File.dirname(path))
169
+ workbook.sheets.map { |sheet| sheet["id"] }
170
+ end
171
+
172
+ def extract_worksheet_rels(entries, path)
173
+ return super unless @oxide_copy
174
+
175
+ entry = entries.find { |item| item.name[/workbook.xml.rels$/] }
176
+ return super unless entry
177
+
178
+ entry.extract(::File.basename(path), destination_directory: ::File.dirname(path))
179
+ bytes = @shared.oxide_metadata.delete(::File.basename(path))
180
+ document = (bytes ? ::Nokogiri::XML(bytes, path) : Roo::Utils.load_xml(path)).remove_namespaces!
181
+ document.xpath("//Relationship").each_with_object({}) do |relationship, sheets|
182
+ sheets[relationship["Id"]] = relationship["Target"] if worksheet_types.include?(relationship["Type"])
183
+ end
184
+ end
185
+
186
+ # Roo::Excelx#extract_sheets_in_order, without the extraction. roo
187
+ # raises or warns reading a worksheet that is missing or that the
188
+ # native extension cannot read; it still does.
189
+ def extract_sheets_in_order(entries, sheet_ids, sheets, tmpdir)
190
+ found = (sheet_ids & sheets.keys).map { |id| sheet_entry(entries, sheets[id]) }
191
+ return super unless found.all?(NativeEntry)
192
+
193
+ deferred = found.each_with_index.to_h { |entry, i| ["#{tmpdir}/roo_sheet#{i + 1}", Sheet.new(entry)] }
194
+ sheet_files.concat(deferred.keys)
195
+ @sheet_files.concat(deferred.keys)
196
+ @shared.instance_variable_set(:@oxide_deferred_sheets, deferred)
197
+ end
198
+
199
+ def sheet_entry(entries, name)
200
+ name && entries.find { |e| "/#{e.name}".end_with?(name) }
201
+ end
202
+ end
203
+
204
+ # Roo constructs these extractors after extraction. Pass retained bytes
205
+ # alongside their original paths, including when native reading is off.
206
+ module MetadataShared
207
+ attr_accessor :oxide_metadata
208
+
209
+ def workbook
210
+ @workbook ||= Roo::Excelx::Workbook.new(::File.join(@dir, "roo_workbook.xml"),
211
+ oxide_xml: oxide_metadata["roo_workbook.xml"])
212
+ end
213
+
214
+ def styles
215
+ @styles ||= Roo::Excelx::Styles.new(::File.join(@dir, "roo_styles.xml"),
216
+ oxide_xml: oxide_metadata["roo_styles.xml"])
217
+ end
218
+ end
219
+
220
+ # Roo's document cache is weak: keep the source bytes so metadata can be
221
+ # parsed again after GC, without keeping a large Nokogiri tree alive.
222
+ module MetadataDocument
223
+ def initialize(*, oxide_xml: nil, **)
224
+ @oxide_xml = oxide_xml
225
+ super(*, **)
226
+ end
227
+
228
+ private
229
+
230
+ def doc
231
+ return super unless @oxide_xml
232
+
233
+ instance_cache(:@doc) do
234
+ raise Roo::FileNotFound, "#{@path} file not found" unless doc_exists?
235
+
236
+ ::Nokogiri::XML(@oxide_xml, @path).remove_namespaces!
237
+ end
238
+ end
239
+
240
+ def doc_exists?
241
+ @oxide_xml ? ::File.directory?(::File.dirname(@path)) : super
242
+ end
243
+ end
244
+
245
+ # Prepended to Roo::Excelx::SheetDoc, below Roo::Oxide::SheetDoc. roo
246
+ # reads a worksheet's file in #doc, #dimensions, and #each_row_streaming;
247
+ # each one extracts a deferred worksheet first.
248
+ module DeferredSheetDoc
249
+ def dimensions
250
+ extract_deferred_sheet
251
+ super
252
+ end
253
+
254
+ def each_row_streaming(&)
255
+ extract_deferred_sheet
256
+ super
257
+ end
258
+
259
+ private
260
+
261
+ def doc
262
+ extract_deferred_sheet
263
+ super
264
+ end
265
+
266
+ # What the native reader reads: the worksheet's Native::ZipEntry, or
267
+ # its file when it was not left in the zip.
268
+ def sheet_source
269
+ deferred_sheet&.entry || @path
270
+ end
271
+
272
+ def extract_deferred_sheet
273
+ deferred_sheet&.extract(@path)
274
+ end
275
+
276
+ # nil once the workbook is closed or collected: its temporary directory
277
+ # is gone, and roo raises reading the worksheet's file.
278
+ def deferred_sheet
279
+ sheet = @shared.instance_variable_get(:@oxide_deferred_sheets)&.[](@path)
280
+ sheet if sheet && ::File.directory?(::File.dirname(@path))
281
+ end
282
+ end
283
+ end
284
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roo
4
+ module Oxide
5
+ # Roo exposes a mutable sheet list, so names must be checked on each lookup.
6
+ module SheetSelection
7
+ private
8
+
9
+ def validate_sheet!(sheet)
10
+ case sheet
11
+ when String then super unless sheets.include?(sheet)
12
+ else
13
+ super
14
+ end
15
+ end
16
+ end
17
+
18
+ # Prepended to Roo::Excelx. With options, Roo::Base#each reads every value
19
+ # through #cell, which resolves the sheet and looks up an Array key in its
20
+ # cells hash, one cell at a time. These keep roo's header handling (its own
21
+ # private methods) and read the cells with less work per cell.
22
+ #
23
+ # Value and type lookups read the store; other single-cell lookups read
24
+ # Sheet#cell_at instead of Sheet#cells, and
25
+ # rows read Sheet#row, so they build only the cells their values need.
26
+ # #each without options reads the native store sequentially, or uses a
27
+ # RowIndex once every cell is built.
28
+ module Excelx
29
+ def each(options = {}, &)
30
+ return super unless block_given?
31
+
32
+ if options.empty?
33
+ build_cells
34
+ return native_rows? ? each_without_options(&) : super
35
+ end
36
+ return super unless Oxide.enabled?
37
+
38
+ each_with_headers(header_columns(options), &)
39
+ end
40
+
41
+ def parse(options = {})
42
+ return super if block_given? || options.empty? || !Oxide.enabled?
43
+
44
+ rows = rows_for(header_columns(options))
45
+ options[:headers] == true ? rows : rows.drop(1)
46
+ end
47
+
48
+ def cell(row, col, sheet = nil)
49
+ row, col = normalize(row, col) if row.is_a?(String) || col.is_a?(String)
50
+ sheet = sheet_for(sheet)
51
+ lazy = sheet.lazy_cells
52
+ return lazy.value_at(row, col) if lazy
53
+
54
+ cell = sheet.cell_at(row, col)
55
+ cell.value if cell.respond_to?(:value)
56
+ end
57
+
58
+ def formula(row, col, sheet = nil)
59
+ row, col = normalize(row, col) if row.is_a?(String) || col.is_a?(String)
60
+ cell = sheet_for(sheet).cell_at(row, col)
61
+ cell.formula if cell.respond_to?(:formula)
62
+ end
63
+
64
+ def font(row, col, sheet = nil)
65
+ row, col = normalize(row, col) if row.is_a?(String) || col.is_a?(String)
66
+ cell = sheet_for(sheet).cell_at(row, col)
67
+ definition_index = cell.style if cell.respond_to?(:style)
68
+ styles.definitions[definition_index] if definition_index
69
+ end
70
+
71
+ def celltype(row, col, sheet = nil)
72
+ row, col = normalize(row, col) if row.is_a?(String) || col.is_a?(String)
73
+ sheet = sheet_for(sheet)
74
+ lazy = sheet.lazy_cells
75
+ return lazy.type_at(row, col) if lazy
76
+
77
+ cell = sheet.cell_at(row, col)
78
+ cell.type if cell.respond_to?(:type)
79
+ end
80
+
81
+ def excelx_type(row, col, sheet = nil)
82
+ row, col = normalize(row, col) if row.is_a?(String) || col.is_a?(String)
83
+ cell = sheet_for(sheet).cell_at(row, col)
84
+ cell.cell_type if cell.respond_to?(:cell_type)
85
+ end
86
+
87
+ def excelx_value(row, col, sheet = nil)
88
+ row, col = normalize(row, col) if row.is_a?(String) || col.is_a?(String)
89
+ cell = sheet_for(sheet).cell_at(row, col)
90
+ cell.cell_value if cell.respond_to?(:cell_value)
91
+ end
92
+
93
+ def empty?(row, col, sheet = nil)
94
+ sheet = sheet_for(sheet)
95
+ key = normalize(row, col)
96
+ cell = (sheet.lazy_cells || sheet.cells)[key]
97
+ !cell || cell.empty? ||
98
+ (row < sheet.first_row || row > sheet.last_row || col < sheet.first_column || col > sheet.last_column)
99
+ end
100
+
101
+ def set(...)
102
+ @oxide_set_calls = (@oxide_set_calls || 0) + 1
103
+ super
104
+ end
105
+
106
+ private
107
+
108
+ def clean_sheet(name)
109
+ store = sheet_for(name).lazy_cells if Oxide.enabled?
110
+ return super unless store
111
+
112
+ store.clean(self)
113
+ @cleaned[name] = true
114
+ end
115
+
116
+ def each_with_headers(headers)
117
+ first, last = headers.map(&:last).minmax
118
+ slots = headers.map { |key, column| [key, column - first] }
119
+ @header_line.upto(last_row) do |line|
120
+ lazy = sheet_for(nil).lazy_cells unless headers.empty?
121
+ yield(lazy ? lazy.hash_row(line, first, last, slots) : cells_header_row(line, headers))
122
+ end
123
+ end
124
+
125
+ def cells_header_row(line, headers)
126
+ cells = nil
127
+ headers.each_with_object({}) do |(key, column), row|
128
+ cells ||= sheet_for(nil).cells
129
+ row[key] = safe_send(cells[normalize(line, column)], :value)
130
+ end
131
+ end
132
+
133
+ # Roo::Base#each, up to its loop over the rows.
134
+ def header_columns(options)
135
+ clean_sheet_if_need(options)
136
+ build_cells
137
+ search_or_set_header(options)
138
+ @headers || (first_column..last_column).to_h { |column| [cell(@header_line, column), column] }
139
+ end
140
+
141
+ # Builds every cell before roo's next step unless the lazy cells answer it. When that raises,
142
+ # roo's own read raises the same error where roo raises it.
143
+ def build_cells
144
+ sheet = sheet_for(nil)
145
+ sheet.cells unless sheet.lazy_cells
146
+ rescue StandardError
147
+ nil
148
+ end
149
+
150
+ def rows_for(headers)
151
+ fields = headers.map { |key, column| [key, normalize(0, column).last] }
152
+ sheet_for(nil).parse_rows(@header_line.upto(last_row), fields)
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roo
4
+ module Oxide
5
+ # Roo 4.0.0's output formatting, using the native records while the
6
+ # sheet's public cells hash has not been materialized.
7
+ module Formatters
8
+ def formatted_value(row, col, sheet = nil)
9
+ row, col = normalize(row, col) if row.is_a?(String) || col.is_a?(String)
10
+ cell = sheet_for(sheet).cell_at(row, col)
11
+ return formatter_temporal_value(cell) if native_formatters? && cell.is_a?(Roo::Excelx::Cell::DateTime)
12
+
13
+ cell.formatted_value if cell.respond_to?(:formatted_value)
14
+ end
15
+
16
+ def to_yaml(prefix = {}, from_row = nil, from_column = nil, to_row = nil, to_column = nil, sheet = default_sheet) # rubocop:disable Metrics/ParameterLists
17
+ return super unless native_formatters?
18
+ return "" unless first_row
19
+
20
+ area = formatter_area(sheet, [from_row, from_column, to_row, to_column])
21
+ store = sheet_for(sheet).lazy_cells
22
+ return super unless store && area.all?(Integer)
23
+
24
+ store.to_yaml(self, prefix, area)
25
+ end
26
+
27
+ def to_matrix(from_row = nil, from_column = nil, to_row = nil, to_column = nil, sheet = default_sheet) # rubocop:disable Metrics/ParameterLists
28
+ return super unless native_formatters?
29
+
30
+ require "matrix"
31
+ return ::Matrix.empty unless first_row
32
+
33
+ area = formatter_rectangle(sheet, [from_row, from_column, to_row, to_column])
34
+ store = sheet_for(sheet).lazy_cells
35
+ return super unless store && area.all?(Integer)
36
+
37
+ ::Matrix.rows(store.matrix_rows(area))
38
+ end
39
+
40
+ def to_xml
41
+ return super unless native_formatters?
42
+ return super if sheets.empty?
43
+
44
+ targets = sheets.map { |name| sheet_for(name) }
45
+ return super unless targets.all?(&:lazy_cells)
46
+
47
+ result = +"<?xml version=\"1.0\"?>\n<spreadsheet>\n"
48
+ document = Nokogiri::XML::Document.new
49
+ sheets.zip(targets).each do |name, target|
50
+ self.default_sheet = name
51
+ result << formatter_xml_sheet(document, name, target)
52
+ end
53
+ result << "</spreadsheet>\n"
54
+ end
55
+
56
+ private
57
+
58
+ def formatter_temporal_value(cell)
59
+ if cell.is_a?(Roo::Excelx::Cell::Time)
60
+ @oxide_time_formats ||= Hash.new { |formats, encoding| formats[encoding] = {} }
61
+ formatter = @oxide_time_formats[cell.format.encoding][cell.format] ||=
62
+ cell.format.gsub(/#{Roo::Excelx::Cell::DateTime::TIME_FORMATS.keys.join("|")}/,
63
+ Roo::Excelx::Cell::DateTime::TIME_FORMATS)
64
+ return cell.instance_variable_get(:@datetime).strftime(formatter)
65
+ end
66
+
67
+ @oxide_datetime_formats ||= Hash.new { |formats, encoding| formats[encoding] = {} }
68
+ formatter = @oxide_datetime_formats[cell.format.encoding][cell.format] ||= formatter_datetime(cell)
69
+ formatter ? cell.value.strftime(formatter) : cell.formatted_value
70
+ end
71
+
72
+ def formatter_datetime(cell)
73
+ parts = cell.format.downcase.split(" ", 0).map do |part|
74
+ parsed = cell.send(:parse_date_or_time_format, part)
75
+ return nil unless parsed
76
+
77
+ parsed
78
+ end
79
+ parts.join(" ")
80
+ end
81
+
82
+ def native_formatters?
83
+ Oxide.enabled? && Roo::VERSION == "4.0.0"
84
+ end
85
+
86
+ def formatter_area(sheet, rectangle)
87
+ target = sheet_for(sheet)
88
+ formatter_rectangle(sheet, rectangle) +
89
+ [target.first_row, target.first_column, target.last_row, target.last_column]
90
+ end
91
+
92
+ def formatter_rectangle(sheet, rectangle)
93
+ bounds = %i[first_row first_column last_row last_column]
94
+ rectangle.zip(bounds).map { |requested, bound| requested || public_send(bound, sheet) }
95
+ end
96
+
97
+ def formatter_xml_sheet(document, name, target)
98
+ element = document.create_element("sheet", name:)
99
+ cells = first_row ? target.lazy_cells.xml_cells(self, formatter_area(name, [nil] * 4)) : ""
100
+ " #{element.to_xml.delete_suffix("/>")}#{cells.empty? ? "/>\n" : ">\n#{cells} </sheet>\n"}"
101
+ end
102
+
103
+ def formatter_yaml_prefix(prefix)
104
+ text = +""
105
+ prefix.each { |key, value| text << " #{key}: #{value} \n" }
106
+ text
107
+ end
108
+
109
+ def formatter_xml_cell(value, row, column, type)
110
+ Nokogiri::XML::Document.new.create_element("cell", value, row:, column:, type:).to_xml
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roo
4
+ module Oxide
5
+ # Answers Excelx#row for the default sheet from its cells in
6
+ # first_row..last_row and first_column..last_column, laid out in one pass
7
+ # over Sheet#cells: in one flat Array with a slot per column of each row
8
+ # (an Array per row costs a major GC on a large sheet), or, on a sheet
9
+ # with fewer than one cell per MAX_SLOTS_PER_CELL slots, in a Hash of an
10
+ # Array per row of its column offsets and cells. roo answers the first ROO_ROWS
11
+ # requests, so a caller that stops early never pays for that pass, and
12
+ # any other row, a sheet too short to repay the pass (fewer than
13
+ # 3 * ROO_ROWS rows), an empty sheet, and every request once the default
14
+ # sheet changes or Excelx#set writes to the workbook.
15
+ class RowIndex
16
+ ROO_ROWS = 1_000
17
+ MAX_SLOTS_PER_CELL = 4
18
+
19
+ def initialize(workbook)
20
+ @workbook = workbook
21
+ @requests = 0
22
+ end
23
+
24
+ def row(line, set_calls)
25
+ index = index(line) if current?(set_calls)
26
+ return @workbook.row(line) unless index
27
+
28
+ @lines ? sparse_row(@lines[index]) : @cells[index * @width, @width].map! { |cell| cell&.value }
29
+ end
30
+
31
+ private
32
+
33
+ def index(line)
34
+ line - @first_row if line.is_a?(Integer) && line.between?(@first_row, @last_row)
35
+ end
36
+
37
+ def current?(set_calls)
38
+ return false if (@requests += 1) <= ROO_ROWS
39
+
40
+ sheet = @workbook.sheet_for(nil)
41
+ build(sheet, set_calls) unless @sheet
42
+ (@cells || @lines) && @sheet.equal?(sheet) && @set_calls == set_calls
43
+ end
44
+
45
+ def sparse_row(entries)
46
+ values = Array.new(@width)
47
+ return values unless entries
48
+
49
+ i = 0
50
+ while i < entries.size
51
+ values[entries[i]] = entries[i + 1]&.value
52
+ i += 2
53
+ end
54
+ values
55
+ end
56
+
57
+ def build(sheet, set_calls)
58
+ @sheet = sheet
59
+ @set_calls = set_calls
60
+ return unless sheet.first_row && sheet.last_row >= 3 * ROO_ROWS
61
+
62
+ @first_row = sheet.first_row
63
+ @last_row = sheet.last_row
64
+ @first = sheet.first_column
65
+ @last = sheet.last_column
66
+ @width = @last - @first + 1
67
+ lay_out(sheet.cells, @last_row - @first_row + 1)
68
+ end
69
+
70
+ def lay_out(cells, rows)
71
+ if rows * @width > MAX_SLOTS_PER_CELL * cells.size
72
+ @lines = {}
73
+ each_cell(cells) { |index, column, cell| (@lines[index] ||= []).push(column, cell) }
74
+ else
75
+ @cells = Array.new(rows * @width)
76
+ each_cell(cells) { |index, column, cell| @cells[(index * @width) + column] = cell }
77
+ end
78
+ end
79
+
80
+ def each_cell(cells) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
81
+ cells.each_pair do |key, cell|
82
+ next unless key.is_a?(Array) && key.size == 2
83
+
84
+ line, column = key
85
+ next unless line.is_a?(Integer) && column.is_a?(Integer) &&
86
+ line >= @first_row && line <= @last_row && column >= @first && column <= @last
87
+
88
+ yield line - @first_row, column - @first, cell
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roo
4
+ module Oxide
5
+ # Roo::Base#each reads the current sheet's row after every block call.
6
+ module Excelx
7
+ private
8
+
9
+ def native_rows?
10
+ Oxide.enabled? && method(:row).owner.equal?(::Roo::Excelx)
11
+ end
12
+
13
+ def each_without_options(&)
14
+ sheet = sheet_for(nil)
15
+ lazy = sheet.lazy_cells
16
+ return each_lazy_row(sheet, lazy, &) if lazy && sheet.first_column
17
+
18
+ index = RowIndex.new(self)
19
+ 1.upto(last_row) { |line| yield index.row(line, @oxide_set_calls) }
20
+ end
21
+
22
+ def each_lazy_row(sheet, lazy)
23
+ last = last_row
24
+ line = lazy.each_row(last, sheet.first_column, sheet.last_column) do |values|
25
+ yield values
26
+ sheet_for(nil).equal?(sheet) && sheet.lazy_cells.equal?(lazy)
27
+ end
28
+ line.upto(last) { |number| yield row(number) }
29
+ 1
30
+ end
31
+ end
32
+ end
33
+ end