lib/fat_table/table.rb in fat_table-0.2.4 vs lib/fat_table/table.rb in fat_table-0.2.6
- old
+ new
@@ -151,13 +151,11 @@
# 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?
result = Table.new
- sth = FatTable.db.prepare(query)
- sth.execute
- sth.fetch_hash do |h|
+ FatTable.db[query].each do |h|
result << h
end
result
end
@@ -593,11 +591,11 @@
new_tab.normalize_boundaries
new_tab
end
# :category: Operators
-
+ #
# Return a Table having the selected column expressions. Each expression can
# be either a
#
# 1. in +cols+, a symbol, +:old_col+, representing a column in the current
# table,
@@ -611,47 +609,127 @@
# (whether selected for the output table or not) as well as any +new_col+
# defined earlier in the argument list. The expression string can also
# access the instance variable @row, as the row number of the row being
# evaluated, and @group, as the group number of the row being evaluated.
#
+ # 4. a hash in +new_cols+ with one of the special keys, +ivars: {literal
+ # hash}+, +before_hook: 'ruby-code'+, or +after_hook: 'ruby-code'+ for
+ # defining custom instance variables to be used during evaluation of
+ # parameters described in point 3 and hooks of ruby code snippets to be
+ # evaluated before and after processing each row.
+ #
# The bare symbol arguments +cols+ (1) must precede any hash arguments
# +new_cols+ (2 or 3). Each expression results in a column in the resulting
# Table in the order given in the argument list. The expressions are
# evaluated in left-to-right order as well. The output table preserves any
# groups present in the input table.
#
# tab.select(:ref, :date, :shares) => table with only 3 columns selected
# tab.select(:ref, :date, shares: :quantity) => rename :shares->:quantity
# tab.select(:ref, :date, :shares, cost: 'price * shares') => new column
# tab.select(:ref, :date, :shares, seq: '@row') => add sequential nums
+ #
+ # The instance variables and hooks mentioned in point 4 above allow you to
+ # keep track of things that cross row boundaries, such as running sums or
+ # the values of columns before or after construction of the new row. You can
+ # define instance variables other than the default @row and @group variables
+ # to be available when evaluating normal string expressions for constructing
+ # a new row.
+ #
+ # You define custom instance variables by passing a Hash to the ivars
+ # parameter. The names of the instance variables will be the keys and their
+ # initial values will be the values. For example, you can keep track of a
+ # running sum of the cost of shares and the number of shares in the prior
+ # row by adding two custom instance variables and the appropriate hooks:
+ #
+ # tab.select(:ref, :date, :shares, :price,
+ # cost: 'shares * price', cumulative_cost: '@total_cost'
+ # ivars: { total_cost: 0, prior_shares: 0},
+ # before_hook: '@total_cost += shares * price,
+ # after_hook: '@prior_shares = shares')
+ #
+ # Notice that in the +ivars:+ parameter, the '@' is not prefixed to the name
+ # since it is a symbol, but must be prefixed when the instance variable is
+ # referenced in an expression, otherwise it would be interpreted as a column
+ # name. You could include the '@' if you use a string as a key, e.g., +{
+ # '@total_cost' => 0 }+ The ivars values are evaluated once, before the
+ # first row is processed with the select statement.
+ #
+ # For each row, the +before_hook+ is evaluated, then the +new_cols+
+ # expressions for setting the new value of columns, then the +after_hook+ is
+ # evaluated.
+ #
+ # In the before_hook, the values of all columns are available as local
+ # variables as they were before processing the row. The values of all
+ # instance variables are available as well with the values they had after
+ # processing the prior row of the table.
+ #
+ # In the string expressions for new columns, all the instance variables are
+ # available with the values they have after the before_hook is evaluated.
+ # You could also modify instance variables in the new_cols expression, but
+ # remember, they are evaluated once for each new column expression. Also,
+ # the new column is assigned the value of the entire expression, so you must
+ # ensure that the last expression is the one you want assigned to the new
+ # column. You might want to use a semicolon: +cost: '@total_cost += shares *
+ # price; shares * price'
+ #
+ # In the after_hook, the new, updated values of all columns, old and new are
+ # available as local variables, and the instance variables are available
+ # with the values they had after executing the before_hook.
def select(*cols, **new_cols)
+ # Set up the Evaluator
+ 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]}"
+ new_cols.delete(:before_hook)
+ end
+ after_hook = nil
+ if new_cols.key?(:after_hook)
+ after_hook = new_cols[:after_hook].to_s
+ new_cols.delete(:after_hook)
+ end
+ ev = Evaluator.new(ivars: ivars,
+ before: before_hook,
+ after: after_hook)
+ # Compute the new Table from this Table
result = Table.new
normalize_boundaries
- ev = Evaluator.new(vars: { row: 0, group: 1 },
- before: '@row = __row; @group = __group')
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)
+ # 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)
new_row[h] = old_row[h]
end
new_cols.each_pair do |key, val|
key = key.as_sym
vars = old_row.merge(new_row)
- vars[:__row] = old_k + 1
- vars[:__group] = row_index_to_group_index(old_k)
case val
when Symbol
raise UserError, "Column '#{val}' in select does not exist" unless vars.keys.include?(val)
new_row[key] = vars[val]
when String
new_row[key] = ev.evaluate(val, vars: vars)
else
- raise UserError, 'Hash parameters to select must be a symbol or string'
+ raise UserError, "Hash parameter '#{key}' to select must be a symbol or string"
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)
result << new_row
end
result.boundaries = boundaries
result.normalize_boundaries
result
@@ -673,16 +751,17 @@
result = Table.new
headers.each do |h|
col = Column.new(header: h)
result.add_column(col)
end
- ev = Evaluator.new(vars: { row: 0 },
- before: '@row = __row; @group = __group')
+ ev = Evaluator.new(ivars: { row: 0, group: 0 },
+ before: '@row += 1')
rows.each_with_index do |row, k|
- vars = row.dup
- vars[:__row] = k + 1
- vars[:__group] = row_index_to_group_index(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)
end
result.normalize_boundaries
result
end