lib/fat_table/table.rb in fat_table-0.2.3 vs lib/fat_table/table.rb in fat_table-0.2.4

- old
+ new

@@ -56,74 +56,81 @@ ########################################################################### # Constructors ########################################################################### # :category: Constructors + # Return an empty FatTable::Table object. def initialize @columns = [] @boundaries = [] end # :category: Constructors - # Construct a Table from the contents of a CSV file. Headers will be taken - # from the first row and converted to symbols. + + # 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) File.open(fname, 'r') do |io| from_csv_io(io) end end # :category: Constructors - # Construct a Table from a string, treated as the input from a CSV file. + + # Construct a Table from a CSV string +str+, treated in the same manner as + # the input from a CSV file in ::from_org_file. def self.from_csv_string(str) from_csv_io(StringIO.new(str)) end # :category: Constructors # Construct a Table from the first table found in the given Emacs org-mode - # file. Headers are taken from the first row if the second row is an hrule. - # Otherwise, synthetic headers of the form +:col_1+, +:col_2+, etc. are - # created. + # file named +fname+. Headers are taken from the first row if the second row + # is an hrule. Otherwise, synthetic headers of the form +:col_1+, +:col_2+, + # etc. are created. def self.from_org_file(fname) File.open(fname, 'r') do |io| from_org_io(io) end end # :category: Constructors - # Construct a Table from a string, treated as the contents of an org-mode - # file. + + # Construct a Table from a string +str+, treated in the same manner as the + # contents of an org-mode file in ::from_org_file. def self.from_org_string(str) from_org_io(StringIO.new(str)) end # :category: Constructors - # Construct a new table from an array of arrays. By default, with +hlines+ - # false, do not look for separators, i.e. nil or a string of dashes, just - # treat the first row as headers. With +hlines+ true, expect 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 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. + # 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. def self.from_aoa(aoa, hlines: false) from_array_of_arrays(aoa, hlines: hlines) end # :category: Constructors - # Construct a Table from an array of hashes, or any objects that respond to - # the #to_h method. All hashes must have the same keys, which, when - # converted to symbols will become the headers for the Table. If hlines is - # set true, mark a group boundary whenever a nil, rather than a hash - # appears in the outer array. + # Construct a Table from +aoh+, an Array of Hashes or an Array of any + # objects that respond to the #to_h method. All hashes must have the same + # keys, which, when converted to symbols will become the headers for the + # Table. If hlines is set true, mark a group boundary whenever a nil, rather + # than a hash appears in the outer array. def self.from_aoh(aoh, hlines: false) if aoh.first.respond_to?(:to_h) from_array_of_hashes(aoh, hlines: hlines) else raise UserError, @@ -131,20 +138,20 @@ end end # :category: Constructors - # Construct a Table from another 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. Return the Table with the query results as rows. + # 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 @@ -279,29 +286,32 @@ ########################################################################### # Attributes ########################################################################### # :category: Attributes - # Return the Column with the given header. + + # Return the table's Column with the given +key+ as its header. def column(key) columns.detect { |c| c.header == key.as_sym } end # :category: Attributes - # Return the type of the Column with the given header + + # Return the type of the Column with the given +key+ as its + # header as a String. def type(key) column(key).type end # :category: Attributes # Return the array of items of the column with the given header symbol - # +key+, or if +key+ is an Integer, return that row number. So a table's - # rows can be accessed by number, and its columns can be accessed by column - # header. Also, double indexing works in either row-major or column-major - # order: \tab\[:id\]\[8\] returns the 9th item in the column headed :id and - # so does \tab\[8\]\[:id\]. + # +key+, or if +key+ is an Integer, return that row at that index. So a + # table's rows can be accessed by number, and its columns can be accessed by + # column header. Also, double indexing works in either row-major or + # 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) rows[key] @@ -323,22 +333,22 @@ headers.include?(key.as_sym) end # :category: Attributes - # Return a Hash of the Table's Column header symbols to types. + # Return a Hash of the Table's Column header symbols to type strings. def types result = {} columns.each do |c| result[c.header] = c.type end result end # :category: Attributes - # Return the headers for the Table as an array of symbols. + # Return the headers for the Table as an Array of Symbols. def headers columns.map(&:header) end # :category: Attributes @@ -459,11 +469,11 @@ # All the other table-transforming methods reset the boundaries in the new # table. For example, #where re-arranges and deletes rows, so the old # boundaries would make no sense anyway. Likewise, #union, #intersection, # #except, and #join reset the boundaries to their default. # - # Return an array of an array of row hashes for the groups in this Table. + # Return an array of an Array of row Hashes for the groups in this Table. def groups normalize_boundaries groups = [] (0..boundaries.size - 1).each do |k| groups << group_rows(k) @@ -480,12 +490,13 @@ def degroup! @boundaries = [] self end - # Mark a boundary at k, and if k is nil, mark the last row in the table as a - # group boundary. This is used for internal purposes. + # 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 + # purposes. def mark_boundary(k = nil) # :nodoc: if k boundaries.push(k) else boundaries.push(size - 1) @@ -549,15 +560,15 @@ public # :category: Operators # Return a new Table sorting the rows of this Table on the possibly multiple - # keys given in the array of syms in headers. Append a ! to the symbol name - # to indicate reverse sorting on that column. + # keys given in +sort_heads+ as an Array of Symbols. Append a ! to the + # symbol name to indicate reverse sorting on that column. # # tab.order_by(:ref, :date) => sorted table - # tab.order_by(:date!) => reverse sort on :date + # tab.order_by(:date!) => reverse sort on :date # # After sorting, the output Table will have group boundaries added after # each row where the sort key changes. def order_by(*sort_heads) sort_heads = [sort_heads].flatten @@ -586,27 +597,29 @@ # :category: Operators # Return a Table having the selected column expressions. Each expression can # be either a # - # 1. a symbol, +:old_col+, representing a column in the current table, + # 1. in +cols+, a symbol, +:old_col+, representing a column in the current + # table, # - # 2. a hash of +new_col: :old_col+ to rename an existing +:old_col+ column as - # +:new_col+, or + # 2. a hash in +new_cols+ of the form +new_col: :old_col+ to rename an + # existing +:old_col+ column as +:new_col+, or # - # 3. a hash of +new_col: 'expression'+, to add a new column that is computed - # as an arbitrary ruby expression of the existing columns (whether - # selected for the output table or not) or any new_col defined earlier in - # the argument list defined as local variables in the expression. 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. + # 3. a hash in +new_cols+ of the form +new_col: 'expression'+, to add a new + # column +new_col+ that is computed as an arbitrary ruby expression in + # which there are local variables bound to the names of existing columns + # (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. # - # The bare symbol arguments (1) must precede any hash arguments (2) or (3). - # Each expression results in a column in the resulting Table in the order - # given. The expressions are evaluated in left-to-right order as well. The - # output table preserves any groups present in the input table. + # 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 @@ -674,11 +687,11 @@ result end # :category: Operators - # Return this table with all duplicate rows eliminated. Resets groups. Same + # Return a new table with all duplicate rows eliminated. Resets groups. Same # as #uniq. def distinct result = Table.new uniq_rows = rows.uniq uniq_rows.each do |row| @@ -695,16 +708,16 @@ distinct end # :category: Operators - # Return a Table that combines this table with +other+ table. In other - # words, return the union of this table with the other. The headers of this - # table are used in the result. There must be the same number of columns of - # the same type in the two tables, or an exception will be thrown. - # Duplicates are eliminated from the result. Any groups present in either - # Table are eliminated in the output Table. + # Return a Table that combines this table with +other+ table, i.e., return + # the union of this table with the other. The headers of this table are used + # in the result. There must be the same number of columns of the same type + # in the two tables, otherwise an exception will be raised. Duplicates are + # eliminated from the result. Any groups present in either Table are + # eliminated in the output Table. def union(other) set_operation(other, :+, distinct: true, add_boundaries: true) end @@ -751,29 +764,29 @@ end # :category: Operators # Return a Table that includes the rows of this table except for any rows - # that are the same as those in another table. In other words, return the - # set difference between this table an the other. The headers of this table + # that are the same as those in Table +other+. In other words, return the + # set difference between this table and +other+. The headers of this table # are used in the result. There must be the same number of columns of the - # same type in the two tables, or an exception will be thrown. Duplicates + # same type in the two tables, or an exception will be raised. Duplicates # are eliminated from the result. Any groups present in either Table are # eliminated in the output Table. def except(other) set_operation(other, :difference, distinct: true) end # :category: Operators # Return a Table that includes the rows of this table except for any rows - # that are the same as those in +other+ Table. In other words, return the + # that are the same as those in Table +other+. In other words, return the # set difference between this table an the other. The headers of this table # are used in the result. There must be the same number of columns of the # same type in the two tables, or an exception will be thrown. Duplicates - # are not eliminated from the result. Any groups present in either Table are - # eliminated in the output Table. + # are /not/ eliminated from the result. Any groups present in either Table + # are eliminated in the output Table. def except_all(other) set_operation(other, :difference, distinct: false) end private @@ -812,12 +825,14 @@ # An Array of symbols for the valid join types. JOIN_TYPES = [:inner, :left, :right, :full, :cross].freeze # :category: Operators # - # Return a table that joins this table to another based on one or more join - # expressions. There are several possibilities for the join expressions: + # 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 + # result table. There are several possible forms for the join expressions + # +exps+: # # 1. If no join expressions are given, the tables will be joined when all # values with the same name in both tables have the same value, a # "natural" join. However, if the join type is :cross, the join # expression will be taken to be 'true'. Otherwise, if there are no @@ -874,12 +889,12 @@ # Cartesian product), the joined table will contain a row # consisting of all columns in T1 followed by all columns in T2. If # the tables have N and M rows respectively, the joined table will # have N * M rows. # - # Any groups present in either Table are eliminated in the output Table. - # See the README for examples. + # Any groups present in either Table are eliminated in the output Table. See + # the README for examples. def join(other, *exps, join_type: :inner) unless other.is_a?(Table) raise UserError, 'need other table as first argument to join' end unless JOIN_TYPES.include?(join_type) @@ -925,35 +940,40 @@ result.normalize_boundaries result end # :category: Operators - # Perform an inner join as described in FatTable::Table.join. + + # Perform an inner join as described in FatTable::Table#join. def inner_join(other, *exps) join(other, *exps) end # :category: Operators - # Perform a left join as described in FatTable::Table.join. + + # Perform a left join as described in FatTable::Table#join. def left_join(other, *exps) join(other, *exps, join_type: :left) end # :category: Operators - # Perform a right join as described in FatTable::Table.join. + + # Perform a right join as described in FatTable::Table#join. def right_join(other, *exps) join(other, *exps, join_type: :right) end # :category: Operators - # Perform a full join as described in FatTable::Table.join. + + # Perform a full join as described in FatTable::Table#join. def full_join(other, *exps) join(other, *exps, join_type: :full) end # :category: Operators - # Perform a cross join as described in FatTable::Table.join. + + # Perform a cross join as described in FatTable::Table#join. def cross_join(other) join(other, join_type: :cross) end private @@ -1097,27 +1117,28 @@ ################################################################################### public # :category: Operators + # Return a Table with a single row for each group of rows in the input table - # where the value of all columns named as simple symbols are equal. All - # other columns are set to the result of aggregating the values of that - # column within the group according to a aggregate function (:count, :sum, - # :min, :max, etc.) that you can specify by adding a hash parameter with the - # column as the key and a symbol for the aggregate function as the value. - # For example, consider the following call: + # where the value of all columns +group_cols+ named as simple symbols are + # equal. All other columns, +agg_cols+, are set to the result of aggregating + # the values of that column within the group according to a aggregate + # function (:count, :sum, :min, :max, etc.) that you can specify by adding a + # hash parameter with the column as the key and a symbol for the aggregate + # function as the value. For example, consider the following call: # # tab.group_by(:date, :code, :price, shares: :sum). # - # The first three parameters are simple symbols, so the table is divided - # into groups of rows in which the value of :date, :code, and :price are - # equal. The shares: hash parameter is set to the aggregate function :sum, - # so it will appear in the result as the sum of all the :shares values in - # each group. Because of the way Ruby parses parameters to a method call, - # all the grouping symbols must appear first in the parameter list before - # any hash parameters. + # The first three parameters are simple symbols and count as +group_cols+, + # so the table is divided into groups of rows in which the value of :date, + # :code, and :price are equal. The shares: hash parameter is an +agg_col+ + # parameter set to the aggregate function :sum, so it will appear in the + # result as the sum of all the :shares values in each group. Because of the + # way Ruby parses parameters to a method call, all the grouping symbols must + # appear first in the parameter list before any hash parameters. 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 @@ -1150,13 +1171,14 @@ ############################################################################ public # :category: Constructors - # Add a row represented by a Hash having the headers as keys. If mark is - # true, mark this row as a boundary. All tables should be built ultimately - # using this method as a primitive. + + # Add a +row+ represented by a Hash having the headers as keys. If +mark:+ + # is set true, mark this row as a boundary. All tables should be built + # ultimately using this method as a primitive. def add_row(row, mark: false) row.each_pair do |k, v| key = k.as_sym columns << Column.new(header: k) unless column?(k) column(key) << v @@ -1164,16 +1186,18 @@ @boundaries << (size - 1) if mark self end # :category: Constructors - # Add a row without marking. + + # Add a +row+ without marking it as a group boundary. def <<(row) add_row(row) end # :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) columns << col self @@ -1191,16 +1215,16 @@ # last method in a chain of Table operations. # :category: Output # Return a string or ruby object according to the format specified in - # FatTable.format. If a block is given, it will yield a Formatter of the - # appropriate type to which format and footers can be applied. Otherwise, the - # default format for the type will be used. + # FatTable.format, passing the +options+ on to the Formatter. If a block is + # given, it will yield a Formatter of the appropriate type to which format + # and footers can be applied. Otherwise, the default format for the type + # will be used. # - # :call-seq: - # to_format(options = {}) { |fmt| ... } + # :call-seq: to_format(options = {}) { |fmt| ... } # def to_format(options = {}) if block_given? to_any(FatTable.format, self, options, &Proc.new) else @@ -1209,15 +1233,15 @@ end # :category: Output # Return a string or ruby object according to the format type +fmt_type+ - # given in the first argument. Valid format types are :psv, :aoa, :aoh, - # :latex, :org, :term, :text, or their string equivalents. If a block is - # given, it will yield a Formatter of the appropriate type to which format - # and footers can be applied. Otherwise, the default format for the type - # will be used. + # given in the first argument, passing the +options+ on to the Formatter. + # Valid format types are :psv, :aoa, :aoh, :latex, :org, :term, :text, or + # their string equivalents. If a block is given, it will yield a Formatter + # of the appropriate type to which format and footers can be applied. + # Otherwise, the default format for the type will be used. # # :call-seq: to_any(fmt_type, options = {}) { |fmt| ... } # def to_any(fmt_type, options = {}) fmt = fmt_type.as_sym @@ -1230,90 +1254,96 @@ end end # :category: Output - # Return the table as a string formatted as a pipe-separated values. If no - # block is given, default formatting is applies to the table's cells. If a - # block is given, it yields a Formatter to the block to which formatting - # instructions and footers can be added by calling methods on it. Since the - # pipe-separated format is the default format for Formatter, there is no - # class PsvFormatter as you might expect. + # Return the table as a string formatted as a pipe-separated values, passing + # the +options+ on to the Formatter. If no block is given, default + # formatting is applies to the table's cells. If a block is given, it yields + # a Formatter to the block to which formatting instructions and footers can + # be added by calling methods on it. Since the pipe-separated format is the + # default format for Formatter, there is no class PsvFormatter as you might + # expect. def to_psv(options = {}) fmt = Formatter.new(self, options) yield fmt if block_given? fmt.output end # :category: Output - # Return the table as an Array of Array of Strings. If no block is given, - # default formatting is applies to the table's cells. If a block is given, - # it yields an AoaFormatter to the block to which formatting instructions - # and footers can be added by calling methods on it. + # Return the table as an Array of Array of Strings, passing the +options+ on + # to the AoaFormatter. If no block is given, default formatting is applies + # to the table's cells. If a block is given, it yields an AoaFormatter to + # the block to which formatting instructions and footers can be added by + # calling methods on it. def to_aoa(options = {}) fmt = FatTable::AoaFormatter.new(self, options) yield fmt if block_given? fmt.output end # :category: Output - # Return the table as an Array of Hashes. Each inner hash uses the Table's - # columns as keys and it values are strings representing the cells of the - # table. If no block is given, default formatting is applies to the table's - # cells. If a block is given, it yields an AohFormatter to the block to - # which formatting instructions and footers can be added by calling methods - # on it. + # Return the table as an Array of Hashes, passing the +options+ on to the + # AohFormatter. Each inner hash uses the Table's columns as keys and it + # values are strings representing the cells of the table. If no block is + # given, default formatting is applies to the table's cells. If a block is + # given, it yields an AohFormatter to the block to which formatting + # instructions and footers can be added by calling methods on it. def to_aoh(options = {}) fmt = AohFormatter.new(self, options) yield fmt if block_given? fmt.output end # :category: Output - # Return the table as a string containing a LaTeX table. If no block is - # given, default formatting applies to the table's cells. If a block is - # given, it yields a LaTeXFormatter to the block to which formatting - # instructions and footers can be added by calling methods on it. + # Return the table as a string containing a LaTeX table, passing the + # +options+ on to the LaTeXFormatter. If no block is given, default + # formatting applies to the table's cells. If a block is given, it yields a + # LaTeXFormatter to the block to which formatting instructions and footers + # can be added by calling methods on it. def to_latex(options = {}) fmt = LaTeXFormatter.new(self, options) yield fmt if block_given? fmt.output end # :category: Output - # Return the table as a string containing an Emacs org-mode table. If no - # block is given, default formatting applies to the table's cells. If a - # block is given, it yields a OrgFormatter to the block to which formatting - # instructions and footers can be added by calling methods on it. + # Return the table as a string containing an Emacs org-mode table, passing + # the +options+ on to the OrgFormatter. If no block is given, default + # formatting applies to the table's cells. If a block is given, it yields a + # OrgFormatter to the block to which formatting instructions and footers can + # be added by calling methods on it. def to_org(options = {}) fmt = OrgFormatter.new(self, options) yield fmt if block_given? fmt.output end # :category: Output # Return the table as a string containing ANSI terminal text representing - # table. If no block is given, default formatting applies to the table's - # cells. If a block is given, it yields a TermFormatter to the block to - # which formatting instructions and footers can be added by calling methods - # on it. + # table, passing the +options+ on to the TermFormatter. If no block is + # given, default formatting applies to the table's cells. If a block is + # given, it yields a TermFormatter to the block to which formatting + # instructions and footers can be added by calling methods on it. def to_term(options = {}) fmt = TermFormatter.new(self, options) yield fmt if block_given? fmt.output end # :category: Output - # Return the table as a string containing ordinary text representing table. - # If no block is given, default formatting applies to the table's cells. If - # a block is given, it yields a TextFormatter to the block to which - # formatting instructions and footers can be added by calling methods on it. + # Return the table as a string containing ordinary text representing table, + # passing the +options+ on to the TextFormatter. If no block is given, + # default formatting applies to the table's cells. If a block is given, it + # yields a TextFormatter to the block to which formatting instructions and + # footers can be added by calling methods on it. + # @return [String] def to_text(options = {}) fmt = TextFormatter.new(self, options) yield fmt if block_given? fmt.output end