lib/fat_table/table.rb in fat_table-0.8.0 vs lib/fat_table/table.rb in fat_table-0.9.0
- old
+ new
@@ -51,10 +51,11 @@
# 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_reader :heads
# Headers of columns that are to be tolerant when they are built.
attr_accessor :tolerant_cols
attr_reader :omni_typ, :omni_tol
@@ -93,13 +94,14 @@
# is treated as a specifying the given type but marking it as tolerant as
# well. The values in the type hash can be any string or sybol that
# starts with 'num', 'dat', 'bool', or 'str' to specify Numeric,
# DateTime, Boolean, or String types respectively.
def initialize(*heads, **types)
+ @heads = heads.flatten.map(&:as_sym)
+ @types = types
@columns = []
@tolerant_cols = []
- @headers = []
# Check for the special 'omni' key
@omni_type = 'NilClass'
@omni_tol = false
if types.keys.map(&:to_s).include?('omni')
# All columns not otherwise included in types should have the type and
@@ -108,36 +110,46 @@
@omni_type, @omni_tol = Table.typ_tol(omni_val)
# Remove omni from types.
types.delete(:omni)
types.delete('omni')
end
- heads += types.keys
- heads.uniq.each do |h|
- typ, tol = Table.typ_tol(types[h])
+ # heads += types.keys
+ (heads.flatten + types.keys).uniq.each do |h|
+ if types[h]
+ typ, tol = Table.typ_tol(types[h])
+ else
+ typ = @omni_type
+ tol = @omni_tol
+ end
@tolerant_cols << h.to_s.as_sym if tol
@columns << Column.new(header: h.to_s.sub(/~\s*\z/, ''), type: typ,
tolerant: tol)
end
@explicit_boundaries = []
end
# :category: Constructors
- # Return an empty duplicate of self. This allows the library to create an
- # empty table that preserves all the instance variables from self. Even
- # though FatTable::Table objects have no instance variables, a class that
- # inherits from it might.
- def empty_dup
- dup.__empty!
+ # Return an new table based on this Table but with empty columns named by
+ # the result_cols parameter, by default the this Table's columns. If any
+ # of the result_cols have the same name as an existing column, inherit
+ # that column's type and tolerance. Also, set any instance variables that
+ # might have been set by a subclass instance.
+ def empty_dup(result_cols = nil)
+ result_cols ||= heads
+ result_types = types.select { |k,_v| result_cols.include?(k) }
+ result = Table.new(result_cols, **result_types)
+ tolerant_cols.each do |h|
+ result.tolerant_cols << h
+ result.column(h).tolerant = true
+ end
+ (instance_variables - result.instance_variables).each do |v|
+ result.instance_variable_set(instance_variable_get(v))
+ end
+ result
end
- def __empty!
- @columns = []
- @explicit_boundaries = []
- self
- end
-
# :category: Constructors
# Construct a Table from the contents of a CSV file named +fname+. Headers
# will be taken from the first CSV row and converted to symbols.
def self.from_csv_file(fname, **types)
@@ -262,11 +274,12 @@
# 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, **types)
- result = new(**types)
+ heads = hashes.first.keys
+ result = new(*heads, **types)
hashes.each do |hsh|
if hsh.nil?
unless hlines
msg = 'found an hline in input: try setting hlines true'
raise UserError, msg
@@ -291,11 +304,10 @@
# boundary. Note: In org mode code blocks, by default (:hlines no) all
# hlines are stripped from the table, otherwise (:hlines yes) they are
# indicated with nil elements in the outer array as expected by this
# method when hlines is set true.
def from_array_of_arrays(rows, hlines: false, **types)
- result = new(**types)
headers = []
if !hlines
# Take the first row as headers
# Second row et seq as data
headers = rows[0].map(&:to_s).map(&:as_sym)
@@ -310,10 +322,11 @@
# Synthesize headers
# Row 0 et seq are data
headers = (1..rows[0].size).to_a.map { |k| "col_#{k}".as_sym }
first_data_row = 0
end
+ result = new(*headers, **types)
rows[first_data_row..-1].each do |row|
if row.nil?
unless hlines
msg = 'found an hline in input: try setting hlines true'
raise UserError, msg
@@ -417,10 +430,11 @@
# Set the column type for Column with the given +key+ as a String type.
def force_string!(*keys)
keys.each do |h|
raise UserError, "force_string!: #{h} not a column in table" unless column(h)
+
column(h).force_string!
end
self
end
@@ -843,10 +857,11 @@
# each row where the sort key changes.
def order_with(expr)
unless expr.is_a?(String)
raise "must call FatTable::Table\#order_with with a single string expression"
end
+
rev = false
if expr.match?(/\s*!\s*\z/)
rev = true
expr = expr.sub(/\s*!\s*\z/, '')
end
@@ -955,12 +970,19 @@
end
ev = Evaluator.new(ivars: ivars,
before: before_hook,
after: after_hook)
# Compute the new Table from this Table
- result = empty_dup
+ result_cols =
+ if cols.include?(:omni)
+ (headers + new_cols.keys - [:omni])
+ else
+ (cols + new_cols.keys)
+ end
+ result = empty_dup(result_cols)
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)
ev.update_ivars(row: old_k + 1, group: grp)
@@ -1026,19 +1048,10 @@
# tab.where('date > Date.today - 30') => rows with recent dates
# tab.where('@row.even? && shares > 500') => even rows with lots of shares
def where(expr)
expr = expr.to_s
result = empty_dup
- headers.each do |h|
- col =
- if tolerant_col?(h)
- Column.new(header: h, tolerant: true)
- else
- Column.new(header: h)
- end
- result.add_column(col)
- end
ev = Evaluator.new(ivars: { row: 0, group: 0 })
rows.each_with_index do |row, k|
grp = row_index_to_group_index(k)
ev.update_ivars(row: k + 1, group: grp)
ev.eval_before_hook(locals: row)
@@ -1092,14 +1105,17 @@
# the same type in the two tables, or an exception will be thrown.
# Duplicates are not eliminated from the result. Adds group boundaries at
# boundaries of the constituent tables. Preserves and adjusts the group
# boundaries of the constituent table.
def union_all(other)
- set_operation(other, :+,
- distinct: false,
- add_boundaries: true,
- inherit_boundaries: true)
+ set_operation(
+ other,
+ :+,
+ distinct: false,
+ add_boundaries: true,
+ inherit_boundaries: true
+ )
end
# :category: Operators
# Return a Table that includes the rows that appear in this table and in
@@ -1487,10 +1503,11 @@
def group_by(*group_cols, **agg_cols)
sorted_tab = order_by(group_cols)
groups = sorted_tab.rows.group_by do |r|
group_cols.map { |k| r[k] }
end
- result = empty_dup
+ grp_types = types.select { |k, _v| group_cols.include?(k) }
+ result = Table.new(*group_cols, **grp_types)
groups.each_pair do |_vals, grp_rows|
result << row_from_group(grp_rows, group_cols, agg_cols)
end
result.normalize_boundaries
result