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