Files
rdb/lib/rdb/table.rb
T
wkh c2ea36c6d6 Schneller Start ohne RubyGems, Rational statt BigDecimal, Helfer in Rdb::Helpers
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.
2026-10-30 10:01:36 +01:00

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