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.
This commit is contained in:
+19
-1
@@ -3,7 +3,6 @@
|
||||
# A Ruby re-creation of /rdb, the relational database tools Mark Williams
|
||||
# Company sold for COHERENT (~1991).
|
||||
|
||||
require 'bigdecimal'
|
||||
require 'open3'
|
||||
require 'date'
|
||||
require 'tmpdir'
|
||||
@@ -33,6 +32,25 @@ module Rdb
|
||||
hits.first
|
||||
end
|
||||
|
||||
# Exact decimal arithmetic with Rational (core Ruby, so bin/rdb can start
|
||||
# without RubyGems): 0.1 + 0.2 is 0.3 and money adds up to the cent.
|
||||
def decimal(str)
|
||||
s = str.to_s.strip
|
||||
return 0r if s.empty?
|
||||
|
||||
Rational(s.match?(NUM) ? s.sub(/\.\z/, '') : s.to_f.to_s)
|
||||
end
|
||||
|
||||
# num with dec places, rounded half up ("%.2f" rounds the binary value).
|
||||
def fixed(num, dec)
|
||||
r = num.is_a?(Float) ? Rational(num.to_s) : num.to_r
|
||||
r = r.round(dec, half: :up)
|
||||
return r.to_i.to_s if dec.zero?
|
||||
|
||||
digits = (r.abs * 10**dec).to_i.to_s.rjust(dec + 1, '0')
|
||||
"#{'-' if r.negative?}#{digits[0...-dec]}.#{digits[-dec..]}"
|
||||
end
|
||||
|
||||
def die(msg)
|
||||
abort "#{File.basename($PROGRAM_NAME)}: #{msg}"
|
||||
end
|
||||
|
||||
+120
-101
@@ -6,54 +6,57 @@
|
||||
require 'stringio'
|
||||
require_relative '../fast'
|
||||
|
||||
# Parse the common fast-access options. Stops at the first non-option, which
|
||||
# is left in args; returns the option hash.
|
||||
def fast_opts(args)
|
||||
o = { m: 's', h: false, l: false, s: false, x: false, exact: false, a: false, v: false, o: nil }
|
||||
until args.empty?
|
||||
a = args.first
|
||||
case a
|
||||
when /\A-m([bhirs]?)\z/ then o[:m] = Regexp.last_match(1).empty? ? 's' : Regexp.last_match(1)
|
||||
when '+x' then o[:exact] = true
|
||||
when '-o' then args.shift; o[:o] = args.first
|
||||
when /\A-[hslxav]+\z/ then a[1..].each_char { |c| o[c.to_sym] = true }
|
||||
else break
|
||||
module Rdb
|
||||
module Helpers
|
||||
# Parse the common fast-access options. Stops at the first non-option, which
|
||||
# is left in args; returns the option hash.
|
||||
def fast_opts(args)
|
||||
o = { m: 's', h: false, l: false, s: false, x: false, exact: false, a: false, v: false, o: nil }
|
||||
until args.empty?
|
||||
a = args.first
|
||||
case a
|
||||
when /\A-m([bhirs]?)\z/ then o[:m] = Regexp.last_match(1).empty? ? 's' : Regexp.last_match(1)
|
||||
when '+x' then o[:exact] = true
|
||||
when '-o' then args.shift; o[:o] = args.first
|
||||
when /\A-[hslxav]+\z/ then a[1..].each_char { |c| o[c.to_sym] = true }
|
||||
else break
|
||||
end
|
||||
args.shift
|
||||
end
|
||||
o
|
||||
end
|
||||
args.shift
|
||||
end
|
||||
o
|
||||
end
|
||||
|
||||
def need_index(table, method)
|
||||
suffix = Rdb::Fast::SUFFIX[method]
|
||||
return unless suffix && !File.exist?(table + suffix)
|
||||
def need_index(table, method)
|
||||
suffix = Rdb::Fast::SUFFIX[method]
|
||||
return unless suffix && !File.exist?(table + suffix)
|
||||
|
||||
Rdb.die("no index #{table}#{suffix}: run index -m#{method} #{table}")
|
||||
end
|
||||
Rdb.die("no index #{table}#{suffix}: run index -m#{method} #{table}")
|
||||
end
|
||||
|
||||
# The key columns and, unless keys come from the terminal, the keys. Without key
|
||||
# columns on the command line a key table names them in its head line
|
||||
# (handbook p. 105: "search -mh mailtable < name"); otherwise the first column.
|
||||
def key_columns(args, data)
|
||||
return [args.empty? ? [data.head[0]] : args, nil] if $stdin.tty?
|
||||
# The key columns and, unless keys come from the terminal, the keys. Without key
|
||||
# columns on the command line a key table names them in its head line
|
||||
# (handbook p. 105: "search -mh mailtable < name"); otherwise the first column.
|
||||
def key_columns(args, data)
|
||||
return [args.empty? ? [data.head[0]] : args, nil] if $stdin.tty?
|
||||
|
||||
text = $stdin.read.to_s
|
||||
lines = text.lines.map { |l| l.chomp("\n") }
|
||||
cols = args
|
||||
if cols.empty?
|
||||
keyed = lines.size >= 2 && Rdb::Table.dash_line?(lines[1].split("\t", -1))
|
||||
cols = keyed ? lines[0].split("\t", -1).map(&:strip) : [data.head[0]]
|
||||
end
|
||||
[cols, Rdb::Fast.read_keys(StringIO.new(text), cols.size)]
|
||||
end
|
||||
text = $stdin.read.to_s
|
||||
lines = text.lines.map { |l| l.chomp("\n") }
|
||||
cols = args
|
||||
if cols.empty?
|
||||
cols = Rdb::Table.headed?(lines) ? lines[0].split("\t", -1).map(&:strip) : [data.head[0]]
|
||||
end
|
||||
[cols, Rdb::Fast.read_keys(StringIO.new(text), cols.size)]
|
||||
end
|
||||
|
||||
def each_key(ncols, keys = nil, &block)
|
||||
if keys
|
||||
keys.each(&block)
|
||||
elsif $stdin.tty?
|
||||
$stdin.each_line { |l| yield l.chomp.split("\t", -1).first(ncols) }
|
||||
else
|
||||
Rdb::Fast.read_keys($stdin, ncols).each { |k| yield k }
|
||||
def each_key(ncols, keys = nil, &block)
|
||||
if keys
|
||||
keys.each(&block)
|
||||
elsif $stdin.tty?
|
||||
$stdin.each_line { |l| yield l.chomp.split("\t", -1).first(ncols) }
|
||||
else
|
||||
Rdb::Fast.read_keys($stdin, ncols).each { |k| yield k }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -76,7 +79,7 @@ Rdb.register('search', 'Search a table',
|
||||
data = Rdb::Fast::Data.new(table)
|
||||
need_index(table, o[:m])
|
||||
keycols, keys = key_columns(args, data)
|
||||
puts data.head.join("\t"), Rdb::Table.dash_row(data.head) unless o[:h]
|
||||
$stdout.write Rdb::Table.head_text(data.head) unless o[:h]
|
||||
each_key(keycols.size, keys) do |key|
|
||||
hits = Rdb::Fast.search(data, o[:m], keycols, key, first: o[:s] || o[:l], partial: o[:x], exact: o[:exact])
|
||||
hits.each do |h|
|
||||
@@ -97,7 +100,7 @@ Rdb.register('seek', 'Return the beginning and ending offset of a row', 'seek [-
|
||||
Rdb::Fast.search(data, o[:m], keycols, key, first: true).each do |h|
|
||||
puts "#{h.row.from} #{h.row.to} #{h.xfrom} #{h.xto}"
|
||||
if o[:o]
|
||||
File.write(o[:o], data.head.join("\t") + "\n" + Rdb::Table.dash_row(data.head) + "\n" + h.row.line + "\n")
|
||||
File.write(o[:o], Rdb::Table.head_text(data.head) + h.row.line + "\n")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -137,7 +140,7 @@ Rdb.register('replace', 'Insert a record into a file at specified location',
|
||||
|
||||
text = $stdin.read
|
||||
lines = text.lines.map { |l| l.chomp("\n") }
|
||||
lines = lines.drop(2) if lines.size >= 2 && Rdb::Table.dash_line?(lines[1].split("\t", -1))
|
||||
lines = lines.drop(2) if Rdb::Table.headed?(lines)
|
||||
die 'replace: no input row' if lines.empty?
|
||||
|
||||
keycol = Rdb::Fast::Data.new(table).head[0]
|
||||
@@ -189,33 +192,41 @@ require 'shellwords'
|
||||
|
||||
# ---- locks -------------------------------------------------------------------
|
||||
|
||||
# Lock files live in a private per-user directory, not loose in the shared
|
||||
# /tmp, so other users can neither forge nor delete them.
|
||||
def lock_dir
|
||||
dir = File.join(Dir.tmpdir, "rdb-locks-#{Process.uid}")
|
||||
Dir.mkdir(dir, 0o700) unless File.directory?(dir)
|
||||
st = File.lstat(dir)
|
||||
Rdb.die("#{dir}: not a private directory of yours") if st.symlink? || st.uid != Process.uid || (st.mode & 0o077) != 0
|
||||
dir
|
||||
end
|
||||
module Rdb
|
||||
module Helpers
|
||||
# Lock files live in a private per-user directory, not loose in the shared
|
||||
# /tmp, so other users can neither forge nor delete them.
|
||||
def lock_dir
|
||||
dir = File.join(Dir.tmpdir, "rdb-locks-#{Process.uid}")
|
||||
begin
|
||||
Dir.mkdir(dir, 0o700)
|
||||
rescue Errno::EEXIST
|
||||
nil # made by someone else, maybe at this very moment; checked below
|
||||
end
|
||||
st = File.lstat(dir)
|
||||
Rdb.die("#{dir}: not a private directory of yours") if st.symlink? || st.uid != Process.uid || (st.mode & 0o077) != 0
|
||||
dir
|
||||
end
|
||||
|
||||
def lock_file(name)
|
||||
File.join(lock_dir, "L#{File.basename(name)}.#{Digest::MD5.hexdigest(File.expand_path(name))[0, 8]}")
|
||||
end
|
||||
def lock_file(name)
|
||||
File.join(lock_dir, "L#{File.basename(name)}.#{Digest::MD5.hexdigest(File.expand_path(name))[0, 8]}")
|
||||
end
|
||||
|
||||
# Read-modify-write of a lock file under an exclusive flock, so two lock or
|
||||
# unlock calls cannot interleave. The block gets the held locks and returns
|
||||
# the new list of lines, or nil to leave the file alone.
|
||||
def with_lock_file(name)
|
||||
File.open(lock_file(name), File::RDWR | File::CREAT, 0o600) do |f|
|
||||
f.flock(File::LOCK_EX)
|
||||
held = f.read.lines(chomp: true).map { |l| l.split.map(&:to_i) }
|
||||
lines = yield held
|
||||
next unless lines
|
||||
# Read-modify-write of a lock file under an exclusive flock, so two lock or
|
||||
# unlock calls cannot interleave. The block gets the held locks and returns
|
||||
# the new list of lines, or nil to leave the file alone.
|
||||
def with_lock_file(name)
|
||||
File.open(lock_file(name), File::RDWR | File::CREAT, 0o600) do |f|
|
||||
f.flock(File::LOCK_EX)
|
||||
held = f.read.lines(chomp: true).map { |l| l.split.map(&:to_i) }
|
||||
lines = yield held
|
||||
next unless lines
|
||||
|
||||
f.rewind
|
||||
f.truncate(0)
|
||||
f.write(lines.map { |l| "#{l}\n" }.join)
|
||||
f.rewind
|
||||
f.truncate(0)
|
||||
f.write(lines.map { |l| "#{l}\n" }.join)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -267,20 +278,24 @@ end
|
||||
|
||||
# ---- interactive editing --------------------------------------------------------
|
||||
|
||||
def show_record(data_head, cells)
|
||||
data_head.each_with_index { |h, i| puts "#{i + 1} #{h}\t#{cells[i].to_s.strip}" }
|
||||
end
|
||||
module Rdb
|
||||
module Helpers
|
||||
def show_record(data_head, cells)
|
||||
data_head.each_with_index { |h, i| puts "#{i + 1} #{h}\t#{cells[i].to_s.strip}" }
|
||||
end
|
||||
|
||||
def edit_cells(head, cells)
|
||||
require 'tempfile'
|
||||
Tempfile.create('rdb-edit') do |f|
|
||||
f.write Rdb::Table.new(head, [cells], list: true).to_s
|
||||
f.flush
|
||||
system(*Shellwords.split(ENV['EDITOR'] || 'vi'), f.path) or Rdb.die("editor #{ENV['EDITOR'] || 'vi'} failed")
|
||||
t = Rdb::Table.parse(File.read(f.path))
|
||||
return nil if t.rows.empty?
|
||||
def edit_cells(head, cells)
|
||||
require 'tempfile'
|
||||
Tempfile.create('rdb-edit') do |f|
|
||||
f.write Rdb::Table.new(head, [cells], list: true).to_s
|
||||
f.flush
|
||||
system(*Shellwords.split(ENV['EDITOR'] || 'vi'), f.path) or Rdb.die("editor #{ENV['EDITOR'] || 'vi'} failed")
|
||||
t = Rdb::Table.parse(File.read(f.path))
|
||||
return nil if t.rows.empty?
|
||||
|
||||
head.map { |h| (j = t.head.index(h)) ? t.rows[0][j] : '' }
|
||||
head.map { |h| (j = t.head.index(h)) ? t.rows[0][j] : '' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -443,7 +458,7 @@ end
|
||||
Rdb.register('validate', 'Find invalid data', "validate 'pattern { action } ...' < tableorlist") do |args|
|
||||
die "usage: validate 'pattern { action } ...' < table" unless args.size == 1
|
||||
t = table_in
|
||||
puts t.head.join("\t"), Rdb::Table.dash_row(t.head)
|
||||
$stdout.write Rdb::Table.head_text(t.head)
|
||||
prog = "BEGIN{OFS=\"\\t\"} #{to_awk(args[0], t.head)} {print}"
|
||||
out, ok = awk_text(prog, t.rows)
|
||||
$stdout.write out
|
||||
@@ -512,27 +527,31 @@ end
|
||||
|
||||
# ---- exercising the fast-access methods ---------------------------------------------
|
||||
|
||||
def sample_table(path, n = 2000)
|
||||
rows = (1..n).map { |i| [i.to_s, "name#{i * 7919 % n}", "city#{i % 97}"] }
|
||||
File.write(path, Rdb::Table.new(%w[Id Name City], rows).to_s)
|
||||
end
|
||||
module Rdb
|
||||
module Helpers
|
||||
def sample_table(path, n = 2000)
|
||||
rows = (1..n).map { |i| [i.to_s, "name#{i * 7919 % n}", "city#{i % 97}"] }
|
||||
File.write(path, Rdb::Table.new(%w[Id Name City], rows).to_s)
|
||||
end
|
||||
|
||||
def time_methods(n = 2000)
|
||||
Dir.mktmpdir do |dir|
|
||||
path = File.join(dir, 'unixtable')
|
||||
sample_table(path, n)
|
||||
%w[s b h i r].each do |m|
|
||||
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
Rdb::Fast.build(path, m, 'Name') unless m == 's'
|
||||
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
data = Rdb::Fast::Data.new(path)
|
||||
hits = 0
|
||||
(1..200).each do |i|
|
||||
key = m == 'r' ? i.to_s : "name#{i * 13 % n}"
|
||||
hits += Rdb::Fast.search(data, m, ['Name'], key).size
|
||||
def time_methods(n = 2000)
|
||||
Dir.mktmpdir do |dir|
|
||||
path = File.join(dir, 'unixtable')
|
||||
sample_table(path, n)
|
||||
%w[s b h i r].each do |m|
|
||||
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
Rdb::Fast.build(path, m, 'Name') unless m == 's'
|
||||
t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
data = Rdb::Fast::Data.new(path)
|
||||
hits = 0
|
||||
(1..200).each do |i|
|
||||
key = m == 'r' ? i.to_s : "name#{i * 13 % n}"
|
||||
hits += Rdb::Fast.search(data, m, ['Name'], key).size
|
||||
end
|
||||
t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
yield m, t1 - t0, t2 - t1, hits
|
||||
end
|
||||
end
|
||||
t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
yield m, t1 - t0, t2 - t1, hits
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+61
-54
@@ -5,43 +5,42 @@
|
||||
# an accounting package: each command reads and writes plain /rdb tables in
|
||||
# the current directory.
|
||||
|
||||
ACT_COMMANDS = %w[act adjust balance bom calcpay calculate cashflow chartdup close consolidate cstate explode
|
||||
Rdb::ACT_COMMANDS = %w[act adjust balance bom calcpay calculate cashflow chartdup close consolidate cstate explode
|
||||
fillform foot getjournal invoice sale tax update.inv w2].freeze
|
||||
|
||||
# Amounts are decimal numbers, not floats: sums of cents stay exact.
|
||||
def num(str)
|
||||
s = str.to_s.strip
|
||||
s.match?(Rdb::NUM) ? BigDecimal(s.sub(/\.\z/, '')) : BigDecimal(s.to_f.to_s)
|
||||
end
|
||||
module Rdb
|
||||
module Helpers
|
||||
# Amounts are exact decimals (Rational), not floats: sums of cents stay exact.
|
||||
def num(str)
|
||||
Rdb.decimal(str)
|
||||
end
|
||||
|
||||
# Two decimals, always (money drops them from whole amounts).
|
||||
def fixed2(val)
|
||||
val = BigDecimal(val.to_s) unless val.is_a?(BigDecimal)
|
||||
val.round(2, :half_up).to_s('F').then { |t| t.sub(/\.(\d*)\z/) { ".#{Regexp.last_match(1).ljust(2, '0')}" } }
|
||||
end
|
||||
# Two decimals, always (money drops them from whole amounts).
|
||||
def fixed2(val)
|
||||
Rdb.fixed(val, 2)
|
||||
end
|
||||
|
||||
def money(val)
|
||||
val = BigDecimal(val.to_s) unless val.is_a?(BigDecimal)
|
||||
return val.to_i.to_s if val == val.to_i
|
||||
def money(val)
|
||||
val == val.to_i ? val.to_i.to_s : Rdb.fixed(val, 2)
|
||||
end
|
||||
|
||||
val.round(2, :half_up).to_s('F').sub(/\.(\d)\z/, '.\10')
|
||||
end
|
||||
def need(file)
|
||||
File.exist?(file) || Rdb.die("#{file}: no such table (the /act commands work on tables in the current directory)")
|
||||
Rdb::Table.open(file)
|
||||
end
|
||||
|
||||
def need(file)
|
||||
File.exist?(file) || Rdb.die("#{file}: no such table (the /act commands work on tables in the current directory)")
|
||||
Rdb::Table.open(file)
|
||||
end
|
||||
def next_date
|
||||
ENV['NEXTDATE'] || Date.today.strftime('%y%m%d')
|
||||
end
|
||||
|
||||
def next_date
|
||||
ENV['NEXTDATE'] || Date.today.strftime('%y%m%d')
|
||||
end
|
||||
|
||||
def account_key(acct)
|
||||
[acct.to_f, acct]
|
||||
def account_key(acct)
|
||||
[acct.to_f, acct]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Rdb.register('act', 'List all /act commands', 'act') do |_args|
|
||||
names = ACT_COMMANDS.sort
|
||||
names = Rdb::ACT_COMMANDS.sort
|
||||
w = names.map(&:size).max + 2
|
||||
names.each_slice(5) { |row| puts row.map { |n| n.ljust(w) }.join.rstrip }
|
||||
end
|
||||
@@ -66,7 +65,7 @@ Rdb.register('foot', 'Foot or subtotal Debits and Credit of ledger', 'foot') do
|
||||
out = []
|
||||
t.rows.sort_by { |r| account_key(r[0]) }.group_by { |r| r[0] }.each_value do |rows|
|
||||
out.concat(rows)
|
||||
out << t.head.map { |h| '-' * [h.size, 1].max }
|
||||
out << Rdb::Table.dash_cells(t.head)
|
||||
sub = [''] * t.head.size
|
||||
sub[0] = rows.first[0]
|
||||
sub[d] = money(rows.sum { |r| num(r[d]) })
|
||||
@@ -84,7 +83,7 @@ Rdb.register('adjust', 'Create adjusted trial balance table', 'adjust') do |_arg
|
||||
m = (meta[h['Account']] ||= {})
|
||||
h.each { |k, v| m[k] = v if !v.to_s.strip.empty? && !%w[Debit Credit].include?(k) && !m.key?(k) }
|
||||
end
|
||||
net = Hash.new(BigDecimal(0))
|
||||
net = Hash.new(0r)
|
||||
[tb, ja].each do |t|
|
||||
d = t.index_of('Debit')
|
||||
c = t.index_of('Credit')
|
||||
@@ -104,11 +103,15 @@ Rdb.register('adjust', 'Create adjusted trial balance table', 'adjust') do |_arg
|
||||
File.write('adjustedtrial', Rdb::Table.new(head, rows).to_s)
|
||||
end
|
||||
|
||||
def trial_parts(t)
|
||||
h = t.head
|
||||
fmt = h.index('Format')
|
||||
rows = t.rows.map { |r| { acct: r[0], debit: num(r[h.index('Debit')]), credit: num(r[h.index('Credit')]), fmt: r[fmt].to_s, name: r[h.index('Name')].to_s, row: r } }
|
||||
rows
|
||||
module Rdb
|
||||
module Helpers
|
||||
def trial_parts(t)
|
||||
h = t.head
|
||||
fmt = h.index('Format')
|
||||
rows = t.rows.map { |r| { acct: r[0], debit: num(r[h.index('Debit')]), credit: num(r[h.index('Credit')]), fmt: r[fmt].to_s, name: r[h.index('Name')].to_s, row: r } }
|
||||
rows
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Rdb.register('balance', 'Create balance sheet from adjusted trial balance', 'balance') do |_args|
|
||||
@@ -183,7 +186,7 @@ Rdb.register('fillform', 'Fill a tax from with adjusted trial balance data', 'fi
|
||||
lines = need("taxform.#{args[0]}")
|
||||
trial = need('adjustedtrial')
|
||||
ti = trial.index_of('Taxline')
|
||||
sums = Hash.new(BigDecimal(0))
|
||||
sums = Hash.new(0r)
|
||||
trial.rows.each { |r| sums[r[ti]] += (num(r[trial.index_of('Debit')]) - num(r[trial.index_of('Credit')])).abs }
|
||||
rows = lines.rows.map { |r| [r[0], money(sums[r[0]]), r[1].to_s] }
|
||||
File.write(args[0], Rdb::Table.new(%w[Taxline Amount Description], rows).to_s)
|
||||
@@ -205,14 +208,14 @@ Rdb.register('tax', 'Compute tax from income and tax table', 'tax income < taxta
|
||||
income = num(args[0])
|
||||
rows = table_in.rows.map { |r| r.map { |c| num(c) } }.sort_by(&:first)
|
||||
row = rows.reverse.find { |r| r[0] <= income }
|
||||
puts format('%7d', row ? (row[1] + (income - row[0]) * row[2] / 100.0).round : 0)
|
||||
puts format('%7d', row ? (row[1] + (income - row[0]) * row[2] / 100).round(half: :up) : 0)
|
||||
end
|
||||
|
||||
Rdb.register('cashflow', 'Compute balance column of cash table', 'cashflow < cashtable') do |_args|
|
||||
t = table_in
|
||||
a = t.index_of('Amount')
|
||||
b = t.index_of('Balance')
|
||||
bal = BigDecimal(0)
|
||||
bal = 0r
|
||||
rows = t.rows.map do |r|
|
||||
r = r.dup
|
||||
if r[a].strip.empty? && !r[b].strip.empty?
|
||||
@@ -250,9 +253,9 @@ end
|
||||
Rdb.register('bom', 'Produce bill-of-materials from parts list', 'bom') do |_args|
|
||||
part = need('part')
|
||||
items = need('saleitem')
|
||||
qty = Hash.new(BigDecimal(0))
|
||||
qty = Hash.new(0r)
|
||||
items.rows.each { |r| qty[r[items.index_of('Code')]] += num(r[items.index_of('Qty')]) }
|
||||
need_total = Hash.new(BigDecimal(0))
|
||||
need_total = Hash.new(0r)
|
||||
part.rows.each do |r|
|
||||
code = r[part.index_of('Code')]
|
||||
need_total[r[part.index_of('Subpart')]] += qty[code] * num(r[part.index_of('Count')]) if qty.key?(code)
|
||||
@@ -263,7 +266,7 @@ end
|
||||
Rdb.register('explode', 'Produce table of subparts and their count for a part', 'explode Part Amount [ table ]') do |args|
|
||||
die 'usage: explode Part Amount [table]' if args.size < 2
|
||||
t = need(args[2] || 'parttable')
|
||||
totals = Hash.new(BigDecimal(0))
|
||||
totals = Hash.new(0r)
|
||||
queue = [[args[0], num(args[1])]]
|
||||
until queue.empty?
|
||||
part, amount = queue.shift
|
||||
@@ -275,20 +278,24 @@ end
|
||||
|
||||
# ---- sales ------------------------------------------------------------------------------
|
||||
|
||||
def ask(prompt)
|
||||
print prompt
|
||||
$stdout.flush
|
||||
($stdin.gets || '').strip
|
||||
end
|
||||
module Rdb
|
||||
module Helpers
|
||||
def ask(prompt)
|
||||
print prompt
|
||||
$stdout.flush
|
||||
($stdin.gets || '').strip
|
||||
end
|
||||
|
||||
def address_block(customers, number)
|
||||
c = customers.rows.find { |r| r[0] == number }
|
||||
return ["Customer Number #{number}"] unless c
|
||||
def address_block(customers, number)
|
||||
c = customers.rows.find { |r| r[0] == number }
|
||||
return ["Customer Number #{number}"] unless c
|
||||
|
||||
h = customers.head.zip(c).to_h
|
||||
["Mailing Address for Customer Number #{number}",
|
||||
[h['Title'], h['First'], h['Last'], h['Position']].compact.join(' ').strip,
|
||||
h['Company'], h['Street'], [h['City'], h['State'], h['Country']].compact.join(' ')].compact.reject(&:empty?)
|
||||
h = customers.head.zip(c).to_h
|
||||
["Mailing Address for Customer Number #{number}",
|
||||
[h['Title'], h['First'], h['Last'], h['Position']].compact.join(' ').strip,
|
||||
h['Company'], h['Street'], [h['City'], h['State'], h['Country']].compact.join(' ')].compact.reject(&:empty?)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Rdb.register('invoice', 'Print invoice for a sale order', 'invoice') do |_args|
|
||||
@@ -359,7 +366,7 @@ Rdb.register('sale', 'Enter sale order, and item, update customer', 'sale') do |
|
||||
end
|
||||
puts address_block(customers, cust)
|
||||
n = Rdb::Table.open('salesorder').rows.map { |r| r[0].to_i }.max.to_i + 1
|
||||
gross = BigDecimal(0)
|
||||
gross = 0r
|
||||
items = []
|
||||
loop do
|
||||
code = ask('Code (Return when done): ')
|
||||
@@ -375,7 +382,7 @@ Rdb.register('sale', 'Enter sale order, and item, update customer', 'sale') do |
|
||||
end
|
||||
next if items.empty?
|
||||
|
||||
tax = gross * BigDecimal('0.05')
|
||||
tax = gross * Rational('0.05')
|
||||
order = [n.to_s, cust, next_date, '0', fixed2(gross), fixed2(tax), fixed2(gross + tax)]
|
||||
puts Rdb::Table.new(%w[Number Cust Date Shipped Gross Tax Total], [order]).to_s
|
||||
if ask('Is this correct? (y, n) ').downcase.start_with?('y')
|
||||
|
||||
+17
-13
@@ -105,21 +105,25 @@ end
|
||||
|
||||
# ---- names that sound alike ----------------------------------------------------
|
||||
|
||||
def soundex_code(word)
|
||||
codes = { 'BFPV' => '1', 'CGJKQSXZ' => '2', 'DT' => '3', 'L' => '4', 'MN' => '5', 'R' => '6' }
|
||||
letters = word.upcase.gsub(/[^A-Z]/, '').chars
|
||||
return '' if letters.empty?
|
||||
module Rdb
|
||||
module Helpers
|
||||
def soundex_code(word)
|
||||
codes = { 'BFPV' => '1', 'CGJKQSXZ' => '2', 'DT' => '3', 'L' => '4', 'MN' => '5', 'R' => '6' }
|
||||
letters = word.upcase.gsub(/[^A-Z]/, '').chars
|
||||
return '' if letters.empty?
|
||||
|
||||
code = ->(c) { codes.find { |k, _| k.include?(c) }&.last }
|
||||
out = letters[0].dup
|
||||
last = code.(letters[0])
|
||||
letters.drop(1).each do |c|
|
||||
d = code.(c)
|
||||
out << d if d && d != last
|
||||
last = d unless 'HW'.include?(c)
|
||||
break if out.size == 4
|
||||
code = ->(c) { codes.find { |k, _| k.include?(c) }&.last }
|
||||
out = letters[0].dup
|
||||
last = code.(letters[0])
|
||||
letters.drop(1).each do |c|
|
||||
d = code.(c)
|
||||
out << d if d && d != last
|
||||
last = d unless 'HW'.include?(c)
|
||||
break if out.size == 4
|
||||
end
|
||||
out.ljust(4, '0')
|
||||
end
|
||||
end
|
||||
out.ljust(4, '0')
|
||||
end
|
||||
|
||||
Rdb.register('soundex', 'Convert a name into soundex code', 'soundex < file > file.x ; index -mb file.x Soundex') do |_args|
|
||||
|
||||
@@ -189,7 +189,7 @@ end
|
||||
# total, mean, maximum, minimum, width, precision and datatype are one program
|
||||
# that decides what to compute from the name it was called by.
|
||||
|
||||
STATS = {
|
||||
Rdb::STATS = {
|
||||
'total' => 'Sum a column',
|
||||
'mean' => 'Display the mean of a column',
|
||||
'maximum' => 'Display the maximum value in a column',
|
||||
@@ -199,7 +199,7 @@ STATS = {
|
||||
'datatype' => 'Display the data type of each column selected'
|
||||
}.freeze
|
||||
|
||||
STATS.each do |cmd, desc|
|
||||
Rdb::STATS.each do |cmd, desc|
|
||||
Rdb.register(cmd, desc, "#{cmd} [ -l ] [ column ... ] < table") do |args|
|
||||
list = args.first == '-l'
|
||||
args.shift if list
|
||||
@@ -209,7 +209,7 @@ STATS.each do |cmd, desc|
|
||||
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.empty? ? BigDecimal(0) : dec_num(v) } : nil
|
||||
nums = filled.all? { |v| numeric?(v) } ? vals.map { |v| dec_num(v) } : nil
|
||||
dec = filled.map { |v| decimals(v) }.max.to_i
|
||||
result[i] =
|
||||
case name
|
||||
@@ -260,7 +260,7 @@ Rdb.register('subtotal', 'Output subtotals of columns in a table',
|
||||
end
|
||||
if list
|
||||
out.concat(group)
|
||||
out << t.head.map { |h| '-' * [h.size, 1].max }
|
||||
out << Rdb::Table.dash_cells(t.head)
|
||||
end
|
||||
out << sub
|
||||
group = []
|
||||
|
||||
+24
-16
@@ -4,19 +4,23 @@
|
||||
|
||||
require 'shellwords'
|
||||
|
||||
# Fill in a form for one row: <Column> is replaced by the value, <!command!>
|
||||
# by the output of the shell command, and <!cmd <Column>!> passes data to it.
|
||||
# Values inside a command are shell-escaped, so a cell can never add commands
|
||||
# of its own; they arrive as one word each (do not put them in quotes).
|
||||
def fill_form(form, head, row)
|
||||
values = head.zip(row).to_h { |h, v| [h.strip, v.to_s] }
|
||||
form.gsub(/<!(.*?)!>|<([^<>!\n]*)>/m) do
|
||||
if Regexp.last_match(1)
|
||||
cmd = Regexp.last_match(1).gsub(/<([^<>!\n]*)>/) { Shellwords.escape(values.fetch(Regexp.last_match(1).strip, Regexp.last_match(0))) }
|
||||
`#{cmd}`
|
||||
else
|
||||
name = Regexp.last_match(2).strip
|
||||
values.key?(name) ? values[name] : Regexp.last_match(0)
|
||||
module Rdb
|
||||
module Helpers
|
||||
# Fill in a form for one row: <Column> is replaced by the value, <!command!>
|
||||
# by the output of the shell command, and <!cmd <Column>!> passes data to it.
|
||||
# Values inside a command are shell-escaped, so a cell can never add commands
|
||||
# of its own; they arrive as one word each (do not put them in quotes).
|
||||
def fill_form(form, head, row)
|
||||
values = head.zip(row).to_h { |h, v| [h.strip, v.to_s] }
|
||||
form.gsub(/<!(.*?)!>|<([^<>!\n]*)>/m) do
|
||||
if Regexp.last_match(1)
|
||||
cmd = Regexp.last_match(1).gsub(/<([^<>!\n]*)>/) { Shellwords.escape(values.fetch(Regexp.last_match(1).strip, Regexp.last_match(0))) }
|
||||
`#{cmd}`
|
||||
else
|
||||
name = Regexp.last_match(2).strip
|
||||
values.key?(name) ? values[name] : Regexp.last_match(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -76,9 +80,13 @@ Rdb.register('screen', 'Convert a form into a screen-input shell program', 'scre
|
||||
puts ': append row', %(echo "#{names.map { |x| "$#{x}" }.join("\t")}")
|
||||
end
|
||||
|
||||
def label_lines(head, row)
|
||||
h = head.zip(row).to_h
|
||||
[h['Name'], h['Company'], h['Street'], "#{h['City']}, #{h['State']} #{h['ZIP']}".strip].compact
|
||||
module Rdb
|
||||
module Helpers
|
||||
def label_lines(head, row)
|
||||
h = head.zip(row).to_h
|
||||
[h['Name'], h['Company'], h['Street'], "#{h['City']}, #{h['State']} #{h['ZIP']}".strip].compact
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Rdb.register('label', 'Print mailing labels from a mailing list', 'label < list') do |_args|
|
||||
|
||||
+68
-60
@@ -4,20 +4,24 @@ require 'shellwords'
|
||||
|
||||
# Help, manual pages, terminal helpers, menus and housekeeping.
|
||||
|
||||
ESC = "\e"
|
||||
TERM_CAPS = {
|
||||
'cl' => "#{ESC}[H#{ESC}[2J", 'ho' => "#{ESC}[H", 'ce' => "#{ESC}[K", 'cd' => "#{ESC}[J",
|
||||
'cm' => "#{ESC}[%i%d;%dH", 'so' => "#{ESC}[7m", 'se' => "#{ESC}[m", 'us' => "#{ESC}[4m",
|
||||
'ue' => "#{ESC}[m", 'mb' => "#{ESC}[5m", 'md' => "#{ESC}[1m", 'me' => "#{ESC}[m",
|
||||
'as' => "#{ESC}(0", 'ae' => "#{ESC}(B", 'up' => "#{ESC}[A", 'do' => "#{ESC}[B", 'bs' => "\b"
|
||||
Rdb::ESC = "\e"
|
||||
Rdb::TERM_CAPS = {
|
||||
'cl' => "#{Rdb::ESC}[H#{Rdb::ESC}[2J", 'ho' => "#{Rdb::ESC}[H", 'ce' => "#{Rdb::ESC}[K", 'cd' => "#{Rdb::ESC}[J",
|
||||
'cm' => "#{Rdb::ESC}[%i%d;%dH", 'so' => "#{Rdb::ESC}[7m", 'se' => "#{Rdb::ESC}[m", 'us' => "#{Rdb::ESC}[4m",
|
||||
'ue' => "#{Rdb::ESC}[m", 'mb' => "#{Rdb::ESC}[5m", 'md' => "#{Rdb::ESC}[1m", 'me' => "#{Rdb::ESC}[m",
|
||||
'as' => "#{Rdb::ESC}(0", 'ae' => "#{Rdb::ESC}(B", 'up' => "#{Rdb::ESC}[A", 'do' => "#{Rdb::ESC}[B", 'bs' => "\b"
|
||||
}.freeze
|
||||
|
||||
def command_rows
|
||||
Rdb::COMMANDS.values.reject { |c| c.name.include?('.list') || c.name == 'rdb.install' }.sort_by(&:name)
|
||||
end
|
||||
module Rdb
|
||||
module Helpers
|
||||
def command_rows
|
||||
Rdb::COMMANDS.values.reject { |c| c.name.include?('.list') || c.name == 'rdb.install' }.sort_by(&:name)
|
||||
end
|
||||
|
||||
def syntax_of(cmd)
|
||||
cmd.syntax.sub(/\A\S+\s*/, '')
|
||||
def syntax_of(cmd)
|
||||
cmd.syntax.sub(/\A\S+\s*/, '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Rdb.register('rdb.list', 'List all /rdb programs', 'rdb') do |_args|
|
||||
@@ -80,56 +84,60 @@ end
|
||||
|
||||
# ---- manual pages ---------------------------------------------------------------
|
||||
|
||||
def troff_clean(str)
|
||||
# a literal backslash (\\ or \e) is parked in \0 so that later rules cannot see it
|
||||
str.gsub('\\\\', "\0").gsub(/\\f[BIRP]/, '').gsub('\\e', "\0").gsub('\\(em', ' -- ').gsub('\\(aa', "'")
|
||||
.gsub('\\-', '-').gsub('\\&', '').gsub('\\*(UX', 'UNIX').gsub('``', '"').gsub("''", '"').gsub('\\ ', ' ')
|
||||
.gsub(/\\\(..|\\\*\(..|\\[a-z]/, '').gsub("\0", '\\')
|
||||
end
|
||||
module Rdb
|
||||
module Helpers
|
||||
def troff_clean(str)
|
||||
# a literal backslash (\\ or \e) is parked in \0 so that later rules cannot see it
|
||||
str.gsub('\\\\', "\0").gsub(/\\f[BIRP]/, '').gsub('\\e', "\0").gsub('\\(em', ' -- ').gsub('\\(aa', "'")
|
||||
.gsub('\\-', '-').gsub('\\&', '').gsub('\\*(UX', 'UNIX').gsub('``', '"').gsub("''", '"').gsub('\\ ', ' ')
|
||||
.gsub(/\\\(..|\\\*\(..|\\[a-z]/, '').gsub("\0", '\\')
|
||||
end
|
||||
|
||||
def render_man(src)
|
||||
out = []
|
||||
para = []
|
||||
verbatim = false
|
||||
flush = lambda do
|
||||
unless para.empty?
|
||||
words = para.join(' ').split
|
||||
line = ''
|
||||
words.each { |w| line.size + w.size + 1 > 72 ? (out << line; line = w) : line = (line.empty? ? w : "#{line} #{w}") }
|
||||
out << line unless line.empty?
|
||||
para.clear
|
||||
end
|
||||
end
|
||||
src.each_line do |raw|
|
||||
l = raw.chomp
|
||||
if l.start_with?('.')
|
||||
macro, rest = l[1..].split(/\s+/, 2)
|
||||
rest = rest.to_s.strip.delete_prefix('"').delete_suffix('"')
|
||||
case macro
|
||||
when 'TH', 'II', 'ta', 'ne', 'R', 'RE', 'SO', 'B1', 'B2', 'nf', 'fi', 'br' then nil
|
||||
when '\\"' then nil
|
||||
when 'PC' then flush.(); out << ''
|
||||
when 'SH' then flush.(); out << '' << troff_clean(rest).upcase
|
||||
when 'PP', 'sp' then flush.(); out << ''
|
||||
when 'DM', 'DS' then flush.(); verbatim = true
|
||||
when 'DE' then verbatim = false
|
||||
when 'IP' then flush.(); out << ''; para << troff_clean(rest.split(/\s+\d/).first.to_s)
|
||||
when 'B', 'I', 'BR', 'IR', 'RB', 'RI' then para << troff_clean(rest.delete('"')).gsub(/\s+/, ' ') unless verbatim
|
||||
else nil
|
||||
def render_man(src)
|
||||
out = []
|
||||
para = []
|
||||
verbatim = false
|
||||
flush = lambda do
|
||||
unless para.empty?
|
||||
words = para.join(' ').split
|
||||
line = ''
|
||||
words.each { |w| line.size + w.size + 1 > 72 ? (out << line; line = w) : line = (line.empty? ? w : "#{line} #{w}") }
|
||||
out << line unless line.empty?
|
||||
para.clear
|
||||
end
|
||||
end
|
||||
src.each_line do |raw|
|
||||
l = raw.chomp
|
||||
if l.start_with?('.')
|
||||
macro, rest = l[1..].split(/\s+/, 2)
|
||||
rest = rest.to_s.strip.delete_prefix('"').delete_suffix('"')
|
||||
case macro
|
||||
when 'TH', 'II', 'ta', 'ne', 'R', 'RE', 'SO', 'B1', 'B2', 'nf', 'fi', 'br' then nil
|
||||
when '\\"' then nil
|
||||
when 'PC' then flush.(); out << ''
|
||||
when 'SH' then flush.(); out << '' << troff_clean(rest).upcase
|
||||
when 'PP', 'sp' then flush.(); out << ''
|
||||
when 'DM', 'DS' then flush.(); verbatim = true
|
||||
when 'DE' then verbatim = false
|
||||
when 'IP' then flush.(); out << ''; para << troff_clean(rest.split(/\s+\d/).first.to_s)
|
||||
when 'B', 'I', 'BR', 'IR', 'RB', 'RI' then para << troff_clean(rest.delete('"')).gsub(/\s+/, ' ') unless verbatim
|
||||
else nil
|
||||
end
|
||||
elsif verbatim
|
||||
out << " #{troff_clean(l)}"
|
||||
elsif l.start_with?("\t", ' ')
|
||||
flush.()
|
||||
out << " #{troff_clean(l.strip)}"
|
||||
elsif l.strip.empty?
|
||||
flush.()
|
||||
else
|
||||
para << troff_clean(l.strip)
|
||||
end
|
||||
end
|
||||
elsif verbatim
|
||||
out << " #{troff_clean(l)}"
|
||||
elsif l.start_with?("\t", ' ')
|
||||
flush.()
|
||||
out << " #{troff_clean(l.strip)}"
|
||||
elsif l.strip.empty?
|
||||
flush.()
|
||||
else
|
||||
para << troff_clean(l.strip)
|
||||
out.join("\n").squeeze("\n")
|
||||
end
|
||||
end
|
||||
flush.()
|
||||
out.join("\n").squeeze("\n")
|
||||
end
|
||||
|
||||
Rdb.register('man', 'Display the manual page for a command', 'man [-g] command') do |args|
|
||||
@@ -161,21 +169,21 @@ end
|
||||
|
||||
Rdb.register('termput', 'Get terminal capability from /etc/termcap file', 'termput capability') do |args|
|
||||
die 'usage: termput capability' unless args.size == 1
|
||||
$stdout.print TERM_CAPS.fetch(args[0]) { die "no such capability '#{args[0]}'" }
|
||||
$stdout.print Rdb::TERM_CAPS.fetch(args[0]) { die "no such capability '#{args[0]}'" }
|
||||
end
|
||||
|
||||
Rdb.register('tset', 'Fetch termcap entry for a terminal type', 'tset [ TERM ]') do |args|
|
||||
term = args.first || ENV['TERM'] || 'ansi'
|
||||
puts ":#{TERM_CAPS.map { |k, v| "#{k}=#{v.inspect[1..-2]}" }.join(':')}:co##{ENV['COLUMNS'] || 80}:li##{ENV['LINES'] || 24}:tc=#{term}:"
|
||||
puts ":#{Rdb::TERM_CAPS.map { |k, v| "#{k}=#{v.inspect[1..-2]}" }.join(':')}:co##{ENV['COLUMNS'] || 80}:li##{ENV['LINES'] || 24}:tc=#{term}:"
|
||||
end
|
||||
|
||||
Rdb.register('clear.rdb', "Clear the terminal's screen", 'clear.rdb') do |_args|
|
||||
$stdout.print TERM_CAPS['cl']
|
||||
$stdout.print Rdb::TERM_CAPS['cl']
|
||||
end
|
||||
|
||||
Rdb.register('cursor', 'Move the cursor to the row and column requested', 'cursor row col [ CURSOR ]') do |args|
|
||||
die 'usage: cursor row col [CURSOR]' if args.size < 2
|
||||
$stdout.print "#{ESC}[#{args[0].to_i};#{args[1].to_i}H"
|
||||
$stdout.print "#{Rdb::ESC}[#{args[0].to_i};#{args[1].to_i}H"
|
||||
$stdout.flush
|
||||
end
|
||||
|
||||
|
||||
+14
-10
@@ -2,16 +2,20 @@
|
||||
|
||||
# Formatting tables and working with text.
|
||||
|
||||
def justify_numbers(cells)
|
||||
parts = cells.map { |c| c.empty? ? ['', nil] : c.split('.', 2) }
|
||||
int_w = parts.map { |i, _| i.size }.max
|
||||
frac_w = parts.map { |_, f| f ? f.size : 0 }.max
|
||||
cells.zip(parts).map do |c, (i, f)|
|
||||
next ' ' * (int_w + (frac_w.zero? ? 0 : frac_w + 1)) if c.empty?
|
||||
module Rdb
|
||||
module Helpers
|
||||
def justify_numbers(cells)
|
||||
parts = cells.map { |c| c.empty? ? ['', nil] : c.split('.', 2) }
|
||||
int_w = parts.map { |i, _| i.size }.max
|
||||
frac_w = parts.map { |_, f| f ? f.size : 0 }.max
|
||||
cells.zip(parts).map do |c, (i, f)|
|
||||
next ' ' * (int_w + (frac_w.zero? ? 0 : frac_w + 1)) if c.empty?
|
||||
|
||||
s = i.rjust(int_w)
|
||||
s += f ? ".#{f.ljust(frac_w)}" : ' ' * (frac_w.zero? ? 0 : frac_w + 1)
|
||||
s
|
||||
s = i.rjust(int_w)
|
||||
s += f ? ".#{f.ljust(frac_w)}" : ' ' * (frac_w.zero? ? 0 : frac_w + 1)
|
||||
s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -112,7 +116,7 @@ Rdb.register('pad', 'Add extra spaces at end of last column', 'pad [-number] < t
|
||||
if t.list
|
||||
t.with(rows: t.rows.map { |r| r[0...-1] + [r.last + ' ' * n] }).write
|
||||
else
|
||||
$stdout.write t.head.join("\t"), "\n", Rdb::Table.dash_row(t.head), "\n"
|
||||
$stdout.write Rdb::Table.head_text(t.head)
|
||||
t.rows.each { |r| puts (r[0...-1] + [r.last + ' ' * n]).join("\t") }
|
||||
end
|
||||
end
|
||||
|
||||
+10
-11
@@ -1,8 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Rdb
|
||||
# Helpers of single command files (lib/rdb/cmd/*.rb) go in here, not on
|
||||
# Object; every Env includes them.
|
||||
module Helpers; end
|
||||
|
||||
# Command blocks run inside an Env, which holds the helpers they share.
|
||||
class Env
|
||||
include Helpers
|
||||
attr_reader :name
|
||||
|
||||
def initialize(name)
|
||||
@@ -30,14 +35,12 @@ module Rdb
|
||||
str.strip[/\.(\d+)\z/, 1].to_s.size
|
||||
end
|
||||
|
||||
# Decimal arithmetic, so that 0.1 + 0.2 is 0.3 and money adds up to the cent.
|
||||
def dec_num(str)
|
||||
BigDecimal(str.strip.sub(/\.\z/, ''))
|
||||
Rdb.decimal(str)
|
||||
end
|
||||
|
||||
def fmt(num, dec)
|
||||
num = BigDecimal(num.to_s) unless num.is_a?(BigDecimal)
|
||||
dec.zero? ? num.round(0, :half_up).to_i.to_s : num.round(dec, :half_up).to_s('F').then { |t| t.sub(/\.(\d*)\z/) { ".#{Regexp.last_match(1).ljust(dec, '0')}" } }
|
||||
Rdb.fixed(num, dec)
|
||||
end
|
||||
|
||||
def blank_row?(row)
|
||||
@@ -102,18 +105,14 @@ module Rdb
|
||||
|
||||
# Run an awk program over rows (tab separated) and return the output rows.
|
||||
def awk(prog, rows)
|
||||
input = rows.map { |r| r.join("\t") }.join("\n")
|
||||
input += "\n" unless input.empty?
|
||||
out, err, st = Open3.capture3('awk', '-F', "\t", prog, stdin_data: input)
|
||||
$stderr.print err unless err.empty?
|
||||
exit 1 unless st.success?
|
||||
out, ok = awk_text(prog, rows)
|
||||
exit 1 unless ok
|
||||
out.each_line.map { |l| l.chomp.split("\t", -1) }
|
||||
end
|
||||
|
||||
# Raw output of awk, for programs that print their own text.
|
||||
def awk_text(prog, rows)
|
||||
input = rows.map { |r| r.join("\t") }.join("\n")
|
||||
input += "\n" unless input.empty?
|
||||
input = rows.map { |r| "#{r.join("\t")}\n" }.join
|
||||
out, err, st = Open3.capture3('awk', '-F', "\t", prog, stdin_data: input)
|
||||
$stderr.print err unless err.empty?
|
||||
[out, st.success?]
|
||||
|
||||
+2
-2
@@ -129,7 +129,7 @@ module Rdb
|
||||
when 'b'
|
||||
k = data.column(keycol)
|
||||
body = live_rows(data).sort_by { |r| r.cells[k].to_s.strip }.map { |r| r.line + "\n" }
|
||||
Rdb.write_file(path, data.head.join("\t") + "\n" + Table.dash_row(data.head) + "\n" + body.join)
|
||||
Rdb.write_file(path, Table.head_text(data.head) + body.join)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -393,7 +393,7 @@ module Rdb
|
||||
return [] if text.empty?
|
||||
|
||||
lines = text.lines.map { |l| l.chomp("\n") }
|
||||
if lines.size >= 2 && Table.dash_line?(lines[1].split("\t", -1))
|
||||
if Table.headed?(lines)
|
||||
lines.drop(2).reject(&:empty?).map { |l| l.split("\t", -1).first(ncols) }
|
||||
else
|
||||
lines.reject(&:empty?).map { |l| l.split("\t", -1).first(ncols) }
|
||||
|
||||
+16
-4
@@ -28,6 +28,11 @@ module Rdb
|
||||
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")
|
||||
@@ -78,15 +83,22 @@ module Rdb
|
||||
def to_s
|
||||
return list_text if @list
|
||||
|
||||
out = +''
|
||||
out << @head.join("\t") << "\n"
|
||||
out << Table.dash_row(@head) << "\n"
|
||||
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)
|
||||
head.map { |h| '-' * [h.size, 1].max }.join("\t")
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user