343 lines
12 KiB
Ruby
343 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Selecting, combining and summarising tables.
|
|
|
|
Rdb.register('row', 'Make a new table where rows match logical condition',
|
|
"row 'column condition column-or-value ..' < table") do |args|
|
|
die "usage: row 'condition' < table" unless args.size == 1
|
|
t = table_in
|
|
prog = "BEGIN{OFS=\"\\t\"} #{to_awk(args[0], t.head)} {print}"
|
|
t.with(rows: awk(prog, t.rows)).write
|
|
end
|
|
Rdb.alias_for('select', 'row')
|
|
|
|
Rdb.register('column', 'Display columns of a table in any order', 'column [ Column ... ] < tableorlist') do |args|
|
|
t = table_in
|
|
next t.write if args.empty?
|
|
|
|
idx = args.map { |a| Rdb.column_index(t.head, a) }
|
|
head = args.each_with_index.map { |a, n| idx[n] ? t.head[idx[n]] : a }
|
|
t.with(head: head, rows: t.rows.map { |r| idx.map { |i| i ? r[i] : '' } }).write
|
|
end
|
|
Rdb.alias_for('project', 'column')
|
|
|
|
Rdb.register('sorttable', 'Sort a table by one or more columns',
|
|
'sorttable [-nrf] [columnname ...] < tableorlist') do |args|
|
|
t = table_in
|
|
numeric = reverse = fold = false
|
|
cols = []
|
|
args.each do |a|
|
|
if a.match?(/\A-[nrf]+\z/)
|
|
numeric ||= a.include?('n')
|
|
reverse ||= a.include?('r')
|
|
fold ||= a.include?('f')
|
|
else
|
|
cols << t.index_of(a)
|
|
end
|
|
end
|
|
cols = (0...t.head.size).to_a if cols.empty?
|
|
key = ->(r) { cols.map { |i| numeric ? r[i].to_f : (fold ? r[i].downcase : r[i]) } }
|
|
sorted = t.rows.each_with_index.sort_by { |r, n| [key.(r), n] }.map(&:first)
|
|
sorted.reverse! if reverse
|
|
t.with(rows: sorted).write
|
|
end
|
|
|
|
Rdb.register('compute', 'Calculate columns of a table', "compute 'column = expression [ ; ... ]' < tableorlist") do |args|
|
|
die "usage: compute 'column = expression [; ...]' < table" unless args.size == 1
|
|
t = table_in
|
|
prog = "BEGIN{OFS=\"\\t\"} { #{to_awk(args[0], t.head)}; print }"
|
|
t.with(rows: awk(prog, t.rows)).write
|
|
end
|
|
|
|
Rdb.register('addcol', 'Add a column to a table', 'addcol newcolumn < table') do |args|
|
|
die 'usage: addcol newcolumn ... < table' if args.empty?
|
|
t = table_in
|
|
t.with(head: t.head + args, rows: t.rows.map { |r| r + [''] * args.size }).write
|
|
end
|
|
|
|
Rdb.register('rename', 'Rename a column', 'rename [ oldcolumn newcolumn ... ] < tableorlist') do |args|
|
|
die 'usage: rename oldcolumn newcolumn ... < table' if args.empty? || args.size.odd?
|
|
t = table_in
|
|
head = t.head.dup
|
|
args.each_slice(2) { |old, new| head[t.index_of(old)] = new }
|
|
t.with(head: head).write
|
|
end
|
|
|
|
{ 'number' => 'number', 'Number' => 'Number' }.each do |cmd, title|
|
|
Rdb.register(cmd, 'Insert a column-row number into a table or list', "#{cmd} < tableorlist") do |_args|
|
|
t = table_in
|
|
t.with(head: [title] + t.head, rows: t.rows.each_with_index.map { |r, i| [(i + 1).to_s] + r }).write
|
|
end
|
|
end
|
|
|
|
Rdb.register('record', 'Find and output a record from a table', 'record number < table') do |args|
|
|
die 'usage: record number < table' unless args.size == 1
|
|
t = table_in
|
|
row = t.rows[args[0].to_i - 1]
|
|
t.with(rows: row ? [row] : []).write
|
|
end
|
|
|
|
# ---- combining tables ------------------------------------------------------
|
|
|
|
Rdb.register('jointable', 'Join two tables into one where keys match',
|
|
'jointable [-a[1|2]] [-j[1|2] column]... [-n] [-] table1 [table2]') do |args|
|
|
all1 = all2 = false
|
|
k1 = k2 = nil
|
|
files = []
|
|
until args.empty?
|
|
a = args.shift
|
|
case a
|
|
when '-a' then all1 = all2 = true
|
|
when '-a1' then all1 = true
|
|
when '-a2' then all2 = true
|
|
when '-j' then k1 = k2 = args.shift
|
|
when '-j1' then k1 = args.shift
|
|
when '-j2' then k2 = args.shift
|
|
when '-n' then nil
|
|
else files << a
|
|
end
|
|
end
|
|
die 'usage: jointable [-a[1|2]] [-j[1|2] column] table1 [table2]' if files.empty? || files.size > 2
|
|
|
|
files << '-' if files.size == 1
|
|
t1, t2 = files.map { |f| open_table(f) }
|
|
i1 = k1 ? t1.index_of(k1) : 0
|
|
i2 = k2 ? t2.index_of(k2) : 0
|
|
keep2 = (0...t2.head.size).to_a - [i2]
|
|
by_key = Hash.new { |h, k| h[k] = [] }
|
|
t2.rows.each { |r| by_key[r[i2]] << r }
|
|
used = {}
|
|
out = []
|
|
t1.rows.each do |r|
|
|
if (m = by_key[r[i1]]).any?
|
|
used[r[i1]] = true
|
|
m.each { |r2| out << r + keep2.map { |i| r2[i] } }
|
|
elsif all1
|
|
out << r + [''] * keep2.size
|
|
end
|
|
end
|
|
if all2
|
|
t2.rows.each do |r2|
|
|
next if used[r2[i2]]
|
|
|
|
blank = [''] * t1.head.size
|
|
blank[i1] = r2[i2]
|
|
out << blank + keep2.map { |i| r2[i] }
|
|
end
|
|
end
|
|
Rdb::Table.new(t1.head + keep2.map { |i| t2.head[i] }, out).write
|
|
end
|
|
|
|
Rdb.register('union', 'Concatenate tables', 'union tableorlist ... [-] [< tableorlist]') do |args|
|
|
args = ['-'] if args.empty?
|
|
tabs = args.map { |a| open_table(a) }
|
|
tabs.first.with(rows: tabs.flat_map(&:rows)).write
|
|
end
|
|
|
|
Rdb.register('intersect', 'Write table of rows that are in both input tables', 'intersect table1 table2') do |args|
|
|
die 'usage: intersect table1 table2' unless args.size == 2
|
|
a, b = args.map { |x| open_table(x) }
|
|
seen = b.rows.to_h { |r| [r, true] }
|
|
a.with(rows: a.rows.select { |r| seen[r] }.uniq).write
|
|
end
|
|
|
|
Rdb.register('difference', 'Output table of rows that are in only one table', 'difference table1 table2') do |args|
|
|
die 'usage: difference table1 table2' unless args.size == 2
|
|
a, b = args.map { |x| open_table(x) }
|
|
sb = b.rows.to_h { |r| [r, true] }
|
|
a.with(rows: a.rows.reject { |r| sb[r] }.uniq).write
|
|
end
|
|
|
|
Rdb.register('paste.rdb', 'Paste together two or more tables', 'paste.rdb table1 table2 ...') do |args|
|
|
die 'usage: paste.rdb table1 table2 ...' if args.size < 2
|
|
tabs = args.map { |a| open_table(a) }
|
|
n = tabs.map { |t| t.rows.size }.max
|
|
head = tabs.flat_map(&:head)
|
|
rows = (0...n).map { |i| tabs.flat_map { |t| t.rows[i] || [''] * t.head.size } }
|
|
Rdb::Table.new(head, rows).write
|
|
end
|
|
|
|
Rdb.register('splittable', 'Divide a table horizontally', 'splittable [-n] table') do |args|
|
|
n = 50
|
|
n = args.shift[1..].to_i if args.first&.match?(/\A-\d+\z/)
|
|
die 'usage: splittable [-n] table' unless args.size == 1
|
|
t = open_table(args[0])
|
|
suffix = ('aa'..'zz').each
|
|
t.rows.each_slice(n) do |chunk|
|
|
File.write("#{args[0]}#{suffix.next}", t.with(rows: chunk).to_s)
|
|
end
|
|
end
|
|
|
|
# ---- aggregates ------------------------------------------------------------
|
|
# total, mean, maximum, minimum, width, precision and datatype are one program
|
|
# that decides what to compute from the name it was called by.
|
|
|
|
STATS = {
|
|
'total' => 'Sum a column',
|
|
'mean' => 'Display the mean of a column',
|
|
'maximum' => 'Display the maximum value in a column',
|
|
'minimum' => 'Display the minimum of each column selected',
|
|
'width' => 'Display the width of each column',
|
|
'precision' => 'Display the precision of a column',
|
|
'datatype' => 'Display the data type of each column selected'
|
|
}.freeze
|
|
|
|
STATS.each do |cmd, desc|
|
|
Rdb.register(cmd, desc, "#{cmd} [ -l ] [ column ... ] < table") do |args|
|
|
list = args.first == '-l'
|
|
args.shift if list
|
|
t = table_in
|
|
sel = args.empty? ? (0...t.head.size).to_a : args.map { |a| t.index_of(a) }
|
|
result = Array.new(t.head.size, '')
|
|
sel.each do |i|
|
|
vals = t.rows.map { |r| r[i] }
|
|
filled = vals.reject { |v| v.strip.empty? }
|
|
nums = filled.all? { |v| numeric?(v) } ? vals.map { |v| v.strip.to_f } : nil
|
|
dec = filled.map { |v| decimals(v) }.max.to_i
|
|
result[i] =
|
|
case name
|
|
when 'width' then (vals.map(&:size) + [t.head[i].size]).max.to_s
|
|
when 'precision' then nums ? dec.to_s : ''
|
|
when 'datatype' then filled.empty? || !nums ? '-1' : dec.to_s
|
|
when 'total' then nums ? fmt(nums.sum, dec) : ''
|
|
when 'mean' then nums && !nums.empty? ? fmt(nums.sum / nums.size, [dec, 2].max) : ''
|
|
when 'maximum' then nums && !nums.empty? ? fmt(nums.max, dec) : ''
|
|
when 'minimum' then nums && !nums.empty? ? fmt(nums.min, dec) : ''
|
|
end
|
|
end
|
|
if list
|
|
$stdout.write t.to_s
|
|
puts Rdb::Table.dash_row(t.head)
|
|
puts result.join("\t")
|
|
else
|
|
Rdb::Table.new(t.head, [result]).write
|
|
end
|
|
end
|
|
end
|
|
|
|
Rdb.register('widest', 'Output the width of the widest entries in a table', 'widest < table') do |_args|
|
|
t = table_in
|
|
res = t.head.each_index.map { |i| (t.rows.map { |r| r[i].size } + [t.head[i].size]).max.to_s }
|
|
Rdb::Table.new(t.head, [res]).write
|
|
end
|
|
|
|
Rdb.register('subtotal', 'Output subtotals of columns in a table',
|
|
'subtotal [ -l ] [ break-column column ... ] < table') do |args|
|
|
list = args.first == '-l'
|
|
args.shift if list
|
|
t = table_in
|
|
brk = args.empty? ? 0 : t.index_of(args.shift)
|
|
sel = args.empty? ? (0...t.head.size).to_a - [brk] : args.map { |a| t.index_of(a) }
|
|
out = []
|
|
group = []
|
|
flush = lambda do
|
|
next if group.empty?
|
|
|
|
sub = Array.new(t.head.size, '')
|
|
sub[brk] = group.first[brk]
|
|
sel.each do |i|
|
|
vals = group.map { |r| r[i] }.reject { |v| v.strip.empty? }
|
|
next if vals.empty? || !vals.all? { |v| numeric?(v) }
|
|
|
|
sub[i] = fmt(vals.sum { |v| v.strip.to_f }, vals.map { |v| decimals(v) }.max)
|
|
end
|
|
if list
|
|
out.concat(group)
|
|
out << t.head.map { |h| '-' * [h.size, 1].max }
|
|
end
|
|
out << sub
|
|
group = []
|
|
end
|
|
t.rows.each do |r|
|
|
flush.() if group.any? && group.first[brk] != r[brk]
|
|
group << r
|
|
end
|
|
flush.()
|
|
t.with(rows: out).write
|
|
end
|
|
|
|
# ---- describing tables ------------------------------------------------------
|
|
|
|
Rdb.register('schema', "Print a table's schema", 'schema tableorlist ...') do |args|
|
|
die 'usage: schema tableorlist ...' if args.empty?
|
|
rows = args.flat_map do |f|
|
|
open_table(f).head.each_with_index.map { |h, i| [File.basename(f), (i + 1).to_s, h] }
|
|
end
|
|
Rdb::Table.new(%w[Table Field Name], rows).write
|
|
end
|
|
|
|
Rdb.register('dbdict', 'Print a data-base dictionary', 'dbdict < table') do |_args|
|
|
t = table_in
|
|
Rdb::Table.new(%w[field name], t.head.each_with_index.map { |h, i| [(i + 1).to_s, h] }).write
|
|
end
|
|
|
|
Rdb.register('tableorlist', 'Report whether a file has table or list format',
|
|
'tableorlist [tableorlist ...] [< tableorlist]') do |args|
|
|
texts = args.empty? ? [$stdin.read] : args.map { |a| File.read(a) }
|
|
status = 0
|
|
texts.each do |tx|
|
|
if tx.start_with?("\n")
|
|
puts 'list'
|
|
status = 1
|
|
else
|
|
puts 'table'
|
|
end
|
|
end
|
|
exit status
|
|
end
|
|
|
|
Rdb.register('check.rdb', 'Report any rows in which columns do not match head line', 'check.rdb < tableorlist') do |_args|
|
|
lines = $stdin.read.lines.map { |l| l.chomp("\n") }
|
|
expected = lines.first.to_s.count("\t") + 1
|
|
bad = 0
|
|
lines.each_with_index do |l, n|
|
|
next if n.zero? || l.count("\t") + 1 == expected
|
|
|
|
bad += 1
|
|
warn "check.rdb: Columns (#{l.count("\t") + 1}) do not equal headline columns (#{expected}) in row #{n - 1} (line #{n + 1})."
|
|
warn l
|
|
end
|
|
exit(bad.zero? ? 0 : 1)
|
|
end
|
|
|
|
# ---- head and dash lines -------------------------------------------------
|
|
|
|
Rdb.register('headon', 'Add an /rdb header to a table', 'headon [column ...] < table') do |args|
|
|
rows = $stdin.each_line.map { |l| l.chomp("\n").split("\t", -1) }
|
|
n = rows.map(&:size).max || 0
|
|
head = args.dup
|
|
head << (head.size + 1).to_s while head.size < n
|
|
Rdb::Table.new(head, rows.map { |r| r.fill('', r.size...head.size) }).write
|
|
end
|
|
|
|
Rdb.register('headoff', 'Remove an /rdb head from both table and list file', 'headoff < tableorlist') do |_args|
|
|
text = $stdin.read
|
|
lines = text.lines
|
|
$stdout.write lines.drop(text.start_with?("\n") ? 1 : 2).join
|
|
end
|
|
|
|
Rdb.register('insertdash', 'Insert dash line as second line in table', 'insertdash < table-without-dashline') do |_args|
|
|
lines = $stdin.read.lines
|
|
head = lines.shift.to_s.chomp("\n")
|
|
puts head
|
|
puts head.gsub(/[^\t]/, '-')
|
|
$stdout.write lines.join
|
|
end
|
|
|
|
Rdb.register('display', 'Write table or list file to standard output', 'display < tableorlist') do |_args|
|
|
$stdout.write $stdin.read
|
|
end
|
|
|
|
Rdb.register('see', 'Display nonprinting as well as printing characters', 'see < tableorlist') do |_args|
|
|
$stdin.read.each_char do |c|
|
|
o = c.ord
|
|
$stdout.write(
|
|
if c == "\n" then "$\n"
|
|
elsif o < 32 then "^#{(o + 64).chr}"
|
|
elsif o == 127 then '^?'
|
|
else c
|
|
end
|
|
)
|
|
end
|
|
end
|