109 lines
3.2 KiB
Ruby
109 lines
3.2 KiB
Ruby
require_relative 'storage_plugin'
|
|
|
|
class DataPipeline
|
|
#
|
|
# options
|
|
# file: path of the .xlsx file
|
|
# sheet: read: name or index (0 = first, default), write: name (default 'Daten')
|
|
# headers: first row holds the column names (default true)
|
|
# columns: column names when there is no header row, fixed column order for writing
|
|
# skip: read: rows before the header row (title lines), default 0
|
|
#
|
|
# read needs the gem roo, write the gem caxlsx; both are loaded on first use.
|
|
# text stays text when writing (no number conversion, leading zeros are kept).
|
|
#
|
|
class ExcelStorage < StoragePlugin
|
|
def initialize **options, &block
|
|
@options = { headers: true, skip: 0 }.merge(options)
|
|
@file_name = @options.fetch(:file) { raise ArgumentError, 'ExcelStorage: option file missing' }
|
|
|
|
yield if block_given?
|
|
end
|
|
|
|
# without block all rows, with block row by row (xlsx is streamed)
|
|
def read(&block)
|
|
return enum_for(:each_row).to_a unless block
|
|
|
|
each_row(&block)
|
|
nil
|
|
end
|
|
|
|
def write rows
|
|
open_sheet
|
|
rows.each { |row| write_row row }
|
|
ensure
|
|
close
|
|
end
|
|
|
|
def write_row row
|
|
open_sheet
|
|
if @columns.nil?
|
|
@columns = (@options[:columns] || row.keys).map(&:to_s)
|
|
add_row @columns, header: true if @options[:headers]
|
|
end
|
|
add_row(@columns.map { |col| fetch(row, col) })
|
|
end
|
|
|
|
def close
|
|
return unless @package
|
|
|
|
@package.serialize(@file_name)
|
|
@package = @sheet = @columns = nil
|
|
end
|
|
|
|
private
|
|
|
|
def each_row
|
|
self.class.require_gem 'roo'
|
|
book = Roo::Spreadsheet.open(@file_name, extension: File.extname(@file_name).delete('.').to_sym)
|
|
sheet = book.sheet(@options.fetch(:sheet, 0))
|
|
keys = @options[:headers] ? nil : @options[:columns]&.map(&:to_s)
|
|
skip = @options[:skip]
|
|
|
|
rows_of(sheet).each do |values|
|
|
next if (skip -= 1) >= 0
|
|
next if values.all? { |v| v.nil? || v.to_s.strip.empty? }
|
|
|
|
if keys.nil?
|
|
keys = values.each_with_index.map { |v, i| v.nil? ? i.to_s : v.to_s }
|
|
next
|
|
end
|
|
yield keys.zip(values).to_h
|
|
end
|
|
ensure
|
|
book&.close
|
|
end
|
|
|
|
# xlsx row by row without loading the sheet, other formats over the row api
|
|
def rows_of sheet
|
|
if sheet.respond_to?(:each_row_streaming)
|
|
Enumerator.new do |y|
|
|
sheet.each_row_streaming(pad_cells: true) { |cells| y << cells.map { |c| c&.value } }
|
|
end
|
|
else
|
|
(sheet.first_row.to_i..sheet.last_row.to_i).lazy.map { |i| sheet.row(i) }
|
|
end
|
|
end
|
|
|
|
def open_sheet
|
|
return if @package
|
|
|
|
self.class.require_gem 'axlsx', 'caxlsx'
|
|
@package = Axlsx::Package.new
|
|
@sheet = @package.workbook.add_worksheet(name: @options.fetch(:sheet, 'Daten').to_s[0, 31])
|
|
@bold = @package.workbook.styles.add_style(b: true)
|
|
end
|
|
|
|
def add_row values, header: false
|
|
types = values.map { |v| v.is_a?(String) ? :string : nil }
|
|
@sheet.add_row(values, types:, style: header ? @bold : nil)
|
|
end
|
|
|
|
def fetch row, col
|
|
return row[col] if row.key?(col)
|
|
|
|
row.key?(col.to_sym) ? row[col.to_sym] : nil
|
|
end
|
|
end
|
|
end
|