Commit 0b57e936 authored by Felix Schäfer's avatar Felix Schäfer

Rip faster_csv out of lib into the Gemfile. #517

parent 3d03a3ed
......@@ -6,6 +6,7 @@ gem "coderay", "~> 0.9.7"
gem "i18n", "~> 0.4.2"
gem "rubytree", "~> 0.5.2", :require => 'tree'
gem "rdoc", ">= 2.4.2"
gem "fastercsv", "~> 1.5.0", :platforms => :ruby_18
group :test do
gem 'shoulda', '~> 2.10.3'
......
#!/usr/local/bin/ruby -w
# = faster_csv.rb -- Faster CSV Reading and Writing
#
# Created by James Edward Gray II on 2005-10-31.
# Copyright 2005 Gray Productions. All rights reserved.
#
# See FasterCSV for documentation.
if RUBY_VERSION >= "1.9"
abort <<-VERSION_WARNING.gsub(/^\s+/, "")
Please switch to Ruby 1.9's standard CSV library. It's FasterCSV plus
support for Ruby 1.9's m17n encoding engine.
VERSION_WARNING
end
require "forwardable"
require "English"
require "enumerator"
require "date"
require "stringio"
#
# This class provides a complete interface to CSV files and data. It offers
# tools to enable you to read and write to and from Strings or IO objects, as
# needed.
#
# == Reading
#
# === From a File
#
# ==== A Line at a Time
#
# FasterCSV.foreach("path/to/file.csv") do |row|
# # use row here...
# end
#
# ==== All at Once
#
# arr_of_arrs = FasterCSV.read("path/to/file.csv")
#
# === From a String
#
# ==== A Line at a Time
#
# FasterCSV.parse("CSV,data,String") do |row|
# # use row here...
# end
#
# ==== All at Once
#
# arr_of_arrs = FasterCSV.parse("CSV,data,String")
#
# == Writing
#
# === To a File
#
# FasterCSV.open("path/to/file.csv", "w") do |csv|
# csv << ["row", "of", "CSV", "data"]
# csv << ["another", "row"]
# # ...
# end
#
# === To a String
#
# csv_string = FasterCSV.generate do |csv|
# csv << ["row", "of", "CSV", "data"]
# csv << ["another", "row"]
# # ...
# end
#
# == Convert a Single Line
#
# csv_string = ["CSV", "data"].to_csv # to CSV
# csv_array = "CSV,String".parse_csv # from CSV
#
# == Shortcut Interface
#
# FCSV { |csv_out| csv_out << %w{my data here} } # to $stdout
# FCSV(csv = "") { |csv_str| csv_str << %w{my data here} } # to a String
# FCSV($stderr) { |csv_err| csv_err << %w{my data here} } # to $stderr
#
class FasterCSV
# The version of the installed library.
VERSION = "1.5.0".freeze
#
# A FasterCSV::Row is part Array and part Hash. It retains an order for the
# fields and allows duplicates just as an Array would, but also allows you to
# access fields by name just as you could if they were in a Hash.
#
# All rows returned by FasterCSV will be constructed from this class, if
# header row processing is activated.
#
class Row
#
# Construct a new FasterCSV::Row from +headers+ and +fields+, which are
# expected to be Arrays. If one Array is shorter than the other, it will be
# padded with +nil+ objects.
#
# The optional +header_row+ parameter can be set to +true+ to indicate, via
# FasterCSV::Row.header_row?() and FasterCSV::Row.field_row?(), that this is
# a header row. Otherwise, the row is assumes to be a field row.
#
# A FasterCSV::Row object supports the following Array methods through
# delegation:
#
# * empty?()
# * length()
# * size()
#
def initialize(headers, fields, header_row = false)
@header_row = header_row
# handle extra headers or fields
@row = if headers.size > fields.size
headers.zip(fields)
else
fields.zip(headers).map { |pair| pair.reverse }
end
end
# Internal data format used to compare equality.
attr_reader :row
protected :row
### Array Delegation ###
extend Forwardable
def_delegators :@row, :empty?, :length, :size
# Returns +true+ if this is a header row.
def header_row?
@header_row
end
# Returns +true+ if this is a field row.
def field_row?
not header_row?
end
# Returns the headers of this row.
def headers
@row.map { |pair| pair.first }
end
#
# :call-seq:
# field( header )
# field( header, offset )
# field( index )
#
# This method will fetch the field value by +header+ or +index+. If a field
# is not found, +nil+ is returned.
#
# When provided, +offset+ ensures that a header match occurrs on or later
# than the +offset+ index. You can use this to find duplicate headers,
# without resorting to hard-coding exact indices.
#
def field(header_or_index, minimum_index = 0)
# locate the pair
finder = header_or_index.is_a?(Integer) ? :[] : :assoc
pair = @row[minimum_index..-1].send(finder, header_or_index)
# return the field if we have a pair
pair.nil? ? nil : pair.last
end
alias_method :[], :field
#
# :call-seq:
# []=( header, value )
# []=( header, offset, value )
# []=( index, value )
#
# Looks up the field by the semantics described in FasterCSV::Row.field()
# and assigns the +value+.
#
# Assigning past the end of the row with an index will set all pairs between
# to <tt>[nil, nil]</tt>. Assigning to an unused header appends the new
# pair.
#
def []=(*args)
value = args.pop
if args.first.is_a? Integer
if @row[args.first].nil? # extending past the end with index
@row[args.first] = [nil, value]
@row.map! { |pair| pair.nil? ? [nil, nil] : pair }
else # normal index assignment
@row[args.first][1] = value
end
else
index = index(*args)
if index.nil? # appending a field
self << [args.first, value]
else # normal header assignment
@row[index][1] = value
end
end
end
#
# :call-seq:
# <<( field )
# <<( header_and_field_array )
# <<( header_and_field_hash )
#
# If a two-element Array is provided, it is assumed to be a header and field
# and the pair is appended. A Hash works the same way with the key being
# the header and the value being the field. Anything else is assumed to be
# a lone field which is appended with a +nil+ header.
#
# This method returns the row for chaining.
#
def <<(arg)
if arg.is_a?(Array) and arg.size == 2 # appending a header and name
@row << arg
elsif arg.is_a?(Hash) # append header and name pairs
arg.each { |pair| @row << pair }
else # append field value
@row << [nil, arg]
end
self # for chaining
end
#
# A shortcut for appending multiple fields. Equivalent to:
#
# args.each { |arg| faster_csv_row << arg }
#
# This method returns the row for chaining.
#
def push(*args)
args.each { |arg| self << arg }
self # for chaining
end
#
# :call-seq:
# delete( header )
# delete( header, offset )
# delete( index )
#
# Used to remove a pair from the row by +header+ or +index+. The pair is
# located as described in FasterCSV::Row.field(). The deleted pair is
# returned, or +nil+ if a pair could not be found.
#
def delete(header_or_index, minimum_index = 0)
if header_or_index.is_a? Integer # by index
@row.delete_at(header_or_index)
else # by header
@row.delete_at(index(header_or_index, minimum_index))
end
end
#
# The provided +block+ is passed a header and field for each pair in the row
# and expected to return +true+ or +false+, depending on whether the pair
# should be deleted.
#
# This method returns the row for chaining.
#
def delete_if(&block)
@row.delete_if(&block)
self # for chaining
end
#
# This method accepts any number of arguments which can be headers, indices,
# Ranges of either, or two-element Arrays containing a header and offset.
# Each argument will be replaced with a field lookup as described in
# FasterCSV::Row.field().
#
# If called with no arguments, all fields are returned.
#
def fields(*headers_and_or_indices)
if headers_and_or_indices.empty? # return all fields--no arguments
@row.map { |pair| pair.last }
else # or work like values_at()
headers_and_or_indices.inject(Array.new) do |all, h_or_i|
all + if h_or_i.is_a? Range
index_begin = h_or_i.begin.is_a?(Integer) ? h_or_i.begin :
index(h_or_i.begin)
index_end = h_or_i.end.is_a?(Integer) ? h_or_i.end :
index(h_or_i.end)
new_range = h_or_i.exclude_end? ? (index_begin...index_end) :
(index_begin..index_end)
fields.values_at(new_range)
else
[field(*Array(h_or_i))]
end
end
end
end
alias_method :values_at, :fields
#
# :call-seq:
# index( header )
# index( header, offset )
#
# This method will return the index of a field with the provided +header+.
# The +offset+ can be used to locate duplicate header names, as described in
# FasterCSV::Row.field().
#
def index(header, minimum_index = 0)
# find the pair
index = headers[minimum_index..-1].index(header)
# return the index at the right offset, if we found one
index.nil? ? nil : index + minimum_index
end
# Returns +true+ if +name+ is a header for this row, and +false+ otherwise.
def header?(name)
headers.include? name
end
alias_method :include?, :header?
#
# Returns +true+ if +data+ matches a field in this row, and +false+
# otherwise.
#
def field?(data)
fields.include? data
end
include Enumerable
#
# Yields each pair of the row as header and field tuples (much like
# iterating over a Hash).
#
# Support for Enumerable.
#
# This method returns the row for chaining.
#
def each(&block)
@row.each(&block)
self # for chaining
end
#
# Returns +true+ if this row contains the same headers and fields in the
# same order as +other+.
#
def ==(other)
@row == other.row
end
#
# Collapses the row into a simple Hash. Be warning that this discards field
# order and clobbers duplicate fields.
#
def to_hash
# flatten just one level of the internal Array
Hash[*@row.inject(Array.new) { |ary, pair| ary.push(*pair) }]
end
#
# Returns the row as a CSV String. Headers are not used. Equivalent to:
#
# faster_csv_row.fields.to_csv( options )
#
def to_csv(options = Hash.new)
fields.to_csv(options)
end
alias_method :to_s, :to_csv
# A summary of fields, by header.
def inspect
str = "#<#{self.class}"
each do |header, field|
str << " #{header.is_a?(Symbol) ? header.to_s : header.inspect}:" <<
field.inspect
end
str << ">"
end
end
#
# A FasterCSV::Table is a two-dimensional data structure for representing CSV
# documents. Tables allow you to work with the data by row or column,
# manipulate the data, and even convert the results back to CSV, if needed.
#
# All tables returned by FasterCSV will be constructed from this class, if
# header row processing is activated.
#
class Table
#
# Construct a new FasterCSV::Table from +array_of_rows+, which are expected
# to be FasterCSV::Row objects. All rows are assumed to have the same
# headers.
#
# A FasterCSV::Table object supports the following Array methods through
# delegation:
#
# * empty?()
# * length()
# * size()
#
def initialize(array_of_rows)
@table = array_of_rows
@mode = :col_or_row
end
# The current access mode for indexing and iteration.
attr_reader :mode
# Internal data format used to compare equality.
attr_reader :table
protected :table
### Array Delegation ###
extend Forwardable
def_delegators :@table, :empty?, :length, :size
#
# Returns a duplicate table object, in column mode. This is handy for
# chaining in a single call without changing the table mode, but be aware
# that this method can consume a fair amount of memory for bigger data sets.
#
# This method returns the duplicate table for chaining. Don't chain
# destructive methods (like []=()) this way though, since you are working
# with a duplicate.
#
def by_col
self.class.new(@table.dup).by_col!
end
#
# Switches the mode of this table to column mode. All calls to indexing and
# iteration methods will work with columns until the mode is changed again.
#
# This method returns the table and is safe to chain.
#
def by_col!
@mode = :col
self
end
#
# Returns a duplicate table object, in mixed mode. This is handy for
# chaining in a single call without changing the table mode, but be aware
# that this method can consume a fair amount of memory for bigger data sets.
#
# This method returns the duplicate table for chaining. Don't chain
# destructive methods (like []=()) this way though, since you are working
# with a duplicate.
#
def by_col_or_row
self.class.new(@table.dup).by_col_or_row!
end
#
# Switches the mode of this table to mixed mode. All calls to indexing and
# iteration methods will use the default intelligent indexing system until
# the mode is changed again. In mixed mode an index is assumed to be a row
# reference while anything else is assumed to be column access by headers.
#
# This method returns the table and is safe to chain.
#
def by_col_or_row!
@mode = :col_or_row
self
end
#
# Returns a duplicate table object, in row mode. This is handy for chaining
# in a single call without changing the table mode, but be aware that this
# method can consume a fair amount of memory for bigger data sets.
#
# This method returns the duplicate table for chaining. Don't chain
# destructive methods (like []=()) this way though, since you are working
# with a duplicate.
#
def by_row
self.class.new(@table.dup).by_row!
end
#
# Switches the mode of this table to row mode. All calls to indexing and
# iteration methods will work with rows until the mode is changed again.
#
# This method returns the table and is safe to chain.
#
def by_row!
@mode = :row
self
end
#
# Returns the headers for the first row of this table (assumed to match all
# other rows). An empty Array is returned for empty tables.
#
def headers
if @table.empty?
Array.new
else
@table.first.headers
end
end
#
# In the default mixed mode, this method returns rows for index access and
# columns for header access. You can force the index association by first
# calling by_col!() or by_row!().
#
# Columns are returned as an Array of values. Altering that Array has no
# effect on the table.
#
def [](index_or_header)
if @mode == :row or # by index
(@mode == :col_or_row and index_or_header.is_a? Integer)
@table[index_or_header]
else # by header
@table.map { |row| row[index_or_header] }
end
end
#
# In the default mixed mode, this method assigns rows for index access and
# columns for header access. You can force the index association by first
# calling by_col!() or by_row!().
#
# Rows may be set to an Array of values (which will inherit the table's
# headers()) or a FasterCSV::Row.
#
# Columns may be set to a single value, which is copied to each row of the
# column, or an Array of values. Arrays of values are assigned to rows top
# to bottom in row major order. Excess values are ignored and if the Array
# does not have a value for each row the extra rows will receive a +nil+.
#
# Assigning to an existing column or row clobbers the data. Assigning to
# new columns creates them at the right end of the table.
#
def []=(index_or_header, value)
if @mode == :row or # by index
(@mode == :col_or_row and index_or_header.is_a? Integer)
if value.is_a? Array
@table[index_or_header] = Row.new(headers, value)
else
@table[index_or_header] = value
end
else # set column
if value.is_a? Array # multiple values
@table.each_with_index do |row, i|
if row.header_row?
row[index_or_header] = index_or_header
else
row[index_or_header] = value[i]
end
end
else # repeated value
@table.each do |row|
if row.header_row?
row[index_or_header] = index_or_header
else
row[index_or_header] = value
end
end
end
end
end
#
# The mixed mode default is to treat a list of indices as row access,
# returning the rows indicated. Anything else is considered columnar
# access. For columnar access, the return set has an Array for each row
# with the values indicated by the headers in each Array. You can force
# column or row mode using by_col!() or by_row!().
#
# You cannot mix column and row access.
#
def values_at(*indices_or_headers)
if @mode == :row or # by indices
( @mode == :col_or_row and indices_or_headers.all? do |index|
index.is_a?(Integer) or
( index.is_a?(Range) and
index.first.is_a?(Integer) and
index.last.is_a?(Integer) )
end )
@table.values_at(*indices_or_headers)
else # by headers
@table.map { |row| row.values_at(*indices_or_headers) }
end
end
#
# Adds a new row to the bottom end of this table. You can provide an Array,
# which will be converted to a FasterCSV::Row (inheriting the table's
# headers()), or a FasterCSV::Row.
#
# This method returns the table for chaining.
#
def <<(row_or_array)
if row_or_array.is_a? Array # append Array
@table << Row.new(headers, row_or_array)
else # append Row
@table << row_or_array
end
self # for chaining
end
#
# A shortcut for appending multiple rows. Equivalent to:
#
# rows.each { |row| self << row }
#
# This method returns the table for chaining.
#
def push(*rows)
rows.each { |row| self << row }
self # for chaining
end
#
# Removes and returns the indicated column or row. In the default mixed
# mode indices refer to rows and everything else is assumed to be a column
# header. Use by_col!() or by_row!() to force the lookup.
#
def delete(index_or_header)
if @mode == :row or # by index
(@mode == :col_or_row and index_or_header.is_a? Integer)
@table.delete_at(index_or_header)
else # by header
@table.map { |row| row.delete(index_or_header).last }
end
end
#
# Removes any column or row for which the block returns +true+. In the
# default mixed mode or row mode, iteration is the standard row major
# walking of rows. In column mode, interation will +yield+ two element
# tuples containing the column name and an Array of values for that column.
#
# This method returns the table for chaining.
#
def delete_if(&block)
if @mode == :row or @mode == :col_or_row # by index
@table.delete_if(&block)
else # by header
to_delete = Array.new
headers.each_with_index do |header, i|
to_delete << header if block[[header, self[header]]]
end
to_delete.map { |header| delete(header) }
end
self # for chaining
end
include Enumerable
#
# In the default mixed mode or row mode, iteration is the standard row major
# walking of rows. In column mode, interation will +yield+ two element
# tuples containing the column name and an Array of values for that column.
#
# This method returns the table for chaining.
#
def each(&block)
if @mode == :col
headers.each { |header| block[[header, self[header]]] }
else
@table.each(&block)
end
self # for chaining
end
# Returns +true+ if all rows of this table ==() +other+'s rows.
def ==(other)
@table == other.table
end
#
# Returns the table as an Array of Arrays. Headers will be the first row,
# then all of the field rows will follow.
#
def to_a
@table.inject([headers]) do |array, row|
if row.header_row?
array
else
array + [row.fields]
end
end
end
#
# Returns the table as a complete CSV String. Headers will be listed first,
# then all of the field rows.
#
def to_csv(options = Hash.new)
@table.inject([headers.to_csv(options)]) do |rows, row|
if row.header_row?
rows
else
rows + [row.fields.to_csv(options)]
end
end.join
end
alias_method :to_s, :to_csv
def inspect
"#<#{self.class} mode:#{@mode} row_count:#{to_a.size}>"
end
end
# The error thrown when the parser encounters illegal CSV formatting.
class MalformedCSVError < RuntimeError; end
#
# A FieldInfo Struct contains details about a field's position in the data
# source it was read from. FasterCSV will pass this Struct to some blocks
# that make decisions based on field structure. See
# FasterCSV.convert_fields() for an example.
#
# <b><tt>index</tt></b>:: The zero-based index of the field in its row.
# <b><tt>line</tt></b>:: The line of the data source this row is from.
# <b><tt>header</tt></b>:: The header for the column, when available.
#
FieldInfo = Struct.new(:index, :line, :header)
# A Regexp used to find and convert some common Date formats.
DateMatcher = / \A(?: (\w+,?\s+)?\w+\s+\d{1,2},?\s+\d{2,4} |
\d{4}-\d{2}-\d{2} )\z /x
# A Regexp used to find and convert some common DateTime formats.
DateTimeMatcher =
/ \A(?: (\w+,?\s+)?\w+\s+\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2},?\s+\d{2,4} |
\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2} )\z /x
#
# This Hash holds the built-in converters of FasterCSV that can be accessed by
# name. You can select Converters with FasterCSV.convert() or through the
# +options+ Hash passed to FasterCSV::new().
#
# <b><tt>:integer</tt></b>:: Converts any field Integer() accepts.
# <b><tt>:float</tt></b>:: Converts any field Float() accepts.
# <b><tt>:numeric</tt></b>:: A combination of <tt>:integer</tt>
# and <tt>:float</tt>.