lib/fat_table/table.rb in fat_table-0.2.6 vs lib/fat_table/table.rb in fat_table-0.2.7
- old
+ new
@@ -47,13 +47,13 @@
#
# In the resulting Table, the headers are converted into symbols, with all
# spaces converted to underscore and everything down-cased. So, the heading,
# 'Two Words' becomes the header +:two_words+.
class Table
-
# An Array of FatTable::Columns that constitute the table.
attr_reader :columns
+ attr_accessor :boundaries
###########################################################################
# Constructors
###########################################################################
@@ -138,22 +138,23 @@
end
end
# :category: Constructors
- # Construct a new table from another FatTable::Table object +table+. Inherit any
- # group boundaries from the input table.
+ # Construct a new table from another FatTable::Table object +table+. Inherit
+ # any group boundaries from the input table.
def self.from_table(table)
table.deep_dup
end
# :category: Constructors
# Construct a Table by running a SQL +query+ against the database set up
# with FatTable.set_db, with the rows of the query result as rows.
def self.from_sql(query)
- raise UserError, 'FatTable.db must be set with FatTable.set_db' if FatTable.db.nil?
+ msg = 'FatTable.db must be set with FatTable.set_db'
+ raise UserError, msg if FatTable.db.nil?
result = Table.new
FatTable.db[query].each do |h|
result << h
end
result
@@ -164,19 +165,20 @@
############################################################################
class << self
private
- # Construct table from an array of hashes or an array of any object that can
- # respond to #to_h. If an array element is a nil, mark it as a group
+ # Construct table from an array of hashes or an array of any object that
+ # can respond to #to_h. If an array element is a nil, mark it as a group
# boundary in the Table.
def from_array_of_hashes(hashes, hlines: false)
result = new
hashes.each do |hsh|
if hsh.nil?
unless hlines
- raise UserError, 'found an hline in input with hlines false; try setting hlines true'
+ msg = 'found an hline in input: try setting hlines true'
+ raise UserError, msg
end
result.mark_boundary
next
end
result << hsh.to_h
@@ -217,11 +219,12 @@
first_data_row = 0
end
rows[first_data_row..-1].each do |row|
if row.nil?
unless hlines
- raise UserError, 'found an hline in input with hlines false; try setting hlines true'
+ msg = 'found an hline in input: try setting hlines true'
+ raise UserError, msg
end
result.mark_boundary
next
end
row = row.map { |s| s.to_s.strip }
@@ -251,19 +254,19 @@
table_found = false
header_found = false
io.each do |line|
unless table_found
# Skip through the file until a table is found
- next unless line =~ table_re
- unless line =~ hrule_re
+ next unless line.match?(table_re)
+ unless line.match?(hrule_re)
line = line.sub(/\A\s*\|/, '').sub(/\|\s*\z/, '')
rows << line.split('|').map(&:clean)
end
table_found = true
next
end
- break unless line =~ table_re
+ break unless line.match?(table_re)
if !header_found && line =~ hrule_re
rows << nil
header_found = true
next
elsif header_found && line =~ hrule_re
@@ -309,17 +312,20 @@
# column-major order: \tab\[:id\]\[8\] returns the 9th item in the column
# headed :id and so does \tab\[8\]\[:id\].
def [](key)
case key
when Integer
- raise UserError, "index '#{key}' out of range" unless (0..size-1).cover?(key.abs)
+ msg = "index '#{key}' out of range"
+ raise UserError, msg unless (0..size - 1).cover?(key.abs)
rows[key]
when String
- raise UserError, "header '#{key}' not in table" unless headers.include?(key)
+ msg = "header '#{key}' not in table"
+ raise UserError, msg unless headers.include?(key)
column(key).items
when Symbol
- raise UserError, "header ':#{key}' not in table" unless headers.include?(key)
+ msg = "header ':#{key}' not in table"
+ raise UserError, msg unless headers.include?(key)
column(key).items
else
raise UserError, "cannot index table with a #{key.class}"
end
end
@@ -411,13 +417,13 @@
end
end
rows
end
- #############################################################################
+ ############################################################################
# Enumerable
- #############################################################################
+ ############################################################################
public
include Enumerable
@@ -428,13 +434,10 @@
rows.each do |row|
yield row
end
end
-
- public
-
# :category: Attributes
# Boundaries mark the last row in each "group" within the table. The last
# row of the table is always an implicit boundary, and having the last row
# as the sole boundary is the default for new tables unless mentioned
@@ -503,20 +506,10 @@
protected
# :stopdoc:
- # Reader for boundaries, but not public.
- def boundaries
- @boundaries
- end
-
- # Writer for boundaries, but not public.
- def boundaries=(bounds)
- @boundaries = bounds
- end
-
# Make sure size - 1 is last boundary and that they are unique and sorted.
def normalize_boundaries
unless empty?
boundaries.push(size - 1) unless boundaries.include?(size - 1)
self.boundaries = boundaries.uniq.sort
@@ -680,13 +673,12 @@
ivars = { row: 0, group: 0 }
if new_cols.key?(:ivars)
ivars = ivars.merge(new_cols[:ivars])
new_cols.delete(:ivars)
end
- before_hook = '@row += 1'
if new_cols.key?(:before_hook)
- before_hook += "; #{new_cols[:before_hook]}"
+ before_hook = new_cols[:before_hook].to_s
new_cols.delete(:before_hook)
end
after_hook = nil
if new_cols.key?(:after_hook)
after_hook = new_cols[:after_hook].to_s
@@ -700,36 +692,39 @@
normalize_boundaries
rows.each_with_index do |old_row, old_k|
# Set the group number in the before hook and run the hook with the
# local variables set to the row before the new row is evaluated.
grp = row_index_to_group_index(old_k)
- vars = old_row.merge(__group: grp)
- ev.eval_before_hook(vars)
+ ev.update_ivars(row: old_k + 1, group: grp)
+ ev.eval_before_hook(locals: old_row)
# Compute the new row.
new_row = {}
cols.each do |k|
h = k.as_sym
- raise UserError, "Column '#{h}' in select does not exist" unless column?(h)
+ msg = "Column '#{h}' in select does not exist"
+ raise UserError, msg unless column?(h)
new_row[h] = old_row[h]
end
- new_cols.each_pair do |key, val|
+ new_cols.each_pair do |key, expr|
key = key.as_sym
vars = old_row.merge(new_row)
- case val
+ case expr
when Symbol
- raise UserError, "Column '#{val}' in select does not exist" unless vars.keys.include?(val)
- new_row[key] = vars[val]
+ msg = "Column '#{expr}' in select does not exist"
+ raise UserError, msg unless vars.keys.include?(expr)
+ new_row[key] = vars[expr]
when String
- new_row[key] = ev.evaluate(val, vars: vars)
+ new_row[key] = ev.evaluate(expr, locals: vars)
else
- raise UserError, "Hash parameter '#{key}' to select must be a symbol or string"
+ msg = "Hash parameter '#{key}' to select must be a symbol or string"
+ raise UserError, msg
end
end
# Set the group number and run the hook with the local variables set to
# the row after the new row is evaluated.
- vars = new_row.merge(__group: grp)
- ev.eval_after_hook(vars)
+ # vars = new_row.merge(__group: grp)
+ ev.eval_after_hook(locals: new_row)
result << new_row
end
result.boundaries = boundaries
result.normalize_boundaries
result
@@ -751,18 +746,17 @@
result = Table.new
headers.each do |h|
col = Column.new(header: h)
result.add_column(col)
end
- ev = Evaluator.new(ivars: { row: 0, group: 0 },
- before: '@row += 1')
+ ev = Evaluator.new(ivars: { row: 0, group: 0 })
rows.each_with_index do |row, k|
grp = row_index_to_group_index(k)
- vars = row.merge(__group: grp)
- ev.eval_before_hook(vars)
- result << row if ev.evaluate(expr, vars: vars)
- ev.eval_after_hook(vars)
+ ev.update_ivars(row: k + 1, group: grp)
+ ev.eval_before_hook(locals: row)
+ result << row if ev.evaluate(expr, locals: row)
+ ev.eval_after_hook(locals: row)
end
result.normalize_boundaries
result
end
@@ -876,14 +870,16 @@
def set_operation(other, op = :+,
distinct: true,
add_boundaries: true,
inherit_boundaries: false)
unless columns.size == other.columns.size
- raise UserError, 'Cannot apply a set operation to tables with a different number of columns.'
+ msg = "can't apply set ops to tables with a different number of columns"
+ raise UserError, msg
end
unless columns.map(&:type) == other.columns.map(&:type)
- raise UserError, 'Cannot apply a set operation to tables with different column types.'
+ msg = "can't apply a set ops to tables with different column types."
+ raise UserError, msg
end
other_rows = other.rows.map { |r| r.replace_keys(headers) }
result = Table.new
new_rows = rows.send(op, other_rows)
new_rows.each_with_index do |row, k|
@@ -900,11 +896,11 @@
end
public
# An Array of symbols for the valid join types.
- JOIN_TYPES = [:inner, :left, :right, :full, :cross].freeze
+ JOIN_TYPES = %i[inner left right full cross].freeze
# :category: Operators
#
# Return a table that joins this Table to +other+ based on one or more join
# expressions +exps+ using the +join_type+ in determining the rows of the
@@ -980,42 +976,42 @@
raise UserError, "join_type may only be: #{JOIN_TYPES.join(', ')}"
end
# These may be needed for outer joins.
self_row_nils = headers.map { |h| [h, nil] }.to_h
other_row_nils = other.headers.map { |h| [h, nil] }.to_h
- join_expression, other_common_heads = build_join_expression(exps, other, join_type)
+ join_exp, other_common_heads =
+ build_join_expression(exps, other, join_type)
ev = Evaluator.new
result = Table.new
other_rows = other.rows
other_row_matches = Array.new(other_rows.size, false)
rows.each do |self_row|
self_row_matched = false
other_rows.each_with_index do |other_row, k|
# Same as other_row, but with keys that are common with self and equal
# in value, removed, so the output table need not repeat them.
locals = build_locals_hash(row_a: self_row, row_b: other_row)
- matches = ev.evaluate(join_expression, vars: locals)
+ matches = ev.evaluate(join_exp, locals: locals)
next unless matches
self_row_matched = other_row_matches[k] = true
out_row = build_out_row(row_a: self_row, row_b: other_row,
common_heads: other_common_heads,
type: join_type)
result << out_row
end
- if join_type == :left || join_type == :full
- unless self_row_matched
- out_row = build_out_row(row_a: self_row, row_b: other_row_nils, type: join_type)
- result << out_row
- end
- end
+ next unless %i[left full].include?(join_type)
+ next if self_row_matched
+ result << build_out_row(row_a: self_row,
+ row_b: other_row_nils,
+ type: join_type)
end
- if join_type == :right || join_type == :full
+ if %i[right full].include?(join_type)
other_rows.each_with_index do |other_row, k|
- unless other_row_matches[k]
- out_row = build_out_row(row_a: self_row_nils, row_b: other_row, type: join_type)
- result << out_row
- end
+ next if other_row_matches[k]
+ result << build_out_row(row_a: self_row_nils,
+ row_b: other_row,
+ type: join_type)
end
end
result.normalize_boundaries
result
end
@@ -1098,12 +1094,12 @@
b_heads = other.headers
common_heads = a_heads & b_heads
b_common_heads = []
if exps.empty?
if common_heads.empty?
- raise UserError,
- 'A non-cross join with no common column names requires join expressions'
+ msg = "#{type}-join with no common column names needs join expression"
+ raise UserError, msg
else
# A Natural join on all common heads
common_heads.each do |h|
ensure_common_types!(self_h: h, other_h: h, other: other)
end
@@ -1124,11 +1120,13 @@
unless a_heads.include?(a_head)
raise UserError, "no column '#{a_head}' in table"
end
if partial_result
# Second of a pair
- ensure_common_types!(self_h: a_head, other_h: last_sym, other: other)
+ ensure_common_types!(self_h: a_head,
+ other_h: last_sym,
+ other: other)
partial_result << "#{a_head}_a)"
and_conds << partial_result
partial_result = nil
else
# First of a pair of _a or _b
@@ -1140,11 +1138,13 @@
unless b_heads.include?(b_head)
raise UserError, "no column '#{b_head}' in second table"
end
if partial_result
# Second of a pair
- ensure_common_types!(self_h: last_sym, other_h: b_head, other: other)
+ ensure_common_types!(self_h: last_sym,
+ other_h: b_head,
+ other: other)
partial_result << "#{b_head}_b)"
and_conds << partial_result
partial_result = nil
else
# First of a pair of _a or _b
@@ -1156,46 +1156,48 @@
# No modifier, so must be one of the common columns
unless partial_result.nil?
# We were expecting the second of a modified pair, but got an
# unmodified symbol instead.
msg =
- "must follow '#{last_sym}' by qualified exp from the other table"
+ "follow '#{last_sym}' by qualified exp from the other table"
raise UserError, msg
end
# We have an unqualified symbol that must appear in both tables
unless common_heads.include?(exp)
- raise UserError, "unqualified column '#{exp}' must occur in both tables"
+ msg = "unqualified column '#{exp}' must occur in both tables"
+ raise UserError, msg
end
ensure_common_types!(self_h: exp, other_h: exp, other: other)
and_conds << "(#{exp}_a == #{exp}_b)"
b_common_heads << exp
end
when String
# We have a string expression in which all column references must be
# qualified.
and_conds << "(#{exp})"
else
- raise UserError, "invalid join expression '#{exp}' of class #{exp.class}"
+ msg = "invalid join expression '#{exp}' of class #{exp.class}"
+ raise UserError, msg
end
end
[and_conds.join(' && '), b_common_heads]
end
end
# Raise an exception unless self_h in this table and other_h in other table
# have the same types.
def ensure_common_types!(self_h:, other_h:, other:)
unless column(self_h).type == other.column(other_h).type
- raise UserError,
- "type of column '#{self_h}' does not match type of column '#{other_h}"
+ msg = "column '#{self_h}' type does not match column '#{other_h}"
+ raise UserError, msg
end
self
end
- ###################################################################################
+ ############################################################################
# Group By
- ###################################################################################
+ ############################################################################
public
# :category: Operators
@@ -1275,11 +1277,12 @@
# :category: Constructors
# Add a FatTable::Column object +col+ to the table.
def add_column(col)
- raise "Table already has a column with header '#{col.header}'" if column?(col.header)
+ msg = "Table already has a column with header '#{col.header}'"
+ raise msg if column?(col.header)
columns << col
self
end
############################################################################
@@ -1322,10 +1325,11 @@
#
# :call-seq: to_any(fmt_type, options = {}) { |fmt| ... }
#
def to_any(fmt_type, options = {})
fmt = fmt_type.as_sym
- raise UserError, "unknown format '#{fmt}'" unless FatTable::FORMATS.include?(fmt)
+ msg = "unknown format '#{fmt}'"
+ raise UserError, msg unless FatTable::FORMATS.include?(fmt)
method = "to_#{fmt}"
if block_given?
send method, options, &Proc.new
else
send method, options