class SeeAsVee::Sheet

Constants

CELL_ERROR_MARKER
CELL_ERROR_STYLE
LEAVE_ERROR_MARKER
WORK_SHEET_NAME

Attributes

checkers[R]
formatters[R]
rows[R]

Public Class Methods

new(whatever, formatters: {}) click to toggle source
# File lib/see_as_vee/sheet.rb, line 17
def initialize whatever, formatters: {}, checkers: {}, skip_blank_rows: false
  @formatters = formatters.map { |k, v| [str_to_sym(k), v] }.to_h
  @checkers = checkers.map { |k, v| [str_to_sym(k), v] }.to_h
  @rows = whatever.is_a?(Array) ? whatever : Helpers.harvest_csv(whatever)

  @rows = @rows.map do |row|
    row unless skip_blank_rows && row.compact.empty?
  end.compact.map.with_index do |row, idx|
    idx.zero? ? row : plough_row(row)
  end
end

Public Instance Methods

[](index, key = nil) click to toggle source
# File lib/see_as_vee/sheet.rb, line 44
def [] index, key = nil
  key.nil? ? values[index] : values[index][header_index(key)]
end
each() { |idx, errors, result| ... } click to toggle source
# File lib/see_as_vee/sheet.rb, line 48
def each
  return enum_for unless block_given?

  values.each_with_index do |row, idx|
    result = headers.zip(row).to_h
    errors = result.select { |_, v| malformed?(v) }
    yield idx, errors, result
  end
end
headers(symbolic = false) click to toggle source
# File lib/see_as_vee/sheet.rb, line 33
def headers symbolic = false
  headers = @rows.first
  unless headers.uniq.length == headers.length
    groups = headers.group_by { |h| h }.select { |_, group| group.size > 1 }
    headers = headers.map.with_index { |e, idx| groups[e].nil? ? e : "#{e} #{idx}" }
  end

  headers = headers.map.with_index { |s, ind| str_to_sym(s || "col #{ind}") } if symbolic
  headers
end
map() { |headers(true).zip(row).to_h| ... } click to toggle source
# File lib/see_as_vee/sheet.rb, line 58
def map
  return enum_for unless block_given?

  values.map do |row|
    yield headers(true).zip(row).to_h
  end
end
produce(csv: true, xlsx: nil, **params) click to toggle source
# File lib/see_as_vee/sheet.rb, line 66
def produce csv: true, xlsx: nil, **params
  [csv && produce_csv(**params), xlsx && produce_xlsx(**params)]
end
values() click to toggle source
# File lib/see_as_vee/sheet.rb, line 29
def values
  @rows[1..-1]
end

Private Instance Methods

check_cell(cell, i) click to toggle source

rubocop:disable Style/MultilineTernaryOperator

# File lib/see_as_vee/sheet.rb, line 141
def check_cell cell, i
  f = @checkers[headers(true)[i]]
  case f
  when Proc then f.call(cell)
  when Symbol then cell.public_send(f)
  else true
  end ? cell : CELL_ERROR_MARKER + cell.to_s.split('').map { |c| "#{c}\u0336" }.join
end
format_cell(cell, i) click to toggle source
# File lib/see_as_vee/sheet.rb, line 132
def format_cell cell, i
  case f = @formatters[headers(true)[i]]
  when Proc then f.call(cell)
  when Symbol then cell.public_send f
  else cell
  end
end
header_index(key) click to toggle source
# File lib/see_as_vee/sheet.rb, line 114
def header_index key
  headers(true).index(str_to_sym(key))
end
malformed?(str) click to toggle source
# File lib/see_as_vee/sheet.rb, line 72
def malformed? str
  str.to_s.start_with? CELL_ERROR_MARKER
end
plough_row(row) click to toggle source
# File lib/see_as_vee/sheet.rb, line 122
def plough_row row
  return row if @formatters.empty? && @checkers.empty? # performance

  row.map.with_index do |cell, i|
    cell = format_cell(cell, i) unless @formatters.empty?
    cell = check_cell(cell, i) unless @checkers.empty?
    cell
  end
end
produce_csv(**params) click to toggle source
# File lib/see_as_vee/sheet.rb, line 76
def produce_csv **params
  return if @rows.empty?

  for_ms_excel = params.delete(:ms_excel) == true
  params.merge!(col_sep: "\t") if for_ms_excel

  Tempfile.open(['see_as_vee', '.csv']).tap do |f|
    content =
      CSV.generate(params) do |csv|
        @rows.each { |row| csv << row }
      end
    content =
      "\xFF\xFE".force_encoding(Encoding::UTF_16LE) <<
      content.encode(Encoding::UTF_16LE) if for_ms_excel

    mode = for_ms_excel ? "w:UTF-16LE" : "w:UTF-8"

    File.open(f.path, mode) { |f| f.write content }
  end
end
produce_xlsx(**params) click to toggle source
# File lib/see_as_vee/sheet.rb, line 97
def produce_xlsx **params
  params, axlsx_params = split_params(params)
  Tempfile.new(['see_as_vee', '.xlsx']).tap do |f|
    Axlsx::Package.new do |p|
      red = p.workbook.styles.add_style(**params[:ces]) if params[:ces].is_a?(Hash)
      p.workbook.add_worksheet(**axlsx_params) do |sheet|
        @rows.each do |row|
          styles = row.map { |cell| malformed?(cell) ? red : nil }
          row = row.map { |cell| malformed?(cell) ? cell.to_s.gsub(/\A#{CELL_ERROR_MARKER}/, '') : cell } if params[:lem]
          sheet.add_row row, style: styles
        end
      end
      p.serialize(f.path)
    end
  end
end
split_params(params) click to toggle source

rubocop:enable Style/MultilineTernaryOperator

# File lib/see_as_vee/sheet.rb, line 151
def split_params params
  params = params.dup
  [
    { ces: params.delete(:cell_error_style) { CELL_ERROR_STYLE.dup },
      lem: params.delete(:leave_error_marker) { LEAVE_ERROR_MARKER } },
    { name: WORK_SHEET_NAME }.merge(params)
  ]
end
squish(str) click to toggle source
# File lib/see_as_vee/sheet.rb, line 160
def squish str
  return str unless str.is_a?(String)

  str.
    gsub(/\A[[:space:]]+/, '').
    gsub(/[[:space:]]+\z/, '').
    gsub(/[[:space:]]+/, ' ')
end
str_to_sym(str) click to toggle source
# File lib/see_as_vee/sheet.rb, line 118
def str_to_sym str
  str.is_a?(Symbol) ? str : squish(str).downcase.gsub(/\W/, '_').to_sym
end