This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
class Array
|
||||
# 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
|
||||
@@ -0,0 +1,9 @@
|
||||
class String
|
||||
# Equivalent to CSV::parse_line(self, options)
|
||||
#
|
||||
# "CSV,data".parse_csv
|
||||
# #=> ["CSV", "data"]
|
||||
def parse_csv(**options)
|
||||
CSV.parse_line(self, **options)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,96 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CSV
|
||||
# Note: Don't use this class directly. This is an internal class.
|
||||
class FieldsConverter
|
||||
include Enumerable
|
||||
|
||||
NO_QUOTED_FIELDS = [] # :nodoc:
|
||||
def NO_QUOTED_FIELDS.[](_index)
|
||||
false
|
||||
end
|
||||
NO_QUOTED_FIELDS.freeze
|
||||
|
||||
#
|
||||
# A CSV::FieldsConverter is a data structure for storing the
|
||||
# fields converter properties to be passed as a parameter
|
||||
# when parsing a new file (e.g. CSV::Parser.new(@io, parser_options))
|
||||
#
|
||||
|
||||
def initialize(options={})
|
||||
@converters = []
|
||||
@nil_value = options[:nil_value]
|
||||
@empty_value = options[:empty_value]
|
||||
@empty_value_is_empty_string = (@empty_value == "")
|
||||
@accept_nil = options[:accept_nil]
|
||||
@builtin_converters_name = options[:builtin_converters_name]
|
||||
@need_static_convert = need_static_convert?
|
||||
end
|
||||
|
||||
def add_converter(name=nil, &converter)
|
||||
if name.nil? # custom converter
|
||||
@converters << converter
|
||||
else # named converter
|
||||
combo = builtin_converters[name]
|
||||
case combo
|
||||
when Array # combo converter
|
||||
combo.each do |sub_name|
|
||||
add_converter(sub_name)
|
||||
end
|
||||
else # individual named converter
|
||||
@converters << combo
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
@converters.each(&block)
|
||||
end
|
||||
|
||||
def empty?
|
||||
@converters.empty?
|
||||
end
|
||||
|
||||
def convert(fields, headers, lineno, quoted_fields=NO_QUOTED_FIELDS)
|
||||
return fields unless need_convert?
|
||||
|
||||
fields.collect.with_index do |field, index|
|
||||
if field.nil?
|
||||
field = @nil_value
|
||||
elsif field.is_a?(String) and field.empty?
|
||||
field = @empty_value unless @empty_value_is_empty_string
|
||||
end
|
||||
@converters.each do |converter|
|
||||
break if field.nil? and @accept_nil
|
||||
if converter.arity == 1 # straight field converter
|
||||
field = converter[field]
|
||||
else # FieldInfo converter
|
||||
if headers
|
||||
header = headers[index]
|
||||
else
|
||||
header = nil
|
||||
end
|
||||
quoted = quoted_fields[index]
|
||||
field = converter[field, FieldInfo.new(index, lineno, header, quoted)]
|
||||
end
|
||||
break unless field.is_a?(String) # short-circuit pipeline for speed
|
||||
end
|
||||
field # final state of each field, converted or original
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def need_static_convert?
|
||||
not (@nil_value.nil? and @empty_value_is_empty_string)
|
||||
end
|
||||
|
||||
def need_convert?
|
||||
@need_static_convert or
|
||||
(not @converters.empty?)
|
||||
end
|
||||
|
||||
def builtin_converters
|
||||
@builtin_converters ||= ::CSV.const_get(@builtin_converters_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,18 @@
|
||||
require "English"
|
||||
require "stringio"
|
||||
|
||||
class CSV
|
||||
module InputRecordSeparator
|
||||
class << self
|
||||
if RUBY_VERSION >= "3.0.0"
|
||||
def value
|
||||
"\n"
|
||||
end
|
||||
else
|
||||
def value
|
||||
$INPUT_RECORD_SEPARATOR
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,757 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "forwardable"
|
||||
|
||||
class CSV
|
||||
# = \CSV::Row
|
||||
# A \CSV::Row instance represents a \CSV table row.
|
||||
# (see {class CSV}[../CSV.html]).
|
||||
#
|
||||
# The instance may have:
|
||||
# - Fields: each is an object, not necessarily a \String.
|
||||
# - Headers: each serves a key, and also need not be a \String.
|
||||
#
|
||||
# === Instance Methods
|
||||
#
|
||||
# \CSV::Row has three groups of instance methods:
|
||||
# - Its own internally defined instance methods.
|
||||
# - Methods included by module Enumerable.
|
||||
# - Methods delegated to class Array.:
|
||||
# * Array#empty?
|
||||
# * Array#length
|
||||
# * Array#size
|
||||
#
|
||||
# == Creating a \CSV::Row Instance
|
||||
#
|
||||
# Commonly, a new \CSV::Row instance is created by parsing \CSV source
|
||||
# that has headers:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# table.each {|row| p row }
|
||||
# Output:
|
||||
# #<CSV::Row "Name":"foo" "Value":"0">
|
||||
# #<CSV::Row "Name":"bar" "Value":"1">
|
||||
# #<CSV::Row "Name":"baz" "Value":"2">
|
||||
#
|
||||
# You can also create a row directly. See ::new.
|
||||
#
|
||||
# == Headers
|
||||
#
|
||||
# Like a \CSV::Table, a \CSV::Row has headers.
|
||||
#
|
||||
# A \CSV::Row that was created by parsing \CSV source
|
||||
# inherits its headers from the table:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table.first
|
||||
# row.headers # => ["Name", "Value"]
|
||||
#
|
||||
# You can also create a new row with headers;
|
||||
# like the keys in a \Hash, the headers need not be Strings:
|
||||
# row = CSV::Row.new([:name, :value], ['foo', 0])
|
||||
# row.headers # => [:name, :value]
|
||||
#
|
||||
# The new row retains its headers even if added to a table
|
||||
# that has headers:
|
||||
# table << row # => #<CSV::Table mode:col_or_row row_count:5>
|
||||
# row.headers # => [:name, :value]
|
||||
# row[:name] # => "foo"
|
||||
# row['Name'] # => nil
|
||||
#
|
||||
#
|
||||
#
|
||||
# == Accessing Fields
|
||||
#
|
||||
# You may access a field in a \CSV::Row with either its \Integer index
|
||||
# (\Array-style) or its header (\Hash-style).
|
||||
#
|
||||
# Fetch a field using method #[]:
|
||||
# row = CSV::Row.new(['Name', 'Value'], ['foo', 0])
|
||||
# row[1] # => 0
|
||||
# row['Value'] # => 0
|
||||
#
|
||||
# Set a field using method #[]=:
|
||||
# row = CSV::Row.new(['Name', 'Value'], ['foo', 0])
|
||||
# row # => #<CSV::Row "Name":"foo" "Value":0>
|
||||
# row[0] = 'bar'
|
||||
# row['Value'] = 1
|
||||
# row # => #<CSV::Row "Name":"bar" "Value":1>
|
||||
#
|
||||
class Row
|
||||
# :call-seq:
|
||||
# CSV::Row.new(headers, fields, header_row = false) -> csv_row
|
||||
#
|
||||
# Returns the new \CSV::Row instance constructed from
|
||||
# arguments +headers+ and +fields+; both should be Arrays;
|
||||
# note that the fields need not be Strings:
|
||||
# row = CSV::Row.new(['Name', 'Value'], ['foo', 0])
|
||||
# row # => #<CSV::Row "Name":"foo" "Value":0>
|
||||
#
|
||||
# If the \Array lengths are different, the shorter is +nil+-filled:
|
||||
# row = CSV::Row.new(['Name', 'Value', 'Date', 'Size'], ['foo', 0])
|
||||
# row # => #<CSV::Row "Name":"foo" "Value":0 "Date":nil "Size":nil>
|
||||
#
|
||||
# Each \CSV::Row object is either a <i>field row</i> or a <i>header row</i>;
|
||||
# by default, a new row is a field row; for the row created above:
|
||||
# row.field_row? # => true
|
||||
# row.header_row? # => false
|
||||
#
|
||||
# If the optional argument +header_row+ is given as +true+,
|
||||
# the created row is a header row:
|
||||
# row = CSV::Row.new(['Name', 'Value'], ['foo', 0], header_row = true)
|
||||
# row # => #<CSV::Row "Name":"foo" "Value":0>
|
||||
# row.field_row? # => false
|
||||
# row.header_row? # => true
|
||||
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
|
||||
|
||||
# :call-seq:
|
||||
# row.initialize_copy(other_row) -> self
|
||||
#
|
||||
# Calls superclass method.
|
||||
def initialize_copy(other)
|
||||
super_return_value = super
|
||||
@row = @row.collect(&:dup)
|
||||
super_return_value
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.header_row? -> true or false
|
||||
#
|
||||
# Returns +true+ if this is a header row, +false+ otherwise.
|
||||
def header_row?
|
||||
@header_row
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.field_row? -> true or false
|
||||
#
|
||||
# Returns +true+ if this is a field row, +false+ otherwise.
|
||||
def field_row?
|
||||
not header_row?
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.headers -> array_of_headers
|
||||
#
|
||||
# Returns the headers for this row:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table.first
|
||||
# row.headers # => ["Name", "Value"]
|
||||
def headers
|
||||
@row.map(&:first)
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# field(index) -> value
|
||||
# field(header) -> value
|
||||
# field(header, offset) -> value
|
||||
#
|
||||
# Returns the field value for the given +index+ or +header+.
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# Fetch field value by \Integer index:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.field(0) # => "foo"
|
||||
# row.field(1) # => "bar"
|
||||
#
|
||||
# Counts backward from the last column if +index+ is negative:
|
||||
# row.field(-1) # => "0"
|
||||
# row.field(-2) # => "foo"
|
||||
#
|
||||
# Returns +nil+ if +index+ is out of range:
|
||||
# row.field(2) # => nil
|
||||
# row.field(-3) # => nil
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# Fetch field value by header (first found):
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.field('Name') # => "Foo"
|
||||
#
|
||||
# Fetch field value by header, ignoring +offset+ leading fields:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.field('Name', 2) # => "Baz"
|
||||
#
|
||||
# Returns +nil+ if the header does not exist.
|
||||
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].public_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) -> value
|
||||
# fetch(header, default) -> value
|
||||
# fetch(header) {|row| ... } -> value
|
||||
#
|
||||
# Returns the field value as specified by +header+.
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# With the single argument +header+, returns the field value
|
||||
# for that header (first found):
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.fetch('Name') # => "Foo"
|
||||
#
|
||||
# Raises exception +KeyError+ if the header does not exist.
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# With arguments +header+ and +default+ given,
|
||||
# returns the field value for the header (first found)
|
||||
# if the header exists, otherwise returns +default+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.fetch('Name', '') # => "Foo"
|
||||
# row.fetch(:nosuch, '') # => ""
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# With argument +header+ and a block given,
|
||||
# returns the field value for the header (first found)
|
||||
# if the header exists; otherwise calls the block
|
||||
# and returns its return value:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.fetch('Name') {|header| fail 'Cannot happen' } # => "Foo"
|
||||
# row.fetch(:nosuch) {|header| "Header '#{header} not found'" } # => "Header 'nosuch not found'"
|
||||
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
|
||||
|
||||
# :call-seq:
|
||||
# row.has_key?(header) -> true or false
|
||||
#
|
||||
# Returns +true+ if there is a field with the given +header+,
|
||||
# +false+ otherwise.
|
||||
def has_key?(header)
|
||||
!!@row.assoc(header)
|
||||
end
|
||||
alias_method :include?, :has_key?
|
||||
alias_method :key?, :has_key?
|
||||
alias_method :member?, :has_key?
|
||||
alias_method :header?, :has_key?
|
||||
|
||||
#
|
||||
# :call-seq:
|
||||
# row[index] = value -> value
|
||||
# row[header, offset] = value -> value
|
||||
# row[header] = value -> value
|
||||
#
|
||||
# Assigns the field value for the given +index+ or +header+;
|
||||
# returns +value+.
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# Assign field value by \Integer index:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row[0] = 'Bat'
|
||||
# row[1] = 3
|
||||
# row # => #<CSV::Row "Name":"Bat" "Value":3>
|
||||
#
|
||||
# Counts backward from the last column if +index+ is negative:
|
||||
# row[-1] = 4
|
||||
# row[-2] = 'Bam'
|
||||
# row # => #<CSV::Row "Name":"Bam" "Value":4>
|
||||
#
|
||||
# Extends the row with <tt>nil:nil</tt> if positive +index+ is not in the row:
|
||||
# row[4] = 5
|
||||
# row # => #<CSV::Row "Name":"bad" "Value":4 nil:nil nil:nil nil:5>
|
||||
#
|
||||
# Raises IndexError if negative +index+ is too small (too far from zero).
|
||||
#
|
||||
# ---
|
||||
#
|
||||
# Assign field value by header (first found):
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row['Name'] = 'Bat'
|
||||
# row # => #<CSV::Row "Name":"Bat" "Name":"Bar" "Name":"Baz">
|
||||
#
|
||||
# Assign field value by header, ignoring +offset+ leading fields:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row['Name', 2] = 4
|
||||
# row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":4>
|
||||
#
|
||||
# Append new field by (new) header:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row['New'] = 6
|
||||
# row# => #<CSV::Row "Name":"foo" "Value":"0" "New":6>
|
||||
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:
|
||||
# row << [header, value] -> self
|
||||
# row << hash -> self
|
||||
# row << value -> self
|
||||
#
|
||||
# Adds a field to +self+; returns +self+:
|
||||
#
|
||||
# If the argument is a 2-element \Array <tt>[header, value]</tt>,
|
||||
# a field is added with the given +header+ and +value+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row << ['NAME', 'Bat']
|
||||
# row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" "NAME":"Bat">
|
||||
#
|
||||
# If the argument is a \Hash, each <tt>key-value</tt> pair is added
|
||||
# as a field with header +key+ and value +value+.
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row << {NAME: 'Bat', name: 'Bam'}
|
||||
# row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" NAME:"Bat" name:"Bam">
|
||||
#
|
||||
# Otherwise, the given +value+ is added as a field with no header.
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row << 'Bag'
|
||||
# row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" nil:"Bag">
|
||||
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
|
||||
|
||||
# :call-seq:
|
||||
# row.push(*values) -> self
|
||||
#
|
||||
# Appends each of the given +values+ to +self+ as a field; returns +self+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.push('Bat', 'Bam')
|
||||
# row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" nil:"Bat" nil:"Bam">
|
||||
def push(*args)
|
||||
args.each { |arg| self << arg }
|
||||
|
||||
self # for chaining
|
||||
end
|
||||
|
||||
#
|
||||
# :call-seq:
|
||||
# delete(index) -> [header, value] or nil
|
||||
# delete(header) -> [header, value] or empty_array
|
||||
# delete(header, offset) -> [header, value] or empty_array
|
||||
#
|
||||
# Removes a specified field from +self+; returns the 2-element \Array
|
||||
# <tt>[header, value]</tt> if the field exists.
|
||||
#
|
||||
# If an \Integer argument +index+ is given,
|
||||
# removes and returns the field at offset +index+,
|
||||
# or returns +nil+ if the field does not exist:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.delete(1) # => ["Name", "Bar"]
|
||||
# row.delete(50) # => nil
|
||||
#
|
||||
# Otherwise, if the single argument +header+ is given,
|
||||
# removes and returns the first-found field with the given header,
|
||||
# of returns a new empty \Array if the field does not exist:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.delete('Name') # => ["Name", "Foo"]
|
||||
# row.delete('NAME') # => []
|
||||
#
|
||||
# If argument +header+ and \Integer argument +offset+ are given,
|
||||
# removes and returns the first-found field with the given header
|
||||
# whose +index+ is at least as large as +offset+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.delete('Name', 1) # => ["Name", "Bar"]
|
||||
# row.delete('NAME', 1) # => []
|
||||
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
|
||||
|
||||
# :call-seq:
|
||||
# row.delete_if {|header, value| ... } -> self
|
||||
#
|
||||
# Removes fields from +self+ as selected by the block; returns +self+.
|
||||
#
|
||||
# Removes each field for which the block returns a truthy value:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.delete_if {|header, value| value.start_with?('B') } # => true
|
||||
# row # => #<CSV::Row "Name":"Foo">
|
||||
# row.delete_if {|header, value| header.start_with?('B') } # => false
|
||||
#
|
||||
# If no block is given, returns a new Enumerator:
|
||||
# row.delete_if # => #<Enumerator: #<CSV::Row "Name":"Foo">:delete_if>
|
||||
def delete_if(&block)
|
||||
return enum_for(__method__) { size } unless block_given?
|
||||
|
||||
@row.delete_if(&block)
|
||||
|
||||
self # for chaining
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# self.fields(*specifiers) -> array_of_fields
|
||||
#
|
||||
# Returns field values per the given +specifiers+, which may be any mixture of:
|
||||
# - \Integer index.
|
||||
# - \Range of \Integer indexes.
|
||||
# - 2-element \Array containing a header and offset.
|
||||
# - Header.
|
||||
# - \Range of headers.
|
||||
#
|
||||
# For +specifier+ in one of the first four cases above,
|
||||
# returns the result of <tt>self.field(specifier)</tt>; see #field.
|
||||
#
|
||||
# Although there may be any number of +specifiers+,
|
||||
# the examples here will illustrate one at a time.
|
||||
#
|
||||
# When the specifier is an \Integer +index+,
|
||||
# returns <tt>self.field(index)</tt>L
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.fields(1) # => ["Bar"]
|
||||
#
|
||||
# When the specifier is a \Range of \Integers +range+,
|
||||
# returns <tt>self.field(range)</tt>:
|
||||
# row.fields(1..2) # => ["Bar", "Baz"]
|
||||
#
|
||||
# When the specifier is a 2-element \Array +array+,
|
||||
# returns <tt>self.field(array)</tt>L
|
||||
# row.fields('Name', 1) # => ["Foo", "Bar"]
|
||||
#
|
||||
# When the specifier is a header +header+,
|
||||
# returns <tt>self.field(header)</tt>L
|
||||
# row.fields('Name') # => ["Foo"]
|
||||
#
|
||||
# When the specifier is a \Range of headers +range+,
|
||||
# forms a new \Range +new_range+ from the indexes of
|
||||
# <tt>range.start</tt> and <tt>range.end</tt>,
|
||||
# and returns <tt>self.field(new_range)</tt>:
|
||||
# source = "Name,NAME,name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.fields('Name'..'NAME') # => ["Foo", "Bar"]
|
||||
#
|
||||
# Returns all fields if no argument given:
|
||||
# row.fields # => ["Foo", "Bar", "Baz"]
|
||||
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
|
||||
# index(header, offset) -> index
|
||||
#
|
||||
# Returns the index for the given header, if it exists;
|
||||
# otherwise returns +nil+.
|
||||
#
|
||||
# With the single argument +header+, returns the index
|
||||
# of the first-found field with the given +header+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.index('Name') # => 0
|
||||
# row.index('NAME') # => nil
|
||||
#
|
||||
# With arguments +header+ and +offset+,
|
||||
# returns the index of the first-found field with given +header+,
|
||||
# but ignoring the first +offset+ fields:
|
||||
# row.index('Name', 1) # => 1
|
||||
# row.index('Name', 3) # => nil
|
||||
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
|
||||
|
||||
# :call-seq:
|
||||
# row.field?(value) -> true or false
|
||||
#
|
||||
# Returns +true+ if +value+ is a field in this row, +false+ otherwise:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.field?('Bar') # => true
|
||||
# row.field?('BAR') # => false
|
||||
def field?(data)
|
||||
fields.include? data
|
||||
end
|
||||
|
||||
include Enumerable
|
||||
|
||||
# :call-seq:
|
||||
# row.each {|header, value| ... } -> self
|
||||
#
|
||||
# Calls the block with each header-value pair; returns +self+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.each {|header, value| p [header, value] }
|
||||
# Output:
|
||||
# ["Name", "Foo"]
|
||||
# ["Name", "Bar"]
|
||||
# ["Name", "Baz"]
|
||||
#
|
||||
# If no block is given, returns a new Enumerator:
|
||||
# row.each # => #<Enumerator: #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz">:each>
|
||||
def each(&block)
|
||||
return enum_for(__method__) { size } unless block_given?
|
||||
|
||||
@row.each(&block)
|
||||
|
||||
self # for chaining
|
||||
end
|
||||
|
||||
alias_method :each_pair, :each
|
||||
|
||||
# :call-seq:
|
||||
# row == other -> true or false
|
||||
#
|
||||
# Returns +true+ if +other+ is a /CSV::Row that has the same
|
||||
# fields (headers and values) in the same order as +self+;
|
||||
# otherwise returns +false+:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# other_row = table[0]
|
||||
# row == other_row # => true
|
||||
# other_row = table[1]
|
||||
# row == other_row # => false
|
||||
def ==(other)
|
||||
return @row == other.row if other.is_a? CSV::Row
|
||||
@row == other
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.to_h -> hash
|
||||
#
|
||||
# Returns the new \Hash formed by adding each header-value pair in +self+
|
||||
# as a key-value pair in the \Hash.
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.to_h # => {"Name"=>"foo", "Value"=>"0"}
|
||||
#
|
||||
# Header order is preserved, but repeated headers are ignored:
|
||||
# source = "Name,Name,Name\nFoo,Bar,Baz\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.to_h # => {"Name"=>"Foo"}
|
||||
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
|
||||
|
||||
# :call-seq:
|
||||
# row.deconstruct_keys(keys) -> hash
|
||||
#
|
||||
# Returns the new \Hash suitable for pattern matching containing only the
|
||||
# keys specified as an argument.
|
||||
def deconstruct_keys(keys)
|
||||
if keys.nil?
|
||||
to_h
|
||||
else
|
||||
keys.to_h { |key| [key, self[key]] }
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :to_ary, :to_a
|
||||
|
||||
# :call-seq:
|
||||
# row.deconstruct -> array
|
||||
#
|
||||
# Returns the new \Array suitable for pattern matching containing the values
|
||||
# of the row.
|
||||
def deconstruct
|
||||
fields
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# row.to_csv -> csv_string
|
||||
#
|
||||
# Returns the row as a \CSV String. Headers are not included:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.to_csv # => "foo,0\n"
|
||||
def to_csv(**options)
|
||||
fields.to_csv(**options)
|
||||
end
|
||||
alias_method :to_s, :to_csv
|
||||
|
||||
# :call-seq:
|
||||
# row.dig(index_or_header, *identifiers) -> object
|
||||
#
|
||||
# Finds and returns the object in nested object that is specified
|
||||
# by +index_or_header+ and +specifiers+.
|
||||
#
|
||||
# The nested objects may be instances of various classes.
|
||||
# See {Dig Methods}[rdoc-ref:dig_methods.rdoc].
|
||||
#
|
||||
# Examples:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.dig(1) # => "0"
|
||||
# row.dig('Value') # => "0"
|
||||
# row.dig(5) # => 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
|
||||
|
||||
# :call-seq:
|
||||
# row.inspect -> string
|
||||
#
|
||||
# Returns an ASCII-compatible \String showing:
|
||||
# - Class \CSV::Row.
|
||||
# - Header-value pairs.
|
||||
# Example:
|
||||
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
||||
# table = CSV.parse(source, headers: true)
|
||||
# row = table[0]
|
||||
# row.inspect # => "#<CSV::Row \"Name\":\"foo\" \"Value\":\"0\">"
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CSV
|
||||
# The version of the installed library.
|
||||
VERSION = "3.3.2"
|
||||
end
|
||||
@@ -0,0 +1,209 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative "input_record_separator"
|
||||
require_relative "row"
|
||||
|
||||
class CSV
|
||||
# Note: Don't use this class directly. This is an internal class.
|
||||
class Writer
|
||||
#
|
||||
# A CSV::Writer receives an output, prepares the header, format and output.
|
||||
# It allows us to write new rows in the object and rewind it.
|
||||
#
|
||||
attr_reader :lineno
|
||||
attr_reader :headers
|
||||
|
||||
def initialize(output, options)
|
||||
@output = output
|
||||
@options = options
|
||||
@lineno = 0
|
||||
@fields_converter = nil
|
||||
prepare
|
||||
if @options[:write_headers] and @headers
|
||||
self << @headers
|
||||
end
|
||||
@fields_converter = @options[:fields_converter]
|
||||
end
|
||||
|
||||
#
|
||||
# Adds a new row
|
||||
#
|
||||
def <<(row)
|
||||
case row
|
||||
when Row
|
||||
row = row.fields
|
||||
when Hash
|
||||
row = @headers.collect {|header| row[header]}
|
||||
end
|
||||
|
||||
@headers ||= row if @use_headers
|
||||
@lineno += 1
|
||||
|
||||
if @fields_converter
|
||||
row = @fields_converter.convert(row, nil, lineno)
|
||||
end
|
||||
|
||||
i = -1
|
||||
converted_row = row.collect do |field|
|
||||
i += 1
|
||||
quote(field, i)
|
||||
end
|
||||
line = converted_row.join(@column_separator) + @row_separator
|
||||
if @output_encoding
|
||||
line = line.encode(@output_encoding)
|
||||
end
|
||||
@output << line
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
#
|
||||
# Winds back to the beginning
|
||||
#
|
||||
def rewind
|
||||
@lineno = 0
|
||||
@headers = nil if @options[:headers].nil?
|
||||
end
|
||||
|
||||
private
|
||||
def prepare
|
||||
@encoding = @options[:encoding]
|
||||
|
||||
prepare_header
|
||||
prepare_format
|
||||
prepare_output
|
||||
end
|
||||
|
||||
def prepare_header
|
||||
headers = @options[:headers]
|
||||
case headers
|
||||
when Array
|
||||
@headers = headers
|
||||
@use_headers = true
|
||||
when String
|
||||
@headers = CSV.parse_line(headers,
|
||||
col_sep: @options[:column_separator],
|
||||
row_sep: @options[:row_separator],
|
||||
quote_char: @options[:quote_character])
|
||||
@use_headers = true
|
||||
when true
|
||||
@headers = nil
|
||||
@use_headers = true
|
||||
else
|
||||
@headers = nil
|
||||
@use_headers = false
|
||||
end
|
||||
return unless @headers
|
||||
|
||||
converter = @options[:header_fields_converter]
|
||||
@headers = converter.convert(@headers, nil, 0, [])
|
||||
@headers.each do |header|
|
||||
header.freeze if header.is_a?(String)
|
||||
end
|
||||
end
|
||||
|
||||
def prepare_force_quotes_fields(force_quotes)
|
||||
@force_quotes_fields = {}
|
||||
force_quotes.each do |name_or_index|
|
||||
case name_or_index
|
||||
when Integer
|
||||
index = name_or_index
|
||||
@force_quotes_fields[index] = true
|
||||
when String, Symbol
|
||||
name = name_or_index.to_s
|
||||
if @headers.nil?
|
||||
message = ":headers is required when you use field name " +
|
||||
"in :force_quotes: " +
|
||||
"#{name_or_index.inspect}: #{force_quotes.inspect}"
|
||||
raise ArgumentError, message
|
||||
end
|
||||
index = @headers.index(name)
|
||||
next if index.nil?
|
||||
@force_quotes_fields[index] = true
|
||||
else
|
||||
message = ":force_quotes element must be " +
|
||||
"field index or field name: " +
|
||||
"#{name_or_index.inspect}: #{force_quotes.inspect}"
|
||||
raise ArgumentError, message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def prepare_format
|
||||
@column_separator = @options[:column_separator].to_s.encode(@encoding)
|
||||
row_separator = @options[:row_separator]
|
||||
if row_separator == :auto
|
||||
@row_separator = InputRecordSeparator.value.encode(@encoding)
|
||||
else
|
||||
@row_separator = row_separator.to_s.encode(@encoding)
|
||||
end
|
||||
@quote_character = @options[:quote_character]
|
||||
force_quotes = @options[:force_quotes]
|
||||
if force_quotes.is_a?(Array)
|
||||
prepare_force_quotes_fields(force_quotes)
|
||||
@force_quotes = false
|
||||
elsif force_quotes
|
||||
@force_quotes_fields = nil
|
||||
@force_quotes = true
|
||||
else
|
||||
@force_quotes_fields = nil
|
||||
@force_quotes = false
|
||||
end
|
||||
unless @force_quotes
|
||||
@quotable_pattern =
|
||||
Regexp.new("[\r\n".encode(@encoding) +
|
||||
Regexp.escape(@column_separator) +
|
||||
Regexp.escape(@quote_character.encode(@encoding)) +
|
||||
"]".encode(@encoding))
|
||||
end
|
||||
@quote_empty = @options.fetch(:quote_empty, true)
|
||||
end
|
||||
|
||||
def prepare_output
|
||||
@output_encoding = nil
|
||||
return unless @output.is_a?(StringIO)
|
||||
|
||||
output_encoding = @output.internal_encoding || @output.external_encoding
|
||||
if @encoding != output_encoding
|
||||
if @options[:force_encoding]
|
||||
@output_encoding = output_encoding
|
||||
else
|
||||
compatible_encoding = Encoding.compatible?(@encoding, output_encoding)
|
||||
if compatible_encoding
|
||||
@output.set_encoding(compatible_encoding)
|
||||
@output.seek(0, IO::SEEK_END)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def quote_field(field)
|
||||
field = String(field)
|
||||
encoded_quote_character = @quote_character.encode(field.encoding)
|
||||
encoded_quote_character +
|
||||
field.gsub(encoded_quote_character,
|
||||
encoded_quote_character * 2) +
|
||||
encoded_quote_character
|
||||
end
|
||||
|
||||
def quote(field, i)
|
||||
if @force_quotes
|
||||
quote_field(field)
|
||||
elsif @force_quotes_fields and @force_quotes_fields[i]
|
||||
quote_field(field)
|
||||
else
|
||||
if field.nil? # represent +nil+ fields as empty unquoted fields
|
||||
""
|
||||
else
|
||||
field = String(field) # Stringify fields
|
||||
# represent empty fields as empty quoted fields
|
||||
if (@quote_empty and field.empty?) or (field.valid_encoding? and @quotable_pattern.match?(field))
|
||||
quote_field(field)
|
||||
else
|
||||
field # unquoted field
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user