lib/table_fu.rb in table_fu-0.3.0 vs lib/table_fu.rb in table_fu-0.3.1

- old
+ new

@@ -1,20 +1,24 @@ -require 'rubygems' -require 'fastercsv' +if RUBY_VERSION > "1.9" + require 'csv' + ::FasterCSV = CSV +else + require 'fastercsv' +end # TableFu turns a matric array(from a csv file for example) into a spreadsheet. # # Allows formatting, macros, sorting, and faceting. # # Documentation: # http://propublica.github.com/table-fu class TableFu - + attr_reader :deleted_rows, :table, :totals, :column_headers attr_accessor :faceted_on, :col_opts - + # Should be initialized with a matrix array or a string containing a csv, and expects the first # array in the matrix to be column headers. def initialize(table, column_opts = {}) # Assume if we're past a string or filehandle we need to parse a csv if table.is_a?(String) || table.is_a?(File) @@ -26,26 +30,26 @@ @col_opts = column_opts yield self if block_given? end - + # Pass it an array and it will delete it from the table, but save the data in # @deleted_rows@ for later perusal. - # - # Returns: + # + # Returns: # nothing def delete_rows!(arr) @deleted_rows ||= [] arr.map do |item| @deleted_rows << @table[item] #account for header and 0 index @table[item] = nil end @table.compact! end - - + + # Inverse slice: Only keep the rows in the range after sorting def only!(range) rows_to_exclude = rows.map do |row| range.include?(row.row_num) ? nil : row.row_num end @@ -54,80 +58,80 @@ # Returns a Row object for the row at a certain index def row_at(row_num) TableFu::Row.new(@table[row_num], row_num, self) end - + # Returns all the Row objects for this object as a collection def rows all_rows = [] @table.each_with_index do |row, index| all_rows << TableFu::Row.new(row, index, self) end all_rows.sort end - + # Return the headers defined in column headers or cherry picked from @col_opts def columns @col_opts[:columns] || column_headers end - - # Return the headers of the array + + # Return the headers of the array def headers all_columns = [] columns.each do |header| all_columns << TableFu::Header.new(header, header, nil, self) end all_columns end - + # Sum the values of a particular column def sum_totals_for(column) @totals[column.to_s] = rows.inject(0) { |sum, r| to_numeric(r.datum_for(column).value) + sum } end - + # Sum the values of a particular column and return a Datum def total_for(column) sum_totals_for(column) Datum.new(@totals[column.to_s], column, nil, self) end - + # Return an array of TableFu instances grouped by a column. def faceted_by(column, opts = {}) faceted_spreadsheets = {} rows.each do |row| unless row.column_for(column).value.nil? faceted_spreadsheets[row.column_for(column).value] ||= [] faceted_spreadsheets[row.column_for(column).value] << row end end - + # Create new table_fu instances for each facet tables = [] faceted_spreadsheets.each do |key,value| new_table = [@column_headers] + value table = TableFu.new(new_table) table.faceted_on = key table.col_opts = @col_opts #formatting should be carried through tables << table end - + tables.sort! do |a,b| a.faceted_on <=> b.faceted_on end - + if opts[:total] opts[:total].each do |c| tables.each do |f| f.sum_totals_for(c) end end end - + tables end - + # Return a numeric instance for a string number, or if it's a string we # return 1, this way if we total up a series of strings it's a count def to_numeric(num) if num.nil? 0 @@ -135,138 +139,138 @@ num else 1 # We count each instance of a string this way end end - + # Return true if this table is faceted def faceted? not faceted_on.nil? end - + # Return the sorted_by column def sorted_by @col_opts[:sorted_by] end - + # Set the sorted_by column def sorted_by=(header) @col_opts[:sorted_by] = header end - + # Return the formatting hash def formatting @col_opts[:formatting] end - + # Set the formatting hash def formatting=(headers) @col_opts[:formatting] = headers end - + # Set up the cherry picked columns def columns=(array) @col_opts[:columns] = array end - - + + end class TableFu # TableFu::Row adds functionality to an row array in a TableFu instance class Row < Array - + attr_reader :row_num - + def initialize(row, row_num, spreadsheet) self.replace row @row_num = row_num @spreadsheet = spreadsheet end - + def columns all_cols = [] @spreadsheet.columns.each do |column| - all_cols << datum_for(column) - end + all_cols << datum_for(column) + end all_cols end - + # This returns a Datum object for a header name. Will return a nil Datum object # for nonexistant column names - # + # # Parameters: # header name - # + # # Returns: # Datum object - # + # def datum_for(col_name) if col_num = @spreadsheet.column_headers.index(col_name) TableFu::Datum.new(self[col_num], col_name, self, @spreadsheet) else # Return a nil Datum object for non existant column names TableFu::Datum.new(nil, col_name, self, @spreadsheet) end end alias_method :column_for, :datum_for - + # sugar def [](col) if col.is_a?(String) column_for(col) else super(col) end end - + # Comparator for sorting a spreadsheet row. # def <=>(b) if @spreadsheet.sorted_by column = @spreadsheet.sorted_by.keys.first order = @spreadsheet.sorted_by[column]["order"] format = @spreadsheet.sorted_by[column]["format"] a = column_for(column).value || '' b = b.column_for(column).value || '' - if format + if format a = TableFu::Formatting.send(format, a) || '' b = TableFu::Formatting.send(format, b) || '' end result = a <=> b result = -1 if result.nil? - result = result * -1 if order == 'descending' + result = result * -1 if order == 'descending' result else -1 end end - + end # A Datum is an individual cell in the TableFu::Row class Datum attr_reader :options, :column_name - + # Each piece of datum should know where it is by column and row number, along # with the spreadsheet it's apart of. There's probably a better way to go # about doing this. Subclass? def initialize(datum, col_name, row, spreadsheet) @datum = datum @column_name = col_name @row = row @spreadsheet = spreadsheet end - + # Our standard formatter for the datum # # Returns: # the formatted value, macro value, or a empty string # # First we test to see if this Datum has a macro attached to it. If so # we let the macro method do it's magic - # + # # Then we test for a simple formatter method. # # And finally we return a empty string object or the value. # def to_s @@ -276,40 +280,40 @@ TableFu::Formatting.send(format_method, @datum) || '' else @datum || '' end end - - # Returns the macro'd format if there is one + + # Returns the macro'd format if there is one # # Returns: # The macro value if it exists, otherwise nil def macro_value # Grab the macro method first # Then get a array of the values in the columns listed as arguments # Splat the arguments to the macro method. # Example: - # @spreadsheet.col_opts[:formatting] = + # @spreadsheet.col_opts[:formatting] = # {'Total Appropriation' => :currency, # 'AppendedColumn' => {'method' => 'append', 'arguments' => ['Projects','State']}} - # + # # in the above case we handle the AppendedColumn in this method if @spreadsheet.formatting && @spreadsheet.formatting[@column_name].is_a?(Hash) method = @spreadsheet.formatting[@column_name]['method'] arguments = @spreadsheet.formatting[@column_name]['arguments'].inject([]) do |arr,arg| - arr << @row.column_for(arg) + arr << @row.column_for(arg) arr end TableFu::Formatting.send(method, *arguments) end end - - # Returns the raw value of a datum + + # Returns the raw value of a datum # # Returns: - # raw value of the datum, could be nil + # raw value of the datum, could be nil def value if @datum =~ /[0-9]+/ @datum.to_i else @datum @@ -318,25 +322,25 @@ # This method missing looks for 4 matches # # First Option # We have a column option by that method name and it applies to this column - # Example - + # Example - # >> @data.column_name # => 'Total' # >> @datum.style # Finds col_opt[:style] = {'Total' => 'text-align:left;'} # => 'text-align:left;' - # + # # Second Option # We have a column option by that method name, but no attribute # >> @data.column_name # => 'Total' - # >> @datum.style + # >> @datum.style # Finds col_opt[:style] = {'State' => 'text-align:left;'} # => '' - # + # # Third Option # The boolean # >> @data.invisible? # And we've set it col_opts[:invisible] = ['Total'] # => true @@ -359,11 +363,11 @@ nil else super end end - + private # Enable string or symbol key access to col_opts # from sinatra def indifferent_access(params) @@ -376,22 +380,22 @@ def indifferent_hash Hash.new {|hash,key| hash[key.to_s] if Symbol === key } end end - + # A header object needs to be a special kind of Datum, and # we may want to extend this further, but currently we just # need to ensure that when to_s is called on a @Header@ object # that we don't run it through a macro, or a formatter. class Header < Datum def to_s @datum end - + end - + end $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) \ No newline at end of file