lib/fat_table/table.rb in fat_table-0.2.8 vs lib/fat_table/table.rb in fat_table-0.2.9

- old
+ new

@@ -104,22 +104,23 @@ end # :category: Constructors # Construct a new table from an Array of Arrays +aoa+. By default, with - # +hlines+ set to false, do not look for separators, i.e. +nils+, just treat - # the first row as headers. With +hlines+ set true, expect +nil+ separators - # to mark the header row and any boundaries. If the second element of the - # array is a +nil+, interpret the first element of the array as a row of - # headers. Otherwise, synthesize headers of the form +:col_1+, +:col_2+, ... - # and so forth. The remaining elements are taken as the body of the table, - # except that if an element of the outer array is a +nil+, mark the - # preceding row as a group boundary. Note for Emacs users: In org mode code - # blocks when an org-mode table is passed in as a variable it is passed in - # as an Array of Arrays. By default (+ HEADER: :hlines no +) org-mode strips - # all from the table; otherwise (+ HEADER: :hlines yes +) they are indicated - # with nil elements in the outer array. + # +hlines+ set to false, do not look for separators, i.e. +nils+, just + # treat the first row as headers. With +hlines+ set true, expect +nil+ + # separators to mark the header row and any boundaries. If the second + # element of the array is a +nil+, interpret the first element of the + # array as a row of headers. Otherwise, synthesize headers of the form + # +:col_1+, +:col_2+, ... and so forth. The remaining elements are taken + # as the body of the table, except that if an element of the outer array + # is a +nil+, mark the preceding row as a group boundary. Note for Emacs + # users: In org mode code blocks when an org-mode table is passed in as a + # variable it is passed in as an Array of Arrays. By default (+ HEADER: + # :hlines no +) org-mode strips all hrules from the table; otherwise (+ + # HEADER: :hlines yes +) they are indicated with nil elements in the outer + # array. def self.from_aoa(aoa, hlines: false) from_array_of_arrays(aoa, hlines: hlines) end # :category: Constructors @@ -147,14 +148,15 @@ 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. + # with FatTable.connect, with the rows of the query result as rows. def self.from_sql(query) - msg = 'FatTable.db must be set with FatTable.set_db' + msg = 'FatTable.db must be set with FatTable.connect' raise UserError, msg if FatTable.db.nil? + result = Table.new FatTable.db[query].each do |h| result << h end result @@ -255,26 +257,28 @@ header_found = false io.each do |line| unless table_found # Skip through the file until a table is found 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.match?(table_re) + if !header_found && line =~ hrule_re rows << nil header_found = true next elsif header_found && line =~ hrule_re # Mark the boundary with a nil rows << nil - elsif line !~ table_re + elsif !line.match?(table_re) # Stop reading at the second hline break else line = line.sub(/\A\s*\|/, '').sub(/\|\s*\z/, '') rows << line.split('|').map(&:clean) @@ -314,18 +318,21 @@ def [](key) case key when Integer msg = "index '#{key}' out of range" raise UserError, msg unless (0..size - 1).cover?(key.abs) + rows[key] when String msg = "header '#{key}' not in table" raise UserError, msg unless headers.include?(key) + column(key).items when Symbol 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 @@ -358,18 +365,20 @@ # :category: Attributes # Return the number of rows in the Table. def size return 0 if columns.empty? + columns.first.size end # :category: Attributes # Return the number of Columns in the Table. def width return 0 if columns.empty? + columns.size end # :category: Attributes @@ -404,10 +413,11 @@ # of any size. def rows_range(first = 0, last = nil) # :nodoc: last ||= size - 1 last = [last, 0].max raise UserError, 'first must be <= last' unless first <= last + rows = [] unless columns.empty? first.upto(last) do |rnum| row = {} columns.each do |col| @@ -491,16 +501,16 @@ def degroup! @boundaries = [] self end - # Mark a group boundary at row +k+, and if +k+ is +nil+, mark the last row - # in the table as a group boundary. This is mainly used for internal + # Mark a group boundary at row +row+, and if +row+ is +nil+, mark the last + # row in the table as a group boundary. This is mainly used for internal # purposes. - def mark_boundary(k = nil) # :nodoc: - if k - boundaries.push(k) + def mark_boundary(row = nil) # :nodoc: + if row + boundaries.push(row) else boundaries.push(size - 1) end end @@ -522,24 +532,25 @@ # #union_all method. def append_boundaries(bounds, shift: 0) @boundaries += bounds.map { |k| k + shift } end - # Return the group number to which row k belongs. Groups, from the user's - # point of view are indexed starting at 1. - def row_index_to_group_index(k) + # Return the group number to which row ~row~ belongs. Groups, from the + # user's point of view are indexed starting at 1. + def row_index_to_group_index(row) boundaries.each_with_index do |b_last, g_num| - return (g_num + 1) if k <= b_last + return (g_num + 1) if row <= b_last end 1 end - def group_rows(k) # :nodoc: + def group_rows(row) # :nodoc: normalize_boundaries - return [] unless k < boundaries.size - first = k.zero? ? 0 : boundaries[k - 1] + 1 - last = boundaries[k] + return [] unless row < boundaries.size + + first = row.zero? ? 0 : boundaries[row - 1] + 1 + last = boundaries[row] rows_range(first, last) end # :startdoc: @@ -700,19 +711,21 @@ new_row = {} cols.each do |k| h = k.as_sym 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, expr| key = key.as_sym vars = old_row.merge(new_row) case expr when Symbol msg = "Column '#{expr}' in select does not exist" - raise UserError, msg unless vars.keys.include?(expr) + raise UserError, msg unless vars.key?(expr) + new_row[key] = vars[expr] when String new_row[key] = ev.evaluate(expr, locals: vars) else msg = "Hash parameter '#{key}' to select must be a symbol or string" @@ -862,14 +875,14 @@ set_operation(other, :difference, distinct: false) end private - # Apply the set operation given by op between this table and the other table - # given in the first argument. If distinct is true, eliminate duplicates - # from the result. - def set_operation(other, op = :+, + # Apply the set operation given by ~oper~ between this table and the other + # table given in the first argument. If distinct is true, eliminate + # duplicates from the result. + def set_operation(other, oper = :+, distinct: true, add_boundaries: true, inherit_boundaries: false) unless columns.size == other.columns.size msg = "can't apply set ops to tables with a different number of columns" @@ -879,11 +892,11 @@ 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 = rows.send(oper, other_rows) new_rows.each_with_index do |row, k| result << row result.mark_boundary if k == size - 1 && add_boundaries end if inherit_boundaries @@ -973,10 +986,11 @@ raise UserError, 'need other table as first argument to join' end unless JOIN_TYPES.include?(join_type) 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_exp, other_common_heads = build_join_expression(exps, other, join_type) @@ -990,25 +1004,28 @@ # 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_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 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 %i[right full].include?(join_type) other_rows.each_with_index do |other_row, k| next if other_row_matches[k] + result << build_out_row(row_a: self_row_nils, row_b: other_row, type: join_type) end end @@ -1088,10 +1105,11 @@ # the expression will be evaluated in the context of a binding in which the # local variables are all the headers in the self table with '_a' appended # and all the headers in the other table with '_b' appended. def build_join_expression(exps, other, type) return ['true', []] if type == :cross + a_heads = headers b_heads = other.headers common_heads = a_heads & b_heads b_common_heads = [] if exps.empty? @@ -1118,10 +1136,11 @@ when /\A(.*)_a\z/ a_head = $1.to_sym 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) @@ -1136,10 +1155,11 @@ when /\A(.*)_b\z/ b_head = $1.to_sym 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) @@ -1279,10 +1299,11 @@ # Add a FatTable::Column object +col+ to the table. def add_column(col) msg = "Table already has a column with header '#{col.header}'" raise msg if column?(col.header) + columns << col self end ############################################################################ @@ -1327,9 +1348,10 @@ # def to_any(fmt_type, options = {}) fmt = fmt_type.as_sym 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