Class: Ruport::Data::Table

Inherits:
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)


265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/ruport/data/table.rb', line 265

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]).



868
869
870
871
872
# File 'lib/ruport/data/table.rb', line 868

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



296
297
298
# File 'lib/ruport/data/table.rb', line 296

def column_names
  @column_names
end

#dataObject (readonly)

This Table’s data



299
300
301
# File 'lib/ruport/data/table.rb', line 299

def data
  @data
end

Class Method Details

.inherited(base) ⇒ Object

:nodoc:



240
241
242
# File 'lib/ruport/data/table.rb', line 240

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)


387
388
389
390
391
392
# File 'lib/ruport/data/table.rb', line 387

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]


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

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("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 }


457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/ruport/data/table.rb', line 457

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("a","b") { |t| t << [1,2] << [3,4] }

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


484
485
486
487
488
489
490
491
# File 'lib/ruport/data/table.rb', line 484

def add_columns(names,options={})     
  raise "Greg isn't smart enough to figure this out.\n"+
        "Send ideas in at http://list.rubyreports.org" if block_given?
  need_reverse = !!(options[:after] || options[:position])
  names = names.reverse if need_reverse
  names.each { |n| add_column(n,options) } 
  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]


688
689
690
691
692
693
694
695
696
697
698
699
# File 'lib/ruport/data/table.rb', line 688

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)


345
346
347
# File 'lib/ruport/data/table.rb', line 345

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

#feed_element(row) ⇒ Object



874
875
876
# File 'lib/ruport/data/table.rb', line 874

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


827
828
829
830
831
832
# File 'lib/ruport/data/table.rb', line 827

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.

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 |
+---------------+


141
142
143
144
145
146
147
148
149
# File 'lib/ruport/data/table.rb', line 141

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.



369
370
371
# File 'lib/ruport/data/table.rb', line 369

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.



672
673
674
675
676
677
# File 'lib/ruport/data/table.rb', line 672

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


500
501
502
503
504
# File 'lib/ruport/data/table.rb', line 500

def remove_column(col)
  col = column_names[col] if col.kind_of? Fixnum
  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()



513
514
515
516
# File 'lib/ruport/data/table.rb', line 513

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


528
529
530
531
532
# File 'lib/ruport/data/table.rb', line 528

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)


547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
# File 'lib/ruport/data/table.rb', line 547

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)


411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/ruport/data/table.rb', line 411

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(%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 |
   +------------+


618
619
620
621
622
623
624
625
# File 'lib/ruport/data/table.rb', line 618

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

#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]]


809
810
811
812
813
814
815
816
817
# File 'lib/ruport/data/table.rb', line 809

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


716
717
718
719
720
721
722
723
724
725
726
727
728
# File 'lib/ruport/data/table.rb', line 716

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)


761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
# File 'lib/ruport/data/table.rb', line 761

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.



793
794
795
796
# File 'lib/ruport/data/table.rb', line 793

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


657
658
659
660
661
662
663
664
665
666
667
# File 'lib/ruport/data/table.rb', line 657

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(%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 |
     +-----------+


590
591
592
593
594
595
596
597
598
599
600
# File 'lib/ruport/data/table.rb', line 590

def swap_column(a,b)    
  if [a,b].all? { |r| r.kind_of? Fixnum }
   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")


852
853
854
855
856
857
# File 'lib/ruport/data/table.rb', line 852

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


842
843
844
# File 'lib/ruport/data/table.rb', line 842

def to_s
  as(:text)
end