Files
datapipeline/lib/datapipeline/storage/excel.rb
T

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