Class: Ruport::Data::Table

Inherits:
Object
  • Object
show all
Extended by:
Forwardable, FromCSV
Includes:
Enumerable, Controller::Hooks
Defined in:
lib/ruport/data/table.rb

Overview

Overview

This class is one of the core classes for building and working with data in Ruport. The idea is to get your data into a standard form, regardless of its source (a database, manual arrays, ActiveRecord, CSVs, etc.).

Table is intended to be used as the data store for structured, tabular data.

Once your data is in a Table object, it can be manipulated to suit your needs, then used to build a report.

Direct Known Subclasses

Group

Defined Under Namespace

Modules: FromCSV Classes: Pivot

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from FromCSV

load, parse

Methods included from Controller::Hooks

#as, included, #save_as

Constructor Details

#initialize(options = {}) {|feeder| ... } ⇒ Table

Creates a new table based on the supplied options.

Valid options:

:data

An Array of Arrays representing the records in this Table.

:column_names

An Array containing the column names for this Table.

:filters

A proc or array of procs that set up conditions to filter the data being added to the table.

:transforms

A proc or array of procs that perform transformations on the data being added to the table.

:record_class

Specify the class of the table’s records.

Example:

table = Table.new :data => [[1,2,3], [3,4,5]],
                  :column_names => %w[a b c]

Yields:

  • (feeder)


333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/ruport/data/table.rb', line 333

def initialize(options={})
  @column_names = options[:column_names] ? options[:column_names].dup : []
  @record_class = options[:record_class] &&
                  options[:record_class].name || "Ruport::Data::Record"
  @data         = []

  feeder = Feeder.new(self)

  Array(options[:filters]).each { |f| feeder.filter(&f) }
  Array(options[:transforms]).each { |t| feeder.transform(&t) }

  if options[:data]
    options[:data].each do |e|
      if e.kind_of?(Record)
        e = if @column_names.empty? or
               e.attributes.all? { |a| a.kind_of?(Numeric) }
          e.to_a
        else
          e.to_hash.values_at(*@column_names)
        end
      end
      r = recordize(e)

      feeder << r
    end
  end

  yield(feeder) if block_given?
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(id, *args, &block) ⇒ Object

Provides a shortcut for the as() method by converting a call to as(:format_name) into a call to to_format_name

Also converts a call to rows_with_columnname to a call to rows_with(:columnname => args[0]).



983
984
985
986
987
# File 'lib/ruport/data/table.rb', line 983

def method_missing(id,*args,&block)
 return as($1.to_sym,*args,&block) if id.to_s =~ /^to_(.*)/
 return rows_with($1.to_sym => args[0]) if id.to_s =~ /^rows_with_(.*)/
 super
end

Instance Attribute Details

#column_namesObject

This Table’s column names



364
365
366
# File 'lib/ruport/data/table.rb', line 364

def column_names
  @column_names
end

#dataObject (readonly)

This Table’s data



367
368
369
# File 'lib/ruport/data/table.rb', line 367

def data
  @data
end

Class Method Details

.inherited(base) ⇒ Object

:nodoc:



308
309
310
# File 'lib/ruport/data/table.rb', line 308

def self.inherited(base) #:nodoc:
  base.renders_as_table
end

Instance Method Details

#+(other) ⇒ Object

Used to merge two Tables by rows. Raises an ArgumentError if the Tables don’t have identical columns.

Example:

inky = Table.new :data => [[1,2], [3,4]],
                 :column_names => %w[a b]

blinky = Table.new :data => [[5,6]],
                   :column_names => %w[a b]

sue = inky + blinky
sue.data #=> [[1,2],[3,4],[5,6]]

Raises:

  • (ArgumentError)


465
466
467
468
469
470
# File 'lib/ruport/data/table.rb', line 465

def +(other)
  raise ArgumentError unless other.column_names == @column_names
  self.class.new( :column_names => @column_names,
                  :data => @data + other.data,
                  :record_class => record_class )
end

#<<(row) ⇒ Object

Used to add extra data to the Table. row can be an Array, Hash or Record. It also can be anything that implements a meaningful to_hash or to_ary.

Example:

data = Table.new :data => [[1,2], [3,4]],
                 :column_names => %w[a b]
data << [8,9]
data << { :a => 4, :b => 5}
data << Record.new [5,6], :attributes => %w[a b]


431
432
433
434
# File 'lib/ruport/data/table.rb', line 431

def <<(row)
  @data << recordize(row)
  return self
end

#add_column(name, options = {}) ⇒ Object

Adds an extra column to the Table.

Available Options:

:default

The default value to use for the column in existing rows. Set to nil if not specified.

:position

Inserts the column at the indicated position number.

:before

Inserts the new column before the column indicated (by name).

:after

Inserts the new column after the column indicated (by name).

If a block is provided, it will be used to build up the column.

Example:

data = Table.new("a","b") { |t| t << [1,2] << [3,4] }

# basic usage, column full of 1's
data.add_column 'new_column', :default => 1

# new empty column before new_column
data.add_column 'new_col2', :before => 'new_column'

# new column placed just after column a
data.add_column 'new_col3', :position => 1

# new column built via a block, added at the end of the table
data.add_column("new_col4") { |r| r.a + r.b }


535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
# File 'lib/ruport/data/table.rb', line 535

def add_column(name,options={})
  if pos = options[:position]
    column_names.insert(pos,name)
  elsif pos = options[:after]
    column_names.insert(column_names.index(pos)+1,name)
  elsif pos = options[:before]
    column_names.insert(column_names.index(pos),name)
  else
    column_names << name
  end

  if block_given?
    each { |r| r[name] = yield(r) || options[:default] }
  else
    each { |r| r[name] = options[:default] }
  end; self
end

#add_columns(names, options = {}) ⇒ Object

Add multiple extra columns to the Table. See add_column for a list of available options.

Example:

data = Table.new("a","b") { |t| t << [1,2] << [3,4] }

data.add_columns ['new_column_1','new_column_2'], :default => 1


562
563
564
565
566
567
568
569
# File 'lib/ruport/data/table.rb', line 562

def add_columns(names,options={})
  raise "Greg isn't smart enough to figure this out.\n" \
        "Send ideas in at github" if block_given?
  need_reverse = !!(options[:after] || options[:position])
  names = names.reverse if need_reverse
  names.each { |n| add_column(n,options) }
  self
end

#add_row(row_data, options = {}) ⇒ Object

Add a row to a certain location within the existing table.

data.add_row(, :position => 0)



441
442
443
444
# File 'lib/ruport/data/table.rb', line 441

def add_row(row_data, options={})
  @data.insert(options[:position] || @data.length, recordize(row_data))
  return self
end

#column(name) ⇒ Object

Returns an array of values for the given column name.

Example:

table = [[1,2],[3,4],[5,6]].to_table(%w[col1 col2])
table.column("col1")   #=> [1,3,5]


766
767
768
769
770
771
772
773
774
775
776
777
# File 'lib/ruport/data/table.rb', line 766

def column(name)
  case(name)
  when Integer
    unless column_names.empty?
      raise ArgumentError if name > column_names.length
    end
  else
    raise ArgumentError unless column_names.include?(name)
  end

  map { |r| r[name] }
end

#eql?(other) ⇒ Boolean Also known as: ==

Compares this Table to another Table and returns true if both the data and column_names are equal.

Example:

one = Table.new :data => [[1,2], [3,4]],
                :column_names => %w[a b]

two = Table.new :data => [[1,2], [3,4]],
                :column_names => %w[a b]

one.eql?(two) #=> true

Returns:

  • (Boolean)


413
414
415
# File 'lib/ruport/data/table.rb', line 413

def eql?(other)
  data.eql?(other.data) && column_names.eql?(other.column_names)
end

#feed_element(row) ⇒ Object



989
990
991
# File 'lib/ruport/data/table.rb', line 989

def feed_element(row)
   recordize(row)
end

#initialize_copy(from) ⇒ Object

Create a copy of the Table. Records will be copied as well.

Example:

one = Table.new :data => [[1,2], [3,4]],
                :column_names => %w[a b]
two = one.dup


942
943
944
945
946
947
# File 'lib/ruport/data/table.rb', line 942

def initialize_copy(from)
  @record_class = from.record_class.name
  @column_names = from.column_names.dup
  @data = []
  from.data.each { |r| self << r.dup }
end

#pivot(pivot_column, options = {}) ⇒ Object

Creates a new table with values from the specified pivot column transformed into columns.

Required options:

:group_by

The name of a column whose unique values should become rows in the new table.

:values

The name of a column that should supply the values for the pivoted columns.

Optional:

:pivot_order

An ordering specification for the pivoted columns, in terms of the source rows. If this is a Proc there is an optional second argument that receives the name of the pivot column, which due to implementation oddity currently is removed from the row provided in the first argument. This wart will likely be fixed in a future version.

:operation

The operation to perform on :values column. Supported operations are :first, :sum, :count, :mean, :min, and :max. If not specified, the default is :first.

Example:

Given a table my_table:

+-------------------------+
| Group | Segment | Value |
+-------------------------+
|   A   |    1    |   0   |
|   A   |    2    |   1   |
|   B   |    1    |   2   |
|   B   |    2    |   3   |
+-------------------------+

Pivoting the table on the Segment column:

my_table.pivot('Segment', :group_by => 'Group', :values => 'Value',
  :pivot_order => proc {|row, name| name})

Yields a new table like this:

+---------------+
| Group | 1 | 2 |
+---------------+
|   A   | 0 | 1 |
|   B   | 2 | 3 |
+---------------+


210
211
212
213
214
215
216
217
218
# File 'lib/ruport/data/table.rb', line 210

def pivot(pivot_column, options = {})
  group_column = options[:group_by] ||
    raise(ArgumentError, ":group_by option required")
  value_column = options[:values]   ||
    raise(ArgumentError, ":values option required")
  Pivot.new(
    self, group_column, pivot_column, value_column, options
  ).to_table
end

#record_classObject

Returns the record class constant being used by the table.



447
448
449
# File 'lib/ruport/data/table.rb', line 447

def record_class
  @record_class.split("::").inject(Class) { |c,el| c.send(:const_get,el) }
end

#reduce(columns = column_names, range = nil, &block) ⇒ Object Also known as: sub_table!

Generates a sub table in place, modifying the receiver. See documentation for sub_table.



750
751
752
753
754
755
# File 'lib/ruport/data/table.rb', line 750

def reduce(columns=column_names,range=nil,&block)
  t = sub_table(columns,range,&block)
  @data = t.data
  @column_names = t.column_names
  self
end

#remove_column(col) ⇒ Object

Removes the given column from the table. May use name or position.

Example:

table.remove_column(0) #=> removes the first column
table.remove_column("apple") #=> removes column named apple


578
579
580
581
582
# File 'lib/ruport/data/table.rb', line 578

def remove_column(col)
  col = column_names[col] if col.kind_of? Integer
  column_names.delete(col)
  each { |r| r.send(:delete,col) }
end

#remove_columns(*cols) ⇒ Object

Removes multiple columns from the table. May use name or position Will autosplat arrays.

Example: table.remove_columns(‘a’,‘b’,‘c’) table.remove_columns()



591
592
593
594
# File 'lib/ruport/data/table.rb', line 591

def remove_columns(*cols)
  cols = cols[0] if cols[0].kind_of? Array
  cols.each { |col| remove_column(col) }
end

#rename_column(old_name, new_name) ⇒ Object

Renames a column. Will update Record attributes as well.

Example:

old_values = table.map { |r| r.a }
table.rename_column("a","zanzibar")
new_values = table.map { |r| r.zanzibar }
old_values == new_values #=> true
table.column_names.include?("a") #=> false


606
607
608
609
610
# File 'lib/ruport/data/table.rb', line 606

def rename_column(old_name,new_name)
  index = column_names.index(old_name) or return
  self.column_names[index] = new_name
  each { |r| r.rename_attribute(old_name,new_name,false)}
end

#rename_columns(old_cols = nil, new_cols = nil) ⇒ Object

Renames multiple columns. Takes either a hash of “old” => “new” names or two arrays of names %w[old names],%w[new names].

Example:

table.column_names #=> ["a", "b"]
table.rename_columns ["a", "b"], ["c", "d"]
table.column_names #=> ["c", "d"]

table.column_names #=> ["a", "b"]
table.rename_columns {"a" => "c", "b" => "d"}
table.column_names #=> ["c", "d"]

Raises:

  • (ArgumentError)


625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
# File 'lib/ruport/data/table.rb', line 625

def rename_columns(old_cols=nil,new_cols=nil)
  if block_given?
    if old_cols
      old_cols.each { |c| rename_column(c,yield(c)) }
    else
      column_names.each { |c| rename_column(c,yield(c)) }
    end
    return
  end

  raise ArgumentError unless old_cols

  if new_cols
    raise ArgumentError,
      "odd number of arguments" unless old_cols.size == new_cols.size
    h = Hash[*old_cols.zip(new_cols).flatten]
  else
    h = old_cols
  end
  h.each {|old,new| rename_column(old,new) }
end

#reorder(*indices) ⇒ Object

Allows you to change the order of, or reduce the number of columns in a Table.

Example:

a = Table.new :data => [[1,2,3],[4,5,6]], :column_names => %w[a b c]
a.reorder("b","c","a")
a.column_names #=> ["b","c","a"]

a = Table.new :data => [[1,2,3],[4,5,6]], :column_names => %w[a b c]
a.reorder(1,2,0)
a.column_names #=> ["b","c","a"]

a = Table.new :data => [[1,2,3],[4,5,6]], :column_names => %w[a b c]
a.reorder(0,2)
a.column_names #=> ["a","c"]

Raises:

  • (ArgumentError)


489
490
491
492
493
494
495
496
497
498
499
500
# File 'lib/ruport/data/table.rb', line 489

def reorder(*indices)
  raise(ArgumentError,"Can't reorder without column names set!") if
    @column_names.empty?

  indices = indices[0] if indices[0].kind_of? Array

  if indices.all? { |i| i.kind_of? Integer }
    indices.map! { |i| @column_names[i] }
  end

  reduce(indices)
end

#replace_column(old_col, new_col = nil, &block) ⇒ Object

Allows you to specify a new column to replace an existing column

in your table via a block.

Example:

>> a = Table.new(%w[a b c]) { |t| t << [1,2,3] << [4,5,6] }
>> a.replace_column("c","c2") { |r| r.c * 2 + r.a }

>> puts a
   +------------+
   | a | b | c2 |
   +------------+
   | 1 | 2 |  7 |
   | 4 | 5 | 16 |
   +------------+


696
697
698
699
700
701
702
703
# File 'lib/ruport/data/table.rb', line 696

def replace_column(old_col,new_col=nil,&block)
  if new_col
    add_column(new_col,:after => old_col,&block)
    remove_column(old_col)
  else
    each { |r| r[old_col] = yield(r) }
  end
end

#row_search(search, options = {}) ⇒ Object

Search row for a string and return the position

Example:

table = Table.new :data => [["Mow Lawn","50"], ["Sew","40"], ["Clean dishes","5"]],
                  :column_names => %w[task cost]
table.row_search("Sew", :column => 0)           #=> [[1,2,3], [1,4,6]]

Search for a number in column 0 greater than 999.
result = table.row_search(999, :column => 0, :greater_than => true)


909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
# File 'lib/ruport/data/table.rb', line 909

def row_search(search, options={})
  position = 0

  if column = options[:column]
    self.each do |row|

      if gt=options[:greater_than]
        return position if row[column] > search
      end

      if lt=options[:less_than]
        return position if row[column] < search
      end

      unless gt or lt
        if row[column] =~ /#{search}/       # Search for part of or whole search text.
          return position
        end
      end

      position += 1
    end
  end
end

#rows_with(columns, &block) ⇒ Object

Get an array of records from the Table limited by the criteria specified.

Example:

table = Table.new :data => [[1,2,3], [1,4,6], [4,5,6]],
                  :column_names => %w[a b c]
table.rows_with(:a => 1)           #=> [[1,2,3], [1,4,6]]
table.rows_with(:a => 1, :b => 4)  #=> [[1,4,6]]
table.rows_with_a(1)               #=> [[1,2,3], [1,4,6]]
table.rows_with(%w[a b]) {|a,b| [a,b] == [1,4] }  #=> [[1,4,6]]


887
888
889
890
891
892
893
894
895
# File 'lib/ruport/data/table.rb', line 887

def rows_with(columns,&block)
  select { |r|
    if block
      block[*(columns.map { |c| r.get(c) })]
    else
      columns.all? { |k,v| r.get(k) == v }
    end
  }
end

#sigma(column = nil) ⇒ Object Also known as: sum

Calculates sums. If a column name or index is given, it will try to convert each element of that column to an integer or float and add them together.

If a block is given, it yields each Record so that you can do your own calculation.

Example:

table = [[1,2],[3,4],[5,6]].to_table(%w[col1 col2])
table.sigma("col1") #=> 9
table.sigma(0)      #=> 9
table.sigma { |r| r.col1 + r.col2 } #=> 21
table.sigma { |r| r.col2 + 1 } #=> 15


794
795
796
797
798
799
800
801
802
803
804
805
806
# File 'lib/ruport/data/table.rb', line 794

def sigma(column=nil)
  inject(0) { |s,r|
    if column
      s + if r.get(column).kind_of? Numeric
        r.get(column)
      else
        r.get(column) =~ /\./ ? r.get(column).to_f : r.get(column).to_i
      end
    else
      s + yield(r)
    end
  }
end

#sort_rows_by(col_names = nil, options = {}, &block) ⇒ Object

Returns a sorted table. If col_names is specified, the block is ignored and the table is sorted by the named columns.

The second argument specifies sorting options. Currently only :order is supported. Default order is ascending, to sort decending use :order => :descending

Example:

table = [[4, 3], [2, 5], [7, 1]].to_table(%w[col1 col2 ])

# returns a new table sorted by col1
table.sort_rows_by {|r| r["col1"]}

# returns a new table sorted by col1, in descending order
table.sort_rows_by(nil, :order => :descending) {|r| r["col1"]}

# returns a new table sorted by col2
table.sort_rows_by(["col2"])

# returns a new table sorted by col2, descending order
table.sort_rows_by("col2", :order => :descending)

# returns a new table sorted by col1, then col2
table.sort_rows_by(["col1", "col2"])

# returns a new table sorted by col1, then col2, in descending order
table.sort_rows_by(["col1", "col2"], :order => descending)


839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
# File 'lib/ruport/data/table.rb', line 839

def sort_rows_by(col_names=nil, options={}, &block)
  # stabilizer is needed because of
  # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/170565
  stabilizer = 0

  nil_rows, sortable = partition do |r|
    Array(col_names).any? { |c| r[c].nil? }
  end

  data_array =
    if col_names
      sortable.sort_by do |r|
        stabilizer += 1
        [Array(col_names).map {|col| r[col]}, stabilizer]
      end
    else
      sortable.sort_by(&block)
    end

  data_array += nil_rows
  data_array.reverse! if options[:order] == :descending

  table = self.class.new( :data => data_array,
                          :column_names => @column_names,
                          :record_class => record_class )

  return table
end

#sort_rows_by!(col_names = nil, options = {}, &block) ⇒ Object

Same as Table#sort_rows_by, but self modifying. See sort_rows_by for documentation.



871
872
873
874
# File 'lib/ruport/data/table.rb', line 871

def sort_rows_by!(col_names=nil,options={},&block)
  table = sort_rows_by(col_names,options,&block)
  @data = table.data
end

#sub_table(cor = column_names, range = nil, &block) ⇒ Object

Generates a sub table

Examples:

  table = [[1,2,3,4],[5,6,7,8],[9,10,11,12]].to_table(%w[a b c d])

Using column_names and a range:

   sub_table = table.sub_table(%w[a b],1..-1)
   sub_table == [[5,6],[9,10]].to_table(%w[a b]) #=> true

Using just column_names:

   sub_table = table.sub_table(%w[a d])
   sub_table == [[1,4],[5,8],[9,12]].to_table(%w[a d]) #=> true

Using column_names and a block:

   sub_table = table.sub_table(%w[d b]) { |r| r.a < 6 }
   sub_table == [[4,2],[8,6]].to_table(%w[d b]) #=> true

Using a range for row reduction:
   sub_table = table.sub_table(1..-1)
   sub_table == [[5,6,7,8],[9,10,11,12]].to_table(%w[a b c d]) #=> true

Using just a block:

   sub_table = table.sub_table { |r| r.c > 10 }
   sub_table == [[9,10,11,12]].to_table(%w[a b c d]) #=> true


735
736
737
738
739
740
741
742
743
744
745
# File 'lib/ruport/data/table.rb', line 735

def sub_table(cor=column_names,range=nil,&block)
  if range
    self.class.new(:column_names => cor,:data => data[range])
  elsif cor.kind_of?(Range)
    self.class.new(:column_names => column_names,:data => data[cor])
  elsif block
    self.class.new( :column_names => cor, :data => data.select(&block))
  else
    self.class.new( :column_names => cor, :data => data)
  end
end

#swap_column(a, b) ⇒ Object

Exchanges one column with another.

Example:

  >> a = Table.new(%w[a b c]) { |t| t << [1,2,3] << [4,5,6] }
  >> puts a
     +-----------+
     | a | b | c |
     +-----------+
     | 1 | 2 | 3 |
     | 4 | 5 | 6 |
     +-----------+
  >> a.swap_column("a","c")
  >> puts a
     +-----------+
     | c | b | a |
     +-----------+
     | 3 | 2 | 1 |
     | 6 | 5 | 4 |
     +-----------+


668
669
670
671
672
673
674
675
676
677
678
# File 'lib/ruport/data/table.rb', line 668

def swap_column(a,b)
  if [a,b].all? { |r| r.kind_of? Integer }
   col_a,col_b = column_names[a],column_names[b]
   column_names[a] = col_b
   column_names[b] = col_a
  else
    a_ind, b_ind = [column_names.index(a), column_names.index(b)]
    column_names[b_ind] = a
    column_names[a_ind] = b
  end
end

#to_group(name = nil) ⇒ Object

Convert the Table into a Group using the supplied group name.

data = Table.new :data => [[1,2], [3,4]],
                 :column_names => %w[a b]
group = data.to_group("my_group")


967
968
969
970
971
972
# File 'lib/ruport/data/table.rb', line 967

def to_group(name=nil)
  Group.new( :data => data,
             :column_names => column_names,
             :name => name,
             :record_class => record_class )
end

#to_sObject

Uses Ruport’s built-in text formatter to render this Table into a String.

Example:

data = Table.new :data => [[1,2], [3,4]],
                 :column_names => %w[a b]
puts data.to_s


957
958
959
# File 'lib/ruport/data/table.rb', line 957

def to_s
  as(:text)
end