mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
Merge csv-1.0.2 from upstream.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@63364 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
This commit is contained in:
parent
dfc56b8c43
commit
5c1941a9be
18 changed files with 1340 additions and 865 deletions
|
@ -1,21 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "csv"
|
||||
spec.version = "1.0.0"
|
||||
spec.date = "2017-12-13"
|
||||
spec.authors = ["James Edward Gray II"]
|
||||
spec.email = [nil]
|
||||
|
||||
spec.summary = "CSV Reading and Writing"
|
||||
spec.description = "the CSV library began its life as FasterCSV."
|
||||
spec.homepage = "https://github.com/ruby/csv"
|
||||
spec.license = "BSD-2-Clause"
|
||||
|
||||
spec.files = ["lib/csv.rb"]
|
||||
spec.require_paths = ["lib"]
|
||||
spec.required_ruby_version = ">= 2.4.0"
|
||||
|
||||
spec.add_development_dependency "bundler", "~> 1.14"
|
||||
spec.add_development_dependency "rake", "~> 12"
|
||||
end
|
1025
lib/csv.rb
Executable file → Normal file
1025
lib/csv.rb
Executable file → Normal file
File diff suppressed because it is too large
Load diff
9
lib/csv/core_ext/array.rb
Normal file
9
lib/csv/core_ext/array.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
class Array # :nodoc:
|
||||
# Equivalent to CSV::generate_line(self, options)
|
||||
#
|
||||
# ["CSV", "data"].to_csv
|
||||
# #=> "CSV,data\n"
|
||||
def to_csv(**options)
|
||||
CSV.generate_line(self, options)
|
||||
end
|
||||
end
|
9
lib/csv/core_ext/string.rb
Normal file
9
lib/csv/core_ext/string.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
class String # :nodoc:
|
||||
# Equivalent to CSV::parse_line(self, options)
|
||||
#
|
||||
# "CSV,data".parse_csv
|
||||
# #=> ["CSV", "data"]
|
||||
def parse_csv(**options)
|
||||
CSV.parse_line(self, options)
|
||||
end
|
||||
end
|
24
lib/csv/csv.gemspec
Normal file
24
lib/csv/csv.gemspec
Normal file
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative "lib/csv/version"
|
||||
|
||||
Gem::Specification.new do |spec|
|
||||
spec.name = "csv"
|
||||
spec.version = CSV::VERSION
|
||||
spec.authors = ["James Edward Gray II", "Kouhei Sutou"]
|
||||
spec.email = [nil, "kou@cozmixng.org"]
|
||||
|
||||
spec.summary = "CSV Reading and Writing"
|
||||
spec.description = "The CSV library 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."
|
||||
spec.homepage = "https://github.com/ruby/csv"
|
||||
spec.license = "BSD-2-Clause"
|
||||
|
||||
spec.files = Dir.glob("lib/**/*.rb")
|
||||
spec.files += ["README.md", "LICENSE.txt", "news.md"]
|
||||
spec.require_paths = ["lib"]
|
||||
spec.required_ruby_version = ">= 2.3.0"
|
||||
|
||||
spec.add_development_dependency "bundler"
|
||||
spec.add_development_dependency "rake"
|
||||
spec.add_development_dependency "benchmark-ips"
|
||||
end
|
388
lib/csv/row.rb
Normal file
388
lib/csv/row.rb
Normal file
|
@ -0,0 +1,388 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "forwardable"
|
||||
|
||||
class CSV
|
||||
#
|
||||
# A CSV::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 CSV will be constructed from this class, if header row
|
||||
# processing is activated.
|
||||
#
|
||||
class Row
|
||||
#
|
||||
# Construct a new CSV::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
|
||||
# CSV::Row.header_row?() and CSV::Row.field_row?(), that this is a header
|
||||
# row. Otherwise, the row is assumes to be a field row.
|
||||
#
|
||||
# A CSV::Row object supports the following Array methods through delegation:
|
||||
#
|
||||
# * empty?()
|
||||
# * length()
|
||||
# * size()
|
||||
#
|
||||
def initialize(headers, fields, header_row = false)
|
||||
@header_row = header_row
|
||||
headers.each { |h| h.freeze if h.is_a? String }
|
||||
|
||||
# handle extra headers or fields
|
||||
@row = if headers.size >= fields.size
|
||||
headers.zip(fields)
|
||||
else
|
||||
fields.zip(headers).each(&: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(&:first)
|
||||
end
|
||||
|
||||
#
|
||||
# :call-seq:
|
||||
# field( header )
|
||||
# field( header, offset )
|
||||
# field( index )
|
||||
#
|
||||
# This method will return the field value by +header+ or +index+. If a field
|
||||
# is not found, +nil+ is returned.
|
||||
#
|
||||
# When provided, +offset+ ensures that a header match occurs 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) || header_or_index.is_a?(Range)) ? :[] : :assoc
|
||||
pair = @row[minimum_index..-1].send(finder, header_or_index)
|
||||
|
||||
# return the field if we have a pair
|
||||
if pair.nil?
|
||||
nil
|
||||
else
|
||||
header_or_index.is_a?(Range) ? pair.map(&:last) : pair.last
|
||||
end
|
||||
end
|
||||
alias_method :[], :field
|
||||
|
||||
#
|
||||
# :call-seq:
|
||||
# fetch( header )
|
||||
# fetch( header ) { |row| ... }
|
||||
# fetch( header, default )
|
||||
#
|
||||
# This method will fetch the field value by +header+. It has the same
|
||||
# behavior as Hash#fetch: if there is a field with the given +header+, its
|
||||
# value is returned. Otherwise, if a block is given, it is yielded the
|
||||
# +header+ and its result is returned; if a +default+ is given as the
|
||||
# second argument, it is returned; otherwise a KeyError is raised.
|
||||
#
|
||||
def fetch(header, *varargs)
|
||||
raise ArgumentError, "Too many arguments" if varargs.length > 1
|
||||
pair = @row.assoc(header)
|
||||
if pair
|
||||
pair.last
|
||||
else
|
||||
if block_given?
|
||||
yield header
|
||||
elsif varargs.empty?
|
||||
raise KeyError, "key not found: #{header}"
|
||||
else
|
||||
varargs.first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns +true+ if there is a field with the given +header+.
|
||||
def has_key?(header)
|
||||
!!@row.assoc(header)
|
||||
end
|
||||
alias_method :include?, :has_key?
|
||||
alias_method :key?, :has_key?
|
||||
alias_method :member?, :has_key?
|
||||
|
||||
#
|
||||
# :call-seq:
|
||||
# []=( header, value )
|
||||
# []=( header, offset, value )
|
||||
# []=( index, value )
|
||||
#
|
||||
# Looks up the field by the semantics described in CSV::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| 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 CSV::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)
|
||||
elsif i = index(header_or_index, minimum_index) # by header
|
||||
@row.delete_at(i)
|
||||
else
|
||||
[ ]
|
||||
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.
|
||||
#
|
||||
# If no block is given, an Enumerator is returned.
|
||||
#
|
||||
def delete_if(&block)
|
||||
return enum_for(__method__) { size } unless block_given?
|
||||
|
||||
@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
|
||||
# CSV::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(&:last)
|
||||
else # or work like values_at()
|
||||
all = []
|
||||
headers_and_or_indices.each do |h_or_i|
|
||||
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)
|
||||
all.concat(fields.values_at(new_range))
|
||||
else
|
||||
all << field(*Array(h_or_i))
|
||||
end
|
||||
end
|
||||
return all
|
||||
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
|
||||
# CSV::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). This method returns the row for chaining.
|
||||
#
|
||||
# If no block is given, an Enumerator is returned.
|
||||
#
|
||||
# Support for Enumerable.
|
||||
#
|
||||
def each(&block)
|
||||
return enum_for(__method__) { size } unless block_given?
|
||||
|
||||
@row.each(&block)
|
||||
|
||||
self # for chaining
|
||||
end
|
||||
|
||||
alias_method :each_pair, :each
|
||||
|
||||
#
|
||||
# Returns +true+ if this row contains the same headers and fields in the
|
||||
# same order as +other+.
|
||||
#
|
||||
def ==(other)
|
||||
return @row == other.row if other.is_a? CSV::Row
|
||||
@row == other
|
||||
end
|
||||
|
||||
#
|
||||
# Collapses the row into a simple Hash. Be warned that this discards field
|
||||
# order and clobbers duplicate fields.
|
||||
#
|
||||
def to_h
|
||||
hash = {}
|
||||
each do |key, _value|
|
||||
hash[key] = self[key] unless hash.key?(key)
|
||||
end
|
||||
hash
|
||||
end
|
||||
alias_method :to_hash, :to_h
|
||||
|
||||
alias_method :to_ary, :to_a
|
||||
|
||||
#
|
||||
# Returns the row as a CSV String. Headers are not used. Equivalent to:
|
||||
#
|
||||
# csv_row.fields.to_csv( options )
|
||||
#
|
||||
def to_csv(**options)
|
||||
fields.to_csv(options)
|
||||
end
|
||||
alias_method :to_s, :to_csv
|
||||
|
||||
#
|
||||
# Extracts the nested value specified by the sequence of +index+ or +header+ objects by calling dig at each step,
|
||||
# returning nil if any intermediate step is nil.
|
||||
#
|
||||
def dig(index_or_header, *indexes)
|
||||
value = field(index_or_header)
|
||||
if value.nil?
|
||||
nil
|
||||
elsif indexes.empty?
|
||||
value
|
||||
else
|
||||
unless value.respond_to?(:dig)
|
||||
raise TypeError, "#{value.class} does not have \#dig method"
|
||||
end
|
||||
value.dig(*indexes)
|
||||
end
|
||||
end
|
||||
|
||||
# A summary of fields, by header, in an ASCII compatible String.
|
||||
def inspect
|
||||
str = ["#<", self.class.to_s]
|
||||
each do |header, field|
|
||||
str << " " << (header.is_a?(Symbol) ? header.to_s : header.inspect) <<
|
||||
":" << field.inspect
|
||||
end
|
||||
str << ">"
|
||||
begin
|
||||
str.join('')
|
||||
rescue # any encoding error
|
||||
str.map do |s|
|
||||
e = Encoding::Converter.asciicompat_encoding(s.encoding)
|
||||
e ? s.encode(e) : s.force_encoding("ASCII-8BIT")
|
||||
end.join('')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
378
lib/csv/table.rb
Normal file
378
lib/csv/table.rb
Normal file
|
@ -0,0 +1,378 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "forwardable"
|
||||
|
||||
class CSV
|
||||
#
|
||||
# A CSV::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 CSV will be constructed from this class, if header
|
||||
# row processing is activated.
|
||||
#
|
||||
class Table
|
||||
#
|
||||
# Construct a new CSV::Table from +array_of_rows+, which are expected
|
||||
# to be CSV::Row objects. All rows are assumed to have the same headers.
|
||||
#
|
||||
# A CSV::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) or index_or_header.is_a?(Range)))
|
||||
@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 CSV::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 CSV::Row (inheriting the table's headers()),
|
||||
# or a CSV::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 columns or rows. In the default mixed
|
||||
# mode indices refer to rows and everything else is assumed to be a column
|
||||
# headers. Use by_col!() or by_row!() to force the lookup.
|
||||
#
|
||||
def delete(*indexes_or_headers)
|
||||
if indexes_or_headers.empty?
|
||||
raise ArgumentError, "wrong number of arguments (given 0, expected 1+)"
|
||||
end
|
||||
deleted_values = indexes_or_headers.map do |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
|
||||
if indexes_or_headers.size == 1
|
||||
deleted_values[0]
|
||||
else
|
||||
deleted_values
|
||||
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, iteration will +yield+ two element
|
||||
# tuples containing the column name and an Array of values for that column.
|
||||
#
|
||||
# This method returns the table for chaining.
|
||||
#
|
||||
# If no block is given, an Enumerator is returned.
|
||||
#
|
||||
def delete_if(&block)
|
||||
return enum_for(__method__) { @mode == :row or @mode == :col_or_row ? size : headers.size } unless block_given?
|
||||
|
||||
if @mode == :row or @mode == :col_or_row # by index
|
||||
@table.delete_if(&block)
|
||||
else # by header
|
||||
deleted = []
|
||||
headers.each do |header|
|
||||
deleted << delete(header) if yield([header, self[header]])
|
||||
end
|
||||
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, iteration will +yield+ two element
|
||||
# tuples containing the column name and an Array of values for that column.
|
||||
#
|
||||
# This method returns the table for chaining.
|
||||
#
|
||||
# If no block is given, an Enumerator is returned.
|
||||
#
|
||||
def each(&block)
|
||||
return enum_for(__method__) { @mode == :col ? headers.size : size } unless block_given?
|
||||
|
||||
if @mode == :col
|
||||
headers.each { |header| yield([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)
|
||||
return @table == other.table if other.is_a? CSV::Table
|
||||
@table == other
|
||||
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
|
||||
array = [headers]
|
||||
@table.each do |row|
|
||||
array.push(row.fields) unless row.header_row?
|
||||
end
|
||||
|
||||
array
|
||||
end
|
||||
|
||||
#
|
||||
# Returns the table as a complete CSV String. Headers will be listed first,
|
||||
# then all of the field rows.
|
||||
#
|
||||
# This method assumes you want the Table.headers(), unless you explicitly
|
||||
# pass <tt>:write_headers => false</tt>.
|
||||
#
|
||||
def to_csv(write_headers: true, **options)
|
||||
array = write_headers ? [headers.to_csv(options)] : []
|
||||
@table.each do |row|
|
||||
array.push(row.fields.to_csv(options)) unless row.header_row?
|
||||
end
|
||||
|
||||
array.join("")
|
||||
end
|
||||
alias_method :to_s, :to_csv
|
||||
|
||||
#
|
||||
# Extracts the nested value specified by the sequence of +index+ or +header+ objects by calling dig at each step,
|
||||
# returning nil if any intermediate step is nil.
|
||||
#
|
||||
def dig(index_or_header, *index_or_headers)
|
||||
value = self[index_or_header]
|
||||
if value.nil?
|
||||
nil
|
||||
elsif index_or_headers.empty?
|
||||
value
|
||||
else
|
||||
unless value.respond_to?(:dig)
|
||||
raise TypeError, "#{value.class} does not have \#dig method"
|
||||
end
|
||||
value.dig(*index_or_headers)
|
||||
end
|
||||
end
|
||||
|
||||
# Shows the mode and size of this table in a US-ASCII String.
|
||||
def inspect
|
||||
"#<#{self.class} mode:#{@mode} row_count:#{to_a.size}>".encode("US-ASCII")
|
||||
end
|
||||
end
|
||||
end
|
6
lib/csv/version.rb
Normal file
6
lib/csv/version.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CSV
|
||||
# The version of the installed library.
|
||||
VERSION = "1.0.2"
|
||||
end
|
|
@ -4,9 +4,7 @@
|
|||
|
||||
# tc_csv_parsing.rb
|
||||
#
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
# Copyright 2005 James Edward Gray II. You can redistribute or modify this code
|
||||
# under the terms of Ruby's license.
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
|
||||
require "timeout"
|
||||
|
||||
|
@ -168,7 +166,7 @@ class TestCSV::Parsing < TestCSV
|
|||
assert_send([csv.lineno, :<, 4])
|
||||
end
|
||||
rescue CSV::MalformedCSVError
|
||||
assert_equal( "Unquoted fields do not allow \\r or \\n (line 4).",
|
||||
assert_equal( "Unquoted fields do not allow \\r or \\n in line 4.",
|
||||
$!.message )
|
||||
end
|
||||
|
||||
|
@ -231,6 +229,16 @@ class TestCSV::Parsing < TestCSV
|
|||
assert_parse_errors_out(data, field_size_limit: 5)
|
||||
end
|
||||
|
||||
def test_col_sep_comma
|
||||
assert_equal([["a", "b", nil, "d"]],
|
||||
CSV.parse("a,b,,d", col_sep: ","))
|
||||
end
|
||||
|
||||
def test_col_sep_space
|
||||
assert_equal([["a", "b", nil, "d"]],
|
||||
CSV.parse("a b d", col_sep: " "))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_parse_errors_out(*args)
|
||||
|
|
|
@ -4,10 +4,7 @@
|
|||
|
||||
# tc_csv_writing.rb
|
||||
#
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
# Copyright 2005 James Edward Gray II. You can redistribute or modify this code
|
||||
# under the terms of Ruby's license.
|
||||
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
require_relative "base"
|
||||
|
||||
class TestCSV::Writing < TestCSV
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
|
||||
# tc_data_converters.rb
|
||||
#
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
# Copyright 2005 James Edward Gray II. You can redistribute or modify this code
|
||||
# under the terms of Ruby's license.
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
|
||||
require_relative "base"
|
||||
|
||||
|
@ -67,6 +65,55 @@ class TestCSV::DataConverters < TestCSV
|
|||
assert_instance_of(String, CSV::Converters[:date_time]["junk"])
|
||||
end
|
||||
|
||||
def test_builtin_date_time_converter_iso8601_date
|
||||
iso8601_string = "2018-01-14"
|
||||
datetime = DateTime.new(2018, 1, 14)
|
||||
assert_equal(datetime,
|
||||
CSV::Converters[:date_time][iso8601_string])
|
||||
end
|
||||
|
||||
def test_builtin_date_time_converter_iso8601_minute
|
||||
iso8601_string = "2018-01-14T22:25"
|
||||
datetime = DateTime.new(2018, 1, 14, 22, 25)
|
||||
assert_equal(datetime,
|
||||
CSV::Converters[:date_time][iso8601_string])
|
||||
end
|
||||
|
||||
def test_builtin_date_time_converter_iso8601_second
|
||||
iso8601_string = "2018-01-14T22:25:19"
|
||||
datetime = DateTime.new(2018, 1, 14, 22, 25, 19)
|
||||
assert_equal(datetime,
|
||||
CSV::Converters[:date_time][iso8601_string])
|
||||
end
|
||||
|
||||
def test_builtin_date_time_converter_iso8601_under_second
|
||||
iso8601_string = "2018-01-14T22:25:19.1"
|
||||
datetime = DateTime.new(2018, 1, 14, 22, 25, 19.1)
|
||||
assert_equal(datetime,
|
||||
CSV::Converters[:date_time][iso8601_string])
|
||||
end
|
||||
|
||||
def test_builtin_date_time_converter_iso8601_under_second_offset
|
||||
iso8601_string = "2018-01-14T22:25:19.1+09:00"
|
||||
datetime = DateTime.new(2018, 1, 14, 22, 25, 19.1, "+9")
|
||||
assert_equal(datetime,
|
||||
CSV::Converters[:date_time][iso8601_string])
|
||||
end
|
||||
|
||||
def test_builtin_date_time_converter_iso8601_offset
|
||||
iso8601_string = "2018-01-14T22:25:19+09:00"
|
||||
datetime = DateTime.new(2018, 1, 14, 22, 25, 19, "+9")
|
||||
assert_equal(datetime,
|
||||
CSV::Converters[:date_time][iso8601_string])
|
||||
end
|
||||
|
||||
def test_builtin_date_time_converter_iso8601_utc
|
||||
iso8601_string = "2018-01-14T22:25:19Z"
|
||||
datetime = DateTime.new(2018, 1, 14, 22, 25, 19)
|
||||
assert_equal(datetime,
|
||||
CSV::Converters[:date_time][iso8601_string])
|
||||
end
|
||||
|
||||
def test_convert_with_builtin_integer
|
||||
# setup parser...
|
||||
assert_respond_to(@parser, :convert)
|
||||
|
@ -105,7 +152,7 @@ class TestCSV::DataConverters < TestCSV
|
|||
end
|
||||
|
||||
# gives us proper number conversion
|
||||
assert_equal( [String, String, Integer, String, Float],
|
||||
assert_equal( [String, String, 0.class, String, Float],
|
||||
@parser.shift.map { |field| field.class } )
|
||||
end
|
||||
|
||||
|
@ -114,7 +161,7 @@ class TestCSV::DataConverters < TestCSV
|
|||
assert_nothing_raised(Exception) { @parser.convert(:numeric) }
|
||||
|
||||
# and use
|
||||
assert_equal( [String, String, Integer, String, Float],
|
||||
assert_equal( [String, String, 0.class, String, Float],
|
||||
@parser.shift.map { |field| field.class } )
|
||||
end
|
||||
|
||||
|
@ -125,7 +172,7 @@ class TestCSV::DataConverters < TestCSV
|
|||
assert_nothing_raised(Exception) { @parser.convert(:all) }
|
||||
|
||||
# and use
|
||||
assert_equal( [String, String, Integer, String, Float, DateTime],
|
||||
assert_equal( [String, String, 0.class, String, Float, DateTime],
|
||||
@parser.shift.map { |field| field.class } )
|
||||
end
|
||||
|
||||
|
@ -270,4 +317,14 @@ class TestCSV::DataConverters < TestCSV
|
|||
assert_respond_to(row, :unconverted_fields)
|
||||
assert_equal(Array.new, row.unconverted_fields)
|
||||
end
|
||||
|
||||
def test_nil_value
|
||||
assert_equal(["nil", "", "a"],
|
||||
CSV.parse_line(',"",a', nil_value: "nil"))
|
||||
end
|
||||
|
||||
def test_empty_value
|
||||
assert_equal([nil, "empty", "a"],
|
||||
CSV.parse_line(',"",a', empty_value: "empty"))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
|
||||
# tc_encodings.rb
|
||||
#
|
||||
# Created by James Edward Gray II on 2008-09-13.
|
||||
# Copyright 2008 James Edward Gray II. You can redistribute or modify this code
|
||||
# under the terms of Ruby's license.
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
|
||||
require_relative "base"
|
||||
|
||||
|
@ -256,6 +254,22 @@ class TestCSV::Encodings < TestCSV
|
|||
assert_equal(["foo,\u3042\n".encode(Encoding::Windows_31J), Encoding::Windows_31J], [s, s.encoding], bug9766)
|
||||
end
|
||||
|
||||
def test_row_separator_detection_with_invalid_encoding
|
||||
csv = CSV.new("invalid,\xF8\r\nvalid,x\r\n".force_encoding("UTF-8"),
|
||||
encoding: "UTF-8")
|
||||
assert_equal("\r\n", csv.row_sep)
|
||||
end
|
||||
|
||||
def test_invalid_encoding_row_error
|
||||
csv = CSV.new("invalid,\xF8\r\nvalid,x\r\n".force_encoding("UTF-8"),
|
||||
encoding: "UTF-8")
|
||||
error = assert_raise(CSV::MalformedCSVError) do
|
||||
csv.shift
|
||||
end
|
||||
assert_equal("Invalid byte sequence in UTF-8 in line 1.",
|
||||
error.message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_parses(fields, encoding, options = { })
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
|
||||
# tc_features.rb
|
||||
#
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
# Copyright 2005 James Edward Gray II. You can redistribute or modify this code
|
||||
# under the terms of Ruby's license.
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
|
||||
begin
|
||||
require "zlib"
|
||||
|
@ -313,7 +311,7 @@ class TestCSV::Features < TestCSV
|
|||
def test_inspect_encoding_is_ascii_compatible
|
||||
csv = CSV.new("one,two,three\n1,2,3\n".encode("UTF-16BE"))
|
||||
assert_send([Encoding, :compatible?,
|
||||
Encoding.find("US-ASCII"), csv.inspect.encoding],
|
||||
Encoding.find("US-ASCII"), csv.inspect.encoding],
|
||||
"inspect() was not ASCII compatible.")
|
||||
end
|
||||
|
||||
|
@ -321,7 +319,7 @@ class TestCSV::Features < TestCSV
|
|||
assert_not_nil(CSV::VERSION)
|
||||
assert_instance_of(String, CSV::VERSION)
|
||||
assert_predicate(CSV::VERSION, :frozen?)
|
||||
assert_match(/\A\d\.\d\.\d\Z/, CSV::VERSION)
|
||||
assert_match(/\A\d\.\d\.\d\z/, CSV::VERSION)
|
||||
end
|
||||
|
||||
def test_accepts_comment_skip_lines_option
|
||||
|
@ -352,11 +350,13 @@ class TestCSV::Features < TestCSV
|
|||
end
|
||||
|
||||
def test_comment_rows_are_ignored_with_heredoc
|
||||
c = CSV.new(<<~EOL, skip_lines: ".")
|
||||
1,foo
|
||||
.2,bar
|
||||
3,baz
|
||||
sample_data = <<~EOL
|
||||
1,foo
|
||||
.2,bar
|
||||
3,baz
|
||||
EOL
|
||||
|
||||
c = CSV.new(sample_data, skip_lines: ".")
|
||||
assert_equal [["1", "foo"], ["3", "baz"]], c.each.to_a
|
||||
end
|
||||
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
|
||||
# tc_headers.rb
|
||||
#
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
# Copyright 2005 James Edward Gray II. You can redistribute or modify this code
|
||||
# under the terms of Ruby's license.
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
|
||||
require_relative "base"
|
||||
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
|
||||
# tc_interface.rb
|
||||
#
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
# Copyright 2005 James Edward Gray II. You can redistribute or modify this code
|
||||
# under the terms of Ruby's license.
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
|
||||
require_relative "base"
|
||||
require "tempfile"
|
||||
|
@ -64,6 +62,55 @@ class TestCSV::Interface < TestCSV
|
|||
assert_equal("Return value.", ret)
|
||||
end
|
||||
|
||||
def test_open_encoding_valid
|
||||
# U+1F600 GRINNING FACE
|
||||
# U+1F601 GRINNING FACE WITH SMILING EYES
|
||||
File.open(@path, "w") do |file|
|
||||
file << "\u{1F600},\u{1F601}"
|
||||
end
|
||||
CSV.open(@path, encoding: "utf-8") do |csv|
|
||||
assert_equal([["\u{1F600}", "\u{1F601}"]],
|
||||
csv.to_a)
|
||||
end
|
||||
end
|
||||
|
||||
def test_open_encoding_invalid
|
||||
# U+1F600 GRINNING FACE
|
||||
# U+1F601 GRINNING FACE WITH SMILING EYES
|
||||
File.open(@path, "w") do |file|
|
||||
file << "\u{1F600},\u{1F601}"
|
||||
end
|
||||
CSV.open(@path, encoding: "EUC-JP") do |csv|
|
||||
error = assert_raise(CSV::MalformedCSVError) do
|
||||
csv.shift
|
||||
end
|
||||
assert_equal("Invalid byte sequence in EUC-JP in line 1.",
|
||||
error.message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_open_encoding_nonexistent
|
||||
_output, error = capture_io do
|
||||
CSV.open(@path, encoding: "nonexistent") do
|
||||
end
|
||||
end
|
||||
assert_equal("path:0: warning: Unsupported encoding nonexistent ignored\n",
|
||||
error.gsub(/\A.+:\d+: /, "path:0: "))
|
||||
end
|
||||
|
||||
def test_open_encoding_utf_8_with_bom
|
||||
# U+FEFF ZERO WIDTH NO-BREAK SPACE, BOM
|
||||
# U+1F600 GRINNING FACE
|
||||
# U+1F601 GRINNING FACE WITH SMILING EYES
|
||||
File.open(@path, "w") do |file|
|
||||
file << "\u{FEFF}\u{1F600},\u{1F601}"
|
||||
end
|
||||
CSV.open(@path, encoding: "bom|utf-8") do |csv|
|
||||
assert_equal([["\u{1F600}", "\u{1F601}"]],
|
||||
csv.to_a)
|
||||
end
|
||||
end
|
||||
|
||||
def test_parse
|
||||
data = File.binread(@path)
|
||||
assert_equal( @expected,
|
||||
|
@ -161,6 +208,9 @@ class TestCSV::Interface < TestCSV
|
|||
assert_equal(csv, csv << ["last", %Q{"row"}])
|
||||
end
|
||||
assert_equal(%Q{1,2,3\n4,,5\nlast,"""row"""\n}, str)
|
||||
|
||||
out = CSV.generate("test") { |csv| csv << ["row"] }
|
||||
assert_equal("testrow\n", out)
|
||||
end
|
||||
|
||||
def test_generate_line
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
|
||||
# tc_row.rb
|
||||
#
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
# Copyright 2005 James Edward Gray II. You can redistribute or modify this code
|
||||
# under the terms of Ruby's license.
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
|
||||
require_relative "base"
|
||||
|
||||
|
@ -304,6 +302,17 @@ class TestCSV::Row < TestCSV
|
|||
end
|
||||
end
|
||||
|
||||
def test_each_pair
|
||||
assert_equal([
|
||||
["A", 1],
|
||||
["B", 2],
|
||||
["C", 3],
|
||||
["A", 4],
|
||||
["A", nil],
|
||||
],
|
||||
@row.each_pair.to_a)
|
||||
end
|
||||
|
||||
def test_enumerable
|
||||
assert_equal( [["A", 1], ["A", 4], ["A", nil]],
|
||||
@row.select { |pair| pair.first == "A" } )
|
||||
|
@ -323,7 +332,7 @@ class TestCSV::Row < TestCSV
|
|||
|
||||
def test_to_hash
|
||||
hash = @row.to_hash
|
||||
assert_equal({"A" => nil, "B" => 2, "C" => 3}, hash)
|
||||
assert_equal({"A" => @row["A"], "B" => @row["B"], "C" => @row["C"]}, hash)
|
||||
hash.keys.each_with_index do |string_key, h|
|
||||
assert_predicate(string_key, :frozen?)
|
||||
assert_same(string_key, @row.headers[h])
|
||||
|
@ -377,4 +386,37 @@ class TestCSV::Row < TestCSV
|
|||
r = @row == []
|
||||
assert_equal false, r
|
||||
end
|
||||
|
||||
def test_dig_by_index
|
||||
assert_equal(2, @row.dig(1))
|
||||
|
||||
assert_nil(@row.dig(100))
|
||||
end
|
||||
|
||||
def test_dig_by_header
|
||||
assert_equal(2, @row.dig("B"))
|
||||
|
||||
assert_nil(@row.dig("Missing"))
|
||||
end
|
||||
|
||||
def test_dig_cell
|
||||
row = CSV::Row.new(%w{A}, [["foo", ["bar", ["baz"]]]])
|
||||
|
||||
assert_equal("foo", row.dig(0, 0))
|
||||
assert_equal("bar", row.dig(0, 1, 0))
|
||||
|
||||
assert_equal("foo", row.dig("A", 0))
|
||||
assert_equal("bar", row.dig("A", 1, 0))
|
||||
end
|
||||
|
||||
def test_dig_cell_no_dig
|
||||
row = CSV::Row.new(%w{A}, ["foo"])
|
||||
|
||||
assert_raise(TypeError) do
|
||||
row.dig(0, 0)
|
||||
end
|
||||
assert_raise(TypeError) do
|
||||
row.dig("A", 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
|
||||
# tc_table.rb
|
||||
#
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
# Copyright 2005 James Edward Gray II. You can redistribute or modify this code
|
||||
# under the terms of Ruby's license.
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
|
||||
require_relative "base"
|
||||
|
||||
|
@ -263,6 +261,15 @@ class TestCSV::Table < TestCSV
|
|||
@table.each { |row| assert_instance_of(CSV::Row, row) }
|
||||
end
|
||||
|
||||
def test_each_split
|
||||
yielded_values = []
|
||||
@table.each do |column1, column2, column3|
|
||||
yielded_values << [column1, column2, column3]
|
||||
end
|
||||
assert_equal(@rows.collect(&:to_a),
|
||||
yielded_values)
|
||||
end
|
||||
|
||||
def test_enumerable
|
||||
assert_equal( @rows.values_at(0, 2),
|
||||
@table.select { |row| (row["B"] % 2).zero? } )
|
||||
|
@ -312,7 +319,7 @@ class TestCSV::Table < TestCSV
|
|||
assert_equal(CSV::Row.new(%w[A B C], [13, 14, 15]), @table[-1])
|
||||
end
|
||||
|
||||
def test_delete_mixed
|
||||
def test_delete_mixed_one
|
||||
##################
|
||||
### Mixed Mode ###
|
||||
##################
|
||||
|
@ -330,6 +337,28 @@ class TestCSV::Table < TestCSV
|
|||
END_RESULT
|
||||
end
|
||||
|
||||
def test_delete_mixed_multiple
|
||||
##################
|
||||
### Mixed Mode ###
|
||||
##################
|
||||
# delete row and col
|
||||
second_row = @rows[1]
|
||||
a_col = @rows.map { |row| row["A"] }
|
||||
a_col_without_second_row = a_col[0..0] + a_col[2..-1]
|
||||
assert_equal([
|
||||
second_row,
|
||||
a_col_without_second_row,
|
||||
],
|
||||
@table.delete(1, "A"))
|
||||
|
||||
# verify resulting table
|
||||
assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv)
|
||||
B,C
|
||||
2,3
|
||||
8,9
|
||||
END_RESULT
|
||||
end
|
||||
|
||||
def test_delete_column
|
||||
###################
|
||||
### Column Mode ###
|
||||
|
@ -494,4 +523,70 @@ class TestCSV::Table < TestCSV
|
|||
@table.inspect.encoding],
|
||||
"inspect() was not ASCII compatible." )
|
||||
end
|
||||
|
||||
def test_dig_mixed
|
||||
# by row
|
||||
assert_equal(@rows[0], @table.dig(0))
|
||||
assert_nil(@table.dig(100)) # empty row
|
||||
|
||||
# by col
|
||||
assert_equal([2, 5, 8], @table.dig("B"))
|
||||
assert_equal([nil] * @rows.size, @table.dig("Z")) # empty col
|
||||
|
||||
# by row then col
|
||||
assert_equal(2, @table.dig(0, 1))
|
||||
assert_equal(6, @table.dig(1, "C"))
|
||||
|
||||
# by col then row
|
||||
assert_equal(5, @table.dig("B", 1))
|
||||
assert_equal(9, @table.dig("C", 2))
|
||||
end
|
||||
|
||||
def test_dig_by_column
|
||||
@table.by_col!
|
||||
|
||||
assert_equal([2, 5, 8], @table.dig(1))
|
||||
assert_equal([2, 5, 8], @table.dig("B"))
|
||||
|
||||
# by col then row
|
||||
assert_equal(5, @table.dig("B", 1))
|
||||
assert_equal(9, @table.dig("C", 2))
|
||||
end
|
||||
|
||||
def test_dig_by_row
|
||||
@table.by_row!
|
||||
|
||||
assert_equal(@rows[1], @table.dig(1))
|
||||
assert_raise(TypeError) { @table.dig("B") }
|
||||
|
||||
# by row then col
|
||||
assert_equal(2, @table.dig(0, 1))
|
||||
assert_equal(6, @table.dig(1, "C"))
|
||||
end
|
||||
|
||||
def test_dig_cell
|
||||
table = CSV::Table.new([CSV::Row.new(["A"], [["foo", ["bar", ["baz"]]]])])
|
||||
|
||||
# by row, col then cell
|
||||
assert_equal("foo", table.dig(0, "A", 0))
|
||||
assert_equal(["baz"], table.dig(0, "A", 1, 1))
|
||||
|
||||
# by col, row then cell
|
||||
assert_equal("foo", table.dig("A", 0, 0))
|
||||
assert_equal(["baz"], table.dig("A", 0, 1, 1))
|
||||
end
|
||||
|
||||
def test_dig_cell_no_dig
|
||||
table = CSV::Table.new([CSV::Row.new(["A"], ["foo"])])
|
||||
|
||||
# by row, col then cell
|
||||
assert_raise(TypeError) do
|
||||
table.dig(0, "A", 0)
|
||||
end
|
||||
|
||||
# by col, row then cell
|
||||
assert_raise(TypeError) do
|
||||
table.dig("A", 0, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,9 +4,7 @@
|
|||
|
||||
# ts_all.rb
|
||||
#
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
# Copyright 2005 James Edward Gray II. You can redistribute or modify this code
|
||||
# under the terms of Ruby's license.
|
||||
# Created by James Edward Gray II on 2005-10-31.
|
||||
|
||||
require "test/unit"
|
||||
|
||||
|
|
Loading…
Reference in a new issue