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