bin/rdb läuft mit --disable-gems (ca. 85 statt 170 ms je Befehl), dafür rechnen Summen und /act mit Rational. Hilfsfunktionen und Konstanten liegen nicht mehr auf Object, Kopf-/Strichzeile und awk-Aufruf an einer Stelle. Tests laufen parallel: 21 statt 70 Sekunden.
122 lines
3.2 KiB
Ruby
122 lines
3.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Rdb
|
|
# A table is a head line of column names, a dash line, then one
|
|
# tab-separated row per line. A list is the same data turned sideways:
|
|
# it starts with a newline and holds "Column<tab>value" lines.
|
|
class Table
|
|
attr_reader :head, :rows
|
|
attr_accessor :list
|
|
|
|
def initialize(head, rows, list: false)
|
|
@head = head
|
|
@rows = rows
|
|
@list = list
|
|
end
|
|
|
|
def self.dash_line?(cells)
|
|
cells.all? { |c| c.match?(/\A\s*-*\s*\z/) } && cells.any? { |c| c.include?('-') }
|
|
end
|
|
|
|
# COHERENT-era files are often Latin-1. Text that is not valid UTF-8 is
|
|
# read as Latin-1 and comes out as UTF-8.
|
|
def self.utf8(text)
|
|
return text if text.encoding == Encoding::UTF_8 && text.valid_encoding?
|
|
|
|
text.dup.force_encoding(Encoding::UTF_8).then do |t|
|
|
t.valid_encoding? ? t : text.dup.force_encoding(Encoding::ISO_8859_1).encode(Encoding::UTF_8)
|
|
end
|
|
end
|
|
|
|
# Do these lines (chomped) start with a head line and a dash line?
|
|
def self.headed?(lines)
|
|
lines.size >= 2 && dash_line?(lines[1].split("\t", -1))
|
|
end
|
|
|
|
def self.parse(text)
|
|
text = utf8(text)
|
|
return parse_list(text) if text.start_with?("\n")
|
|
|
|
lines = text.lines.map { |l| l.chomp("\n") }
|
|
# justify pads the names too ("Name "); they still name the column
|
|
head = (lines.shift || '').split("\t", -1).map(&:strip)
|
|
lines.shift if lines.first && dash_line?(lines.first.split("\t", -1))
|
|
rows = lines.map { |l| l.split("\t", -1) }
|
|
new(head, rows.map { |r| r.fill('', r.size...head.size) })
|
|
end
|
|
|
|
def self.parse_list(text)
|
|
head = []
|
|
records = []
|
|
rec = nil
|
|
text.sub(/\A\n/, '').each_line do |line|
|
|
line = line.chomp("\n")
|
|
if line.strip.empty?
|
|
rec = nil
|
|
next
|
|
end
|
|
name, value = line.split("\t", 2)
|
|
value ||= ''
|
|
head << name unless head.include?(name)
|
|
rec = nil if rec && name == head.first && rec.key?(name)
|
|
unless rec
|
|
rec = {}
|
|
records << rec
|
|
end
|
|
rec[name] = value
|
|
end
|
|
new(head, records.map { |r| head.map { |h| r.fetch(h, '') } }, list: true)
|
|
end
|
|
|
|
def self.read(io)
|
|
parse(io.read)
|
|
end
|
|
|
|
def self.open(path)
|
|
path == '-' ? read($stdin) : parse(File.read(path))
|
|
end
|
|
|
|
def index_of(name)
|
|
Rdb.column_index(@head, name) || Rdb.die("no column named '#{name}'")
|
|
end
|
|
|
|
def to_s
|
|
return list_text if @list
|
|
|
|
out = Table.head_text(@head)
|
|
@rows.each { |r| out << r.join("\t") << "\n" }
|
|
out
|
|
end
|
|
|
|
def self.dash_cells(head)
|
|
head.map { |h| '-' * [h.size, 1].max }
|
|
end
|
|
|
|
def self.dash_row(head)
|
|
dash_cells(head).join("\t")
|
|
end
|
|
|
|
# Head line and dash line, each with its newline.
|
|
def self.head_text(head)
|
|
+"#{head.join("\t")}\n#{dash_row(head)}\n"
|
|
end
|
|
|
|
def list_text
|
|
out = +"\n"
|
|
@rows.each do |r|
|
|
@head.each_with_index { |h, i| out << h << "\t" << r[i].to_s << "\n" }
|
|
out << "\n"
|
|
end
|
|
out
|
|
end
|
|
|
|
def write(out = $stdout)
|
|
out.write to_s
|
|
end
|
|
|
|
def with(head: @head, rows: @rows)
|
|
Table.new(head, rows, list: @list)
|
|
end
|
|
end
|
|
end
|