csv 3.1.7 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/csv/row.rb CHANGED
@@ -3,30 +3,105 @@
3
3
  require "forwardable"
4
4
 
5
5
  class CSV
6
+ # = \CSV::Row
7
+ # A \CSV::Row instance represents a \CSV table row.
8
+ # (see {class CSV}[../CSV.html]).
6
9
  #
7
- # A CSV::Row is part Array and part Hash. It retains an order for the fields
8
- # and allows duplicates just as an Array would, but also allows you to access
9
- # fields by name just as you could if they were in a Hash.
10
+ # The instance may have:
11
+ # - Fields: each is an object, not necessarily a \String.
12
+ # - Headers: each serves a key, and also need not be a \String.
10
13
  #
11
- # All rows returned by CSV will be constructed from this class, if header row
12
- # processing is activated.
14
+ # === Instance Methods
15
+ #
16
+ # \CSV::Row has three groups of instance methods:
17
+ # - Its own internally defined instance methods.
18
+ # - Methods included by module Enumerable.
19
+ # - Methods delegated to class Array.:
20
+ # * Array#empty?
21
+ # * Array#length
22
+ # * Array#size
23
+ #
24
+ # == Creating a \CSV::Row Instance
25
+ #
26
+ # Commonly, a new \CSV::Row instance is created by parsing \CSV source
27
+ # that has headers:
28
+ # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
29
+ # table = CSV.parse(source, headers: true)
30
+ # table.each {|row| p row }
31
+ # Output:
32
+ # #<CSV::Row "Name":"foo" "Value":"0">
33
+ # #<CSV::Row "Name":"bar" "Value":"1">
34
+ # #<CSV::Row "Name":"baz" "Value":"2">
35
+ #
36
+ # You can also create a row directly. See ::new.
37
+ #
38
+ # == Headers
39
+ #
40
+ # Like a \CSV::Table, a \CSV::Row has headers.
41
+ #
42
+ # A \CSV::Row that was created by parsing \CSV source
43
+ # inherits its headers from the table:
44
+ # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
45
+ # table = CSV.parse(source, headers: true)
46
+ # row = table.first
47
+ # row.headers # => ["Name", "Value"]
48
+ #
49
+ # You can also create a new row with headers;
50
+ # like the keys in a \Hash, the headers need not be Strings:
51
+ # row = CSV::Row.new([:name, :value], ['foo', 0])
52
+ # row.headers # => [:name, :value]
53
+ #
54
+ # The new row retains its headers even if added to a table
55
+ # that has headers:
56
+ # table << row # => #<CSV::Table mode:col_or_row row_count:5>
57
+ # row.headers # => [:name, :value]
58
+ # row[:name] # => "foo"
59
+ # row['Name'] # => nil
60
+ #
61
+ #
62
+ #
63
+ # == Accessing Fields
64
+ #
65
+ # You may access a field in a \CSV::Row with either its \Integer index
66
+ # (\Array-style) or its header (\Hash-style).
67
+ #
68
+ # Fetch a field using method #[]:
69
+ # row = CSV::Row.new(['Name', 'Value'], ['foo', 0])
70
+ # row[1] # => 0
71
+ # row['Value'] # => 0
72
+ #
73
+ # Set a field using method #[]=:
74
+ # row = CSV::Row.new(['Name', 'Value'], ['foo', 0])
75
+ # row # => #<CSV::Row "Name":"foo" "Value":0>
76
+ # row[0] = 'bar'
77
+ # row['Value'] = 1
78
+ # row # => #<CSV::Row "Name":"bar" "Value":1>
13
79
  #
14
80
  class Row
15
- #
16
- # Constructs a new CSV::Row from +headers+ and +fields+, which are expected
17
- # to be Arrays. If one Array is shorter than the other, it will be padded
18
- # with +nil+ objects.
19
- #
20
- # The optional +header_row+ parameter can be set to +true+ to indicate, via
21
- # CSV::Row.header_row?() and CSV::Row.field_row?(), that this is a header
22
- # row. Otherwise, the row assumes to be a field row.
23
- #
24
- # A CSV::Row object supports the following Array methods through delegation:
25
- #
26
- # * empty?()
27
- # * length()
28
- # * size()
29
- #
81
+ # :call-seq:
82
+ # CSV::Row.new(headers, fields, header_row = false) -> csv_row
83
+ #
84
+ # Returns the new \CSV::Row instance constructed from
85
+ # arguments +headers+ and +fields+; both should be Arrays;
86
+ # note that the fields need not be Strings:
87
+ # row = CSV::Row.new(['Name', 'Value'], ['foo', 0])
88
+ # row # => #<CSV::Row "Name":"foo" "Value":0>
89
+ #
90
+ # If the \Array lengths are different, the shorter is +nil+-filled:
91
+ # row = CSV::Row.new(['Name', 'Value', 'Date', 'Size'], ['foo', 0])
92
+ # row # => #<CSV::Row "Name":"foo" "Value":0 "Date":nil "Size":nil>
93
+ #
94
+ # Each \CSV::Row object is either a <i>field row</i> or a <i>header row</i>;
95
+ # by default, a new row is a field row; for the row created above:
96
+ # row.field_row? # => true
97
+ # row.header_row? # => false
98
+ #
99
+ # If the optional argument +header_row+ is given as +true+,
100
+ # the created row is a header row:
101
+ # row = CSV::Row.new(['Name', 'Value'], ['foo', 0], header_row = true)
102
+ # row # => #<CSV::Row "Name":"foo" "Value":0>
103
+ # row.field_row? # => false
104
+ # row.header_row? # => true
30
105
  def initialize(headers, fields, header_row = false)
31
106
  @header_row = header_row
32
107
  headers.each { |h| h.freeze if h.is_a? String }
@@ -48,43 +123,87 @@ class CSV
48
123
  extend Forwardable
49
124
  def_delegators :@row, :empty?, :length, :size
50
125
 
126
+ # :call-seq:
127
+ # row.initialize_copy(other_row) -> self
128
+ #
129
+ # Calls superclass method.
51
130
  def initialize_copy(other)
52
- super
131
+ super_return_value = super
53
132
  @row = @row.collect(&:dup)
133
+ super_return_value
54
134
  end
55
135
 
56
- # Returns +true+ if this is a header row.
136
+ # :call-seq:
137
+ # row.header_row? -> true or false
138
+ #
139
+ # Returns +true+ if this is a header row, +false+ otherwise.
57
140
  def header_row?
58
141
  @header_row
59
142
  end
60
143
 
61
- # Returns +true+ if this is a field row.
144
+ # :call-seq:
145
+ # row.field_row? -> true or false
146
+ #
147
+ # Returns +true+ if this is a field row, +false+ otherwise.
62
148
  def field_row?
63
149
  not header_row?
64
150
  end
65
151
 
66
- # Returns the headers of this row.
152
+ # :call-seq:
153
+ # row.headers -> array_of_headers
154
+ #
155
+ # Returns the headers for this row:
156
+ # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
157
+ # table = CSV.parse(source, headers: true)
158
+ # row = table.first
159
+ # row.headers # => ["Name", "Value"]
67
160
  def headers
68
161
  @row.map(&:first)
69
162
  end
70
163
 
71
- #
72
164
  # :call-seq:
73
- # field( header )
74
- # field( header, offset )
75
- # field( index )
165
+ # field(index) -> value
166
+ # field(header) -> value
167
+ # field(header, offset) -> value
168
+ #
169
+ # Returns the field value for the given +index+ or +header+.
170
+ #
171
+ # ---
172
+ #
173
+ # Fetch field value by \Integer index:
174
+ # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
175
+ # table = CSV.parse(source, headers: true)
176
+ # row = table[0]
177
+ # row.field(0) # => "foo"
178
+ # row.field(1) # => "bar"
179
+ #
180
+ # Counts backward from the last column if +index+ is negative:
181
+ # row.field(-1) # => "0"
182
+ # row.field(-2) # => "foo"
183
+ #
184
+ # Returns +nil+ if +index+ is out of range:
185
+ # row.field(2) # => nil
186
+ # row.field(-3) # => nil
76
187
  #
77
- # This method will return the field value by +header+ or +index+. If a field
78
- # is not found, +nil+ is returned.
188
+ # ---
79
189
  #
80
- # When provided, +offset+ ensures that a header match occurs on or later
81
- # than the +offset+ index. You can use this to find duplicate headers,
82
- # without resorting to hard-coding exact indices.
190
+ # Fetch field value by header (first found):
191
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
192
+ # table = CSV.parse(source, headers: true)
193
+ # row = table[0]
194
+ # row.field('Name') # => "Foo"
83
195
  #
196
+ # Fetch field value by header, ignoring +offset+ leading fields:
197
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
198
+ # table = CSV.parse(source, headers: true)
199
+ # row = table[0]
200
+ # row.field('Name', 2) # => "Baz"
201
+ #
202
+ # Returns +nil+ if the header does not exist.
84
203
  def field(header_or_index, minimum_index = 0)
85
204
  # locate the pair
86
205
  finder = (header_or_index.is_a?(Integer) || header_or_index.is_a?(Range)) ? :[] : :assoc
87
- pair = @row[minimum_index..-1].send(finder, header_or_index)
206
+ pair = @row[minimum_index..-1].public_send(finder, header_or_index)
88
207
 
89
208
  # return the field if we have a pair
90
209
  if pair.nil?
@@ -97,16 +216,45 @@ class CSV
97
216
 
98
217
  #
99
218
  # :call-seq:
100
- # fetch( header )
101
- # fetch( header ) { |row| ... }
102
- # fetch( header, default )
103
- #
104
- # This method will fetch the field value by +header+. It has the same
105
- # behavior as Hash#fetch: if there is a field with the given +header+, its
106
- # value is returned. Otherwise, if a block is given, it is yielded the
107
- # +header+ and its result is returned; if a +default+ is given as the
108
- # second argument, it is returned; otherwise a KeyError is raised.
109
- #
219
+ # fetch(header) -> value
220
+ # fetch(header, default) -> value
221
+ # fetch(header) {|row| ... } -> value
222
+ #
223
+ # Returns the field value as specified by +header+.
224
+ #
225
+ # ---
226
+ #
227
+ # With the single argument +header+, returns the field value
228
+ # for that header (first found):
229
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
230
+ # table = CSV.parse(source, headers: true)
231
+ # row = table[0]
232
+ # row.fetch('Name') # => "Foo"
233
+ #
234
+ # Raises exception +KeyError+ if the header does not exist.
235
+ #
236
+ # ---
237
+ #
238
+ # With arguments +header+ and +default+ given,
239
+ # returns the field value for the header (first found)
240
+ # if the header exists, otherwise returns +default+:
241
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
242
+ # table = CSV.parse(source, headers: true)
243
+ # row = table[0]
244
+ # row.fetch('Name', '') # => "Foo"
245
+ # row.fetch(:nosuch, '') # => ""
246
+ #
247
+ # ---
248
+ #
249
+ # With argument +header+ and a block given,
250
+ # returns the field value for the header (first found)
251
+ # if the header exists; otherwise calls the block
252
+ # and returns its return value:
253
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
254
+ # table = CSV.parse(source, headers: true)
255
+ # row = table[0]
256
+ # row.fetch('Name') {|header| fail 'Cannot happen' } # => "Foo"
257
+ # row.fetch(:nosuch) {|header| "Header '#{header} not found'" } # => "Header 'nosuch not found'"
110
258
  def fetch(header, *varargs)
111
259
  raise ArgumentError, "Too many arguments" if varargs.length > 1
112
260
  pair = @row.assoc(header)
@@ -123,7 +271,11 @@ class CSV
123
271
  end
124
272
  end
125
273
 
126
- # Returns +true+ if there is a field with the given +header+.
274
+ # :call-seq:
275
+ # row.has_key?(header) -> true or false
276
+ #
277
+ # Returns +true+ if there is a field with the given +header+,
278
+ # +false+ otherwise.
127
279
  def has_key?(header)
128
280
  !!@row.assoc(header)
129
281
  end
@@ -134,17 +286,56 @@ class CSV
134
286
 
135
287
  #
136
288
  # :call-seq:
137
- # []=( header, value )
138
- # []=( header, offset, value )
139
- # []=( index, value )
140
- #
141
- # Looks up the field by the semantics described in CSV::Row.field() and
142
- # assigns the +value+.
143
- #
144
- # Assigning past the end of the row with an index will set all pairs between
145
- # to <tt>[nil, nil]</tt>. Assigning to an unused header appends the new
146
- # pair.
147
- #
289
+ # row[index] = value -> value
290
+ # row[header, offset] = value -> value
291
+ # row[header] = value -> value
292
+ #
293
+ # Assigns the field value for the given +index+ or +header+;
294
+ # returns +value+.
295
+ #
296
+ # ---
297
+ #
298
+ # Assign field value by \Integer index:
299
+ # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
300
+ # table = CSV.parse(source, headers: true)
301
+ # row = table[0]
302
+ # row[0] = 'Bat'
303
+ # row[1] = 3
304
+ # row # => #<CSV::Row "Name":"Bat" "Value":3>
305
+ #
306
+ # Counts backward from the last column if +index+ is negative:
307
+ # row[-1] = 4
308
+ # row[-2] = 'Bam'
309
+ # row # => #<CSV::Row "Name":"Bam" "Value":4>
310
+ #
311
+ # Extends the row with <tt>nil:nil</tt> if positive +index+ is not in the row:
312
+ # row[4] = 5
313
+ # row # => #<CSV::Row "Name":"bad" "Value":4 nil:nil nil:nil nil:5>
314
+ #
315
+ # Raises IndexError if negative +index+ is too small (too far from zero).
316
+ #
317
+ # ---
318
+ #
319
+ # Assign field value by header (first found):
320
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
321
+ # table = CSV.parse(source, headers: true)
322
+ # row = table[0]
323
+ # row['Name'] = 'Bat'
324
+ # row # => #<CSV::Row "Name":"Bat" "Name":"Bar" "Name":"Baz">
325
+ #
326
+ # Assign field value by header, ignoring +offset+ leading fields:
327
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
328
+ # table = CSV.parse(source, headers: true)
329
+ # row = table[0]
330
+ # row['Name', 2] = 4
331
+ # row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":4>
332
+ #
333
+ # Append new field by (new) header:
334
+ # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
335
+ # table = CSV.parse(source, headers: true)
336
+ # row = table[0]
337
+ # row['New'] = 6
338
+ # row# => #<CSV::Row "Name":"foo" "Value":"0" "New":6>
148
339
  def []=(*args)
149
340
  value = args.pop
150
341
 
@@ -167,17 +358,34 @@ class CSV
167
358
 
168
359
  #
169
360
  # :call-seq:
170
- # <<( field )
171
- # <<( header_and_field_array )
172
- # <<( header_and_field_hash )
173
- #
174
- # If a two-element Array is provided, it is assumed to be a header and field
175
- # and the pair is appended. A Hash works the same way with the key being
176
- # the header and the value being the field. Anything else is assumed to be
177
- # a lone field which is appended with a +nil+ header.
178
- #
179
- # This method returns the row for chaining.
180
- #
361
+ # row << [header, value] -> self
362
+ # row << hash -> self
363
+ # row << value -> self
364
+ #
365
+ # Adds a field to +self+; returns +self+:
366
+ #
367
+ # If the argument is a 2-element \Array <tt>[header, value]</tt>,
368
+ # a field is added with the given +header+ and +value+:
369
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
370
+ # table = CSV.parse(source, headers: true)
371
+ # row = table[0]
372
+ # row << ['NAME', 'Bat']
373
+ # row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" "NAME":"Bat">
374
+ #
375
+ # If the argument is a \Hash, each <tt>key-value</tt> pair is added
376
+ # as a field with header +key+ and value +value+.
377
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
378
+ # table = CSV.parse(source, headers: true)
379
+ # row = table[0]
380
+ # row << {NAME: 'Bat', name: 'Bam'}
381
+ # row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" NAME:"Bat" name:"Bam">
382
+ #
383
+ # Otherwise, the given +value+ is added as a field with no header.
384
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
385
+ # table = CSV.parse(source, headers: true)
386
+ # row = table[0]
387
+ # row << 'Bag'
388
+ # row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" nil:"Bag">
181
389
  def <<(arg)
182
390
  if arg.is_a?(Array) and arg.size == 2 # appending a header and name
183
391
  @row << arg
@@ -190,13 +398,15 @@ class CSV
190
398
  self # for chaining
191
399
  end
192
400
 
193
- #
194
- # A shortcut for appending multiple fields. Equivalent to:
195
- #
196
- # args.each { |arg| csv_row << arg }
197
- #
198
- # This method returns the row for chaining.
199
- #
401
+ # :call-seq:
402
+ # row.push(*values) -> self
403
+ #
404
+ # Appends each of the given +values+ to +self+ as a field; returns +self+:
405
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
406
+ # table = CSV.parse(source, headers: true)
407
+ # row = table[0]
408
+ # row.push('Bat', 'Bam')
409
+ # row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" nil:"Bat" nil:"Bam">
200
410
  def push(*args)
201
411
  args.each { |arg| self << arg }
202
412
 
@@ -205,14 +415,39 @@ class CSV
205
415
 
206
416
  #
207
417
  # :call-seq:
208
- # delete( header )
209
- # delete( header, offset )
210
- # delete( index )
211
- #
212
- # Removes a pair from the row by +header+ or +index+. The pair is
213
- # located as described in CSV::Row.field(). The deleted pair is returned,
214
- # or +nil+ if a pair could not be found.
215
- #
418
+ # delete(index) -> [header, value] or nil
419
+ # delete(header) -> [header, value] or empty_array
420
+ # delete(header, offset) -> [header, value] or empty_array
421
+ #
422
+ # Removes a specified field from +self+; returns the 2-element \Array
423
+ # <tt>[header, value]</tt> if the field exists.
424
+ #
425
+ # If an \Integer argument +index+ is given,
426
+ # removes and returns the field at offset +index+,
427
+ # or returns +nil+ if the field does not exist:
428
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
429
+ # table = CSV.parse(source, headers: true)
430
+ # row = table[0]
431
+ # row.delete(1) # => ["Name", "Bar"]
432
+ # row.delete(50) # => nil
433
+ #
434
+ # Otherwise, if the single argument +header+ is given,
435
+ # removes and returns the first-found field with the given header,
436
+ # of returns a new empty \Array if the field does not exist:
437
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
438
+ # table = CSV.parse(source, headers: true)
439
+ # row = table[0]
440
+ # row.delete('Name') # => ["Name", "Foo"]
441
+ # row.delete('NAME') # => []
442
+ #
443
+ # If argument +header+ and \Integer argument +offset+ are given,
444
+ # removes and returns the first-found field with the given header
445
+ # whose +index+ is at least as large as +offset+:
446
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
447
+ # table = CSV.parse(source, headers: true)
448
+ # row = table[0]
449
+ # row.delete('Name', 1) # => ["Name", "Bar"]
450
+ # row.delete('NAME', 1) # => []
216
451
  def delete(header_or_index, minimum_index = 0)
217
452
  if header_or_index.is_a? Integer # by index
218
453
  @row.delete_at(header_or_index)
@@ -223,15 +458,21 @@ class CSV
223
458
  end
224
459
  end
225
460
 
461
+ # :call-seq:
462
+ # row.delete_if {|header, value| ... } -> self
226
463
  #
227
- # The provided +block+ is passed a header and field for each pair in the row
228
- # and expected to return +true+ or +false+, depending on whether the pair
229
- # should be deleted.
230
- #
231
- # This method returns the row for chaining.
464
+ # Removes fields from +self+ as selected by the block; returns +self+.
232
465
  #
233
- # If no block is given, an Enumerator is returned.
466
+ # Removes each field for which the block returns a truthy value:
467
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
468
+ # table = CSV.parse(source, headers: true)
469
+ # row = table[0]
470
+ # row.delete_if {|header, value| value.start_with?('B') } # => true
471
+ # row # => #<CSV::Row "Name":"Foo">
472
+ # row.delete_if {|header, value| header.start_with?('B') } # => false
234
473
  #
474
+ # If no block is given, returns a new Enumerator:
475
+ # row.delete_if # => #<Enumerator: #<CSV::Row "Name":"Foo">:delete_if>
235
476
  def delete_if(&block)
236
477
  return enum_for(__method__) { size } unless block_given?
237
478
 
@@ -240,14 +481,52 @@ class CSV
240
481
  self # for chaining
241
482
  end
242
483
 
243
- #
244
- # This method accepts any number of arguments which can be headers, indices,
245
- # Ranges of either, or two-element Arrays containing a header and offset.
246
- # Each argument will be replaced with a field lookup as described in
247
- # CSV::Row.field().
248
- #
249
- # If called with no arguments, all fields are returned.
250
- #
484
+ # :call-seq:
485
+ # self.fields(*specifiers) -> array_of_fields
486
+ #
487
+ # Returns field values per the given +specifiers+, which may be any mixture of:
488
+ # - \Integer index.
489
+ # - \Range of \Integer indexes.
490
+ # - 2-element \Array containing a header and offset.
491
+ # - Header.
492
+ # - \Range of headers.
493
+ #
494
+ # For +specifier+ in one of the first four cases above,
495
+ # returns the result of <tt>self.field(specifier)</tt>; see #field.
496
+ #
497
+ # Although there may be any number of +specifiers+,
498
+ # the examples here will illustrate one at a time.
499
+ #
500
+ # When the specifier is an \Integer +index+,
501
+ # returns <tt>self.field(index)</tt>L
502
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
503
+ # table = CSV.parse(source, headers: true)
504
+ # row = table[0]
505
+ # row.fields(1) # => ["Bar"]
506
+ #
507
+ # When the specifier is a \Range of \Integers +range+,
508
+ # returns <tt>self.field(range)</tt>:
509
+ # row.fields(1..2) # => ["Bar", "Baz"]
510
+ #
511
+ # When the specifier is a 2-element \Array +array+,
512
+ # returns <tt>self.field(array)</tt>L
513
+ # row.fields('Name', 1) # => ["Foo", "Bar"]
514
+ #
515
+ # When the specifier is a header +header+,
516
+ # returns <tt>self.field(header)</tt>L
517
+ # row.fields('Name') # => ["Foo"]
518
+ #
519
+ # When the specifier is a \Range of headers +range+,
520
+ # forms a new \Range +new_range+ from the indexes of
521
+ # <tt>range.start</tt> and <tt>range.end</tt>,
522
+ # and returns <tt>self.field(new_range)</tt>:
523
+ # source = "Name,NAME,name\nFoo,Bar,Baz\n"
524
+ # table = CSV.parse(source, headers: true)
525
+ # row = table[0]
526
+ # row.fields('Name'..'NAME') # => ["Foo", "Bar"]
527
+ #
528
+ # Returns all fields if no argument given:
529
+ # row.fields # => ["Foo", "Bar", "Baz"]
251
530
  def fields(*headers_and_or_indices)
252
531
  if headers_and_or_indices.empty? # return all fields--no arguments
253
532
  @row.map(&:last)
@@ -271,15 +550,26 @@ class CSV
271
550
  end
272
551
  alias_method :values_at, :fields
273
552
 
274
- #
275
553
  # :call-seq:
276
- # index( header )
277
- # index( header, offset )
278
- #
279
- # This method will return the index of a field with the provided +header+.
280
- # The +offset+ can be used to locate duplicate header names, as described in
281
- # CSV::Row.field().
282
- #
554
+ # index(header) -> index
555
+ # index(header, offset) -> index
556
+ #
557
+ # Returns the index for the given header, if it exists;
558
+ # otherwise returns +nil+.
559
+ #
560
+ # With the single argument +header+, returns the index
561
+ # of the first-found field with the given +header+:
562
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
563
+ # table = CSV.parse(source, headers: true)
564
+ # row = table[0]
565
+ # row.index('Name') # => 0
566
+ # row.index('NAME') # => nil
567
+ #
568
+ # With arguments +header+ and +offset+,
569
+ # returns the index of the first-found field with given +header+,
570
+ # but ignoring the first +offset+ fields:
571
+ # row.index('Name', 1) # => 1
572
+ # row.index('Name', 3) # => nil
283
573
  def index(header, minimum_index = 0)
284
574
  # find the pair
285
575
  index = headers[minimum_index..-1].index(header)
@@ -287,24 +577,36 @@ class CSV
287
577
  index.nil? ? nil : index + minimum_index
288
578
  end
289
579
 
290
- #
291
- # Returns +true+ if +data+ matches a field in this row, and +false+
292
- # otherwise.
293
- #
580
+ # :call-seq:
581
+ # row.field?(value) -> true or false
582
+ #
583
+ # Returns +true+ if +value+ is a field in this row, +false+ otherwise:
584
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
585
+ # table = CSV.parse(source, headers: true)
586
+ # row = table[0]
587
+ # row.field?('Bar') # => true
588
+ # row.field?('BAR') # => false
294
589
  def field?(data)
295
590
  fields.include? data
296
591
  end
297
592
 
298
593
  include Enumerable
299
594
 
300
- #
301
- # Yields each pair of the row as header and field tuples (much like
302
- # iterating over a Hash). This method returns the row for chaining.
303
- #
304
- # If no block is given, an Enumerator is returned.
305
- #
306
- # Support for Enumerable.
307
- #
595
+ # :call-seq:
596
+ # row.each {|header, value| ... } -> self
597
+ #
598
+ # Calls the block with each header-value pair; returns +self+:
599
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
600
+ # table = CSV.parse(source, headers: true)
601
+ # row = table[0]
602
+ # row.each {|header, value| p [header, value] }
603
+ # Output:
604
+ # ["Name", "Foo"]
605
+ # ["Name", "Bar"]
606
+ # ["Name", "Baz"]
607
+ #
608
+ # If no block is given, returns a new Enumerator:
609
+ # row.each # => #<Enumerator: #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz">:each>
308
610
  def each(&block)
309
611
  return enum_for(__method__) { size } unless block_given?
310
612
 
@@ -315,19 +617,39 @@ class CSV
315
617
 
316
618
  alias_method :each_pair, :each
317
619
 
318
- #
319
- # Returns +true+ if this row contains the same headers and fields in the
320
- # same order as +other+.
321
- #
620
+ # :call-seq:
621
+ # row == other -> true or false
622
+ #
623
+ # Returns +true+ if +other+ is a /CSV::Row that has the same
624
+ # fields (headers and values) in the same order as +self+;
625
+ # otherwise returns +false+:
626
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
627
+ # table = CSV.parse(source, headers: true)
628
+ # row = table[0]
629
+ # other_row = table[0]
630
+ # row == other_row # => true
631
+ # other_row = table[1]
632
+ # row == other_row # => false
322
633
  def ==(other)
323
634
  return @row == other.row if other.is_a? CSV::Row
324
635
  @row == other
325
636
  end
326
637
 
327
- #
328
- # Collapses the row into a simple Hash. Be warned that this discards field
329
- # order and clobbers duplicate fields.
330
- #
638
+ # :call-seq:
639
+ # row.to_h -> hash
640
+ #
641
+ # Returns the new \Hash formed by adding each header-value pair in +self+
642
+ # as a key-value pair in the \Hash.
643
+ # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
644
+ # table = CSV.parse(source, headers: true)
645
+ # row = table[0]
646
+ # row.to_h # => {"Name"=>"foo", "Value"=>"0"}
647
+ #
648
+ # Header order is preserved, but repeated headers are ignored:
649
+ # source = "Name,Name,Name\nFoo,Bar,Baz\n"
650
+ # table = CSV.parse(source, headers: true)
651
+ # row = table[0]
652
+ # row.to_h # => {"Name"=>"Foo"}
331
653
  def to_h
332
654
  hash = {}
333
655
  each do |key, _value|
@@ -337,22 +659,59 @@ class CSV
337
659
  end
338
660
  alias_method :to_hash, :to_h
339
661
 
662
+ # :call-seq:
663
+ # row.deconstruct_keys(keys) -> hash
664
+ #
665
+ # Returns the new \Hash suitable for pattern matching containing only the
666
+ # keys specified as an argument.
667
+ def deconstruct_keys(keys)
668
+ if keys.nil?
669
+ to_h
670
+ else
671
+ keys.to_h { |key| [key, self[key]] }
672
+ end
673
+ end
674
+
340
675
  alias_method :to_ary, :to_a
341
676
 
677
+ # :call-seq:
678
+ # row.deconstruct -> array
342
679
  #
343
- # Returns the row as a CSV String. Headers are not used. Equivalent to:
344
- #
345
- # csv_row.fields.to_csv( options )
680
+ # Returns the new \Array suitable for pattern matching containing the values
681
+ # of the row.
682
+ def deconstruct
683
+ fields
684
+ end
685
+
686
+ # :call-seq:
687
+ # row.to_csv -> csv_string
346
688
  #
689
+ # Returns the row as a \CSV String. Headers are not included:
690
+ # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
691
+ # table = CSV.parse(source, headers: true)
692
+ # row = table[0]
693
+ # row.to_csv # => "foo,0\n"
347
694
  def to_csv(**options)
348
695
  fields.to_csv(**options)
349
696
  end
350
697
  alias_method :to_s, :to_csv
351
698
 
699
+ # :call-seq:
700
+ # row.dig(index_or_header, *identifiers) -> object
701
+ #
702
+ # Finds and returns the object in nested object that is specified
703
+ # by +index_or_header+ and +specifiers+.
352
704
  #
353
- # Extracts the nested value specified by the sequence of +index+ or +header+ objects by calling dig at each step,
354
- # returning nil if any intermediate step is nil.
705
+ # The nested objects may be instances of various classes.
706
+ # See {Dig Methods}[https://docs.ruby-lang.org/en/master/doc/dig_methods_rdoc.html].
355
707
  #
708
+ # Examples:
709
+ # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
710
+ # table = CSV.parse(source, headers: true)
711
+ # row = table[0]
712
+ # row.dig(1) # => "0"
713
+ # row.dig('Value') # => "0"
714
+ # row.dig(5) # => nil
356
715
  def dig(index_or_header, *indexes)
357
716
  value = field(index_or_header)
358
717
  if value.nil?
@@ -367,9 +726,17 @@ class CSV
367
726
  end
368
727
  end
369
728
 
370
- #
371
- # A summary of fields, by header, in an ASCII compatible String.
372
- #
729
+ # :call-seq:
730
+ # row.inspect -> string
731
+ #
732
+ # Returns an ASCII-compatible \String showing:
733
+ # - Class \CSV::Row.
734
+ # - Header-value pairs.
735
+ # Example:
736
+ # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
737
+ # table = CSV.parse(source, headers: true)
738
+ # row = table[0]
739
+ # row.inspect # => "#<CSV::Row \"Name\":\"foo\" \"Value\":\"0\">"
373
740
  def inspect
374
741
  str = ["#<", self.class.to_s]
375
742
  each do |header, field|