# frozen_string_literal: true
# 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.
# Constructs 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
# 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 assumes to be a field row.
# A CSV::Row object supports the following Array methods through delegation:
def initialize(headers, fields, header_row = false)
headers.each { |h| h.freeze if h.is_a? String }
# handle extra headers or fields
@row = if headers.size >= fields.size
fields.zip(headers).each(&:reverse!)
# Internal data format used to compare equality.
def_delegators :@row, :empty?, :length, :size
def initialize_copy(other)
# Returns +true+ if this is a header row.
# Returns +true+ if this is a field row.
# Returns the headers of this row.
# field( header, offset )
# 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)
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
header_or_index.is_a?(Range) ? pair.map(&:last) : pair.last
# 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)
raise KeyError, "key not found: #{header}"
# Returns +true+ if there is a field with the given +header+.
alias_method :include?, :has_key?
alias_method :key?, :has_key?
alias_method :member?, :has_key?
alias_method :header?, :has_key?
# []=( header, offset, value )
# Looks up the field by the semantics described in CSV::Row.field() and
# 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
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
if index.nil? # appending a field
self << [args.first, value]
else # normal header assignment
# <<( 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.
if arg.is_a?(Array) and arg.size == 2 # appending a header and name
elsif arg.is_a?(Hash) # append header and name pairs
arg.each { |pair| @row << pair }
else # append field value
# A shortcut for appending multiple fields. Equivalent to:
# args.each { |arg| csv_row << arg }
# This method returns the row for chaining.
args.each { |arg| self << arg }
# delete( header, offset )
# Removes 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
# 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
# This method returns the row for chaining.
# If no block is given, an Enumerator is returned.
return enum_for(__method__) { size } unless block_given?
# 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
# 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
else # or work like values_at()
headers_and_or_indices.each do |h_or_i|
index_begin = h_or_i.begin.is_a?(Integer) ? h_or_i.begin :
index_end = h_or_i.end.is_a?(Integer) ? h_or_i.end :
new_range = h_or_i.exclude_end? ? (index_begin...index_end) :
all.concat(fields.values_at(new_range))
all << field(*Array(h_or_i))
alias_method :values_at, :fields
# 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
def index(header, minimum_index = 0)
index = headers[minimum_index..-1].index(header)
# return the index at the right offset, if we found one
index.nil? ? nil : index + minimum_index
# Returns +true+ if +data+ matches a field in this row, and +false+
# 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.
return enum_for(__method__) { size } unless block_given?
alias_method :each_pair, :each
# Returns +true+ if this row contains the same headers and fields in the
return @row == other.row if other.is_a? CSV::Row
# Collapses the row into a simple Hash. Be warned that this discards field
# order and clobbers duplicate fields.
hash[key] = self[key] unless hash.key?(key)
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 )
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)
unless value.respond_to?(:dig)
raise TypeError, "#{value.class} does not have \#dig method"
# A summary of fields, by header, in an ASCII compatible String.
str = ["#<", self.class.to_s]
str << " " << (header.is_a?(Symbol) ? header.to_s : header.inspect) <<
rescue # any encoding error
e = Encoding::Converter.asciicompat_encoding(s.encoding)
e ? s.encode(e) : s.force_encoding("ASCII-8BIT")