csv 3.1.7 → 3.1.8
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.
- checksums.yaml +4 -4
- data/NEWS.md +11 -0
- data/README.md +5 -0
- data/doc/csv/options/common/col_sep.rdoc +1 -7
- data/doc/csv/options/common/row_sep.rdoc +0 -9
- data/doc/csv/options/generating/write_converters.rdoc +0 -8
- data/doc/csv/recipes/filtering.rdoc +158 -0
- data/doc/csv/recipes/generating.rdoc +298 -0
- data/doc/csv/recipes/parsing.rdoc +545 -0
- data/doc/csv/recipes/recipes.rdoc +6 -0
- data/lib/csv.rb +67 -26
- data/lib/csv/row.rb +477 -132
- data/lib/csv/table.rb +486 -65
- data/lib/csv/version.rb +1 -1
- metadata +11 -3
data/lib/csv.rb
CHANGED
@@ -104,6 +104,17 @@ require_relative "csv/writer"
|
|
104
104
|
using CSV::MatchP if CSV.const_defined?(:MatchP)
|
105
105
|
|
106
106
|
# == \CSV
|
107
|
+
#
|
108
|
+
# === In a Hurry?
|
109
|
+
#
|
110
|
+
# If you are familiar with \CSV data and have a particular task in mind,
|
111
|
+
# you may want to go directly to the:
|
112
|
+
# - {Recipes for CSV}[doc/csv/recipes/recipes_rdoc.html].
|
113
|
+
#
|
114
|
+
# Otherwise, read on here, about the API: classes, methods, and constants.
|
115
|
+
#
|
116
|
+
# === \CSV Data
|
117
|
+
#
|
107
118
|
# \CSV (comma-separated values) data is a text representation of a table:
|
108
119
|
# - A _row_ _separator_ delimits table rows.
|
109
120
|
# A common row separator is the newline character <tt>"\n"</tt>.
|
@@ -117,6 +128,11 @@ using CSV::MatchP if CSV.const_defined?(:MatchP)
|
|
117
128
|
#
|
118
129
|
# Despite the name \CSV, a \CSV representation can use different separators.
|
119
130
|
#
|
131
|
+
# For more about tables, see the Wikipedia article
|
132
|
+
# "{Table (information)}[https://en.wikipedia.org/wiki/Table_(information)]",
|
133
|
+
# especially its section
|
134
|
+
# "{Simple table}[https://en.wikipedia.org/wiki/Table_(information)#Simple_table]"
|
135
|
+
#
|
120
136
|
# == \Class \CSV
|
121
137
|
#
|
122
138
|
# Class \CSV provides methods for:
|
@@ -674,12 +690,15 @@ using CSV::MatchP if CSV.const_defined?(:MatchP)
|
|
674
690
|
#
|
675
691
|
# You can define a custom field converter:
|
676
692
|
# strip_converter = proc {|field| field.strip }
|
677
|
-
# Add it to the \Converters \Hash:
|
678
|
-
# CSV::Converters[:strip] = strip_converter
|
679
|
-
# Use it by name:
|
680
693
|
# string = " foo , 0 \n bar , 1 \n baz , 2 \n"
|
681
694
|
# array = CSV.parse(string, converters: strip_converter)
|
682
695
|
# array # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
696
|
+
# You can register the converter in \Converters \Hash,
|
697
|
+
# which allows you to refer to it by name:
|
698
|
+
# CSV::Converters[:strip] = strip_converter
|
699
|
+
# string = " foo , 0 \n bar , 1 \n baz , 2 \n"
|
700
|
+
# array = CSV.parse(string, converters: :strip)
|
701
|
+
# array # => [["foo", "0"], ["bar", "1"], ["baz", "2"]]
|
683
702
|
#
|
684
703
|
# ==== Header \Converters
|
685
704
|
#
|
@@ -737,13 +756,16 @@ using CSV::MatchP if CSV.const_defined?(:MatchP)
|
|
737
756
|
#
|
738
757
|
# You can define a custom header converter:
|
739
758
|
# upcase_converter = proc {|header| header.upcase }
|
740
|
-
# Add it to the \HeaderConverters \Hash:
|
741
|
-
# CSV::HeaderConverters[:upcase] = upcase_converter
|
742
|
-
# Use it by name:
|
743
759
|
# string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
744
|
-
# table = CSV.parse(string, headers: true,
|
760
|
+
# table = CSV.parse(string, headers: true, header_converters: upcase_converter)
|
761
|
+
# table # => #<CSV::Table mode:col_or_row row_count:4>
|
762
|
+
# table.headers # => ["NAME", "VALUE"]
|
763
|
+
# You can register the converter in \HeaderConverters \Hash,
|
764
|
+
# which allows you to refer to it by name:
|
765
|
+
# CSV::HeaderConverters[:upcase] = upcase_converter
|
766
|
+
# table = CSV.parse(string, headers: true, header_converters: :upcase)
|
745
767
|
# table # => #<CSV::Table mode:col_or_row row_count:4>
|
746
|
-
# table.headers # => ["
|
768
|
+
# table.headers # => ["NAME", "VALUE"]
|
747
769
|
#
|
748
770
|
# ===== Write \Converters
|
749
771
|
#
|
@@ -752,23 +774,23 @@ using CSV::MatchP if CSV.const_defined?(:MatchP)
|
|
752
774
|
# its return value becomes the new value for the field.
|
753
775
|
# A converter might, for example, strip whitespace from a field.
|
754
776
|
#
|
755
|
-
#
|
756
|
-
#
|
757
|
-
#
|
758
|
-
#
|
759
|
-
#
|
760
|
-
#
|
761
|
-
#
|
762
|
-
#
|
763
|
-
#
|
764
|
-
#
|
765
|
-
#
|
766
|
-
#
|
767
|
-
#
|
768
|
-
#
|
769
|
-
#
|
770
|
-
#
|
771
|
-
#
|
777
|
+
# Using no write converter (all fields unmodified):
|
778
|
+
# output_string = CSV.generate do |csv|
|
779
|
+
# csv << [' foo ', 0]
|
780
|
+
# csv << [' bar ', 1]
|
781
|
+
# csv << [' baz ', 2]
|
782
|
+
# end
|
783
|
+
# output_string # => " foo ,0\n bar ,1\n baz ,2\n"
|
784
|
+
# Using option +write_converters+ with two custom write converters:
|
785
|
+
# strip_converter = proc {|field| field.respond_to?(:strip) ? field.strip : field }
|
786
|
+
# upcase_converter = proc {|field| field.respond_to?(:upcase) ? field.upcase : field }
|
787
|
+
# write_converters = [strip_converter, upcase_converter]
|
788
|
+
# output_string = CSV.generate(write_converters: write_converters) do |csv|
|
789
|
+
# csv << [' foo ', 0]
|
790
|
+
# csv << [' bar ', 1]
|
791
|
+
# csv << [' baz ', 2]
|
792
|
+
# end
|
793
|
+
# output_string # => "FOO,0\nBAR,1\nBAZ,2\n"
|
772
794
|
#
|
773
795
|
# === Character Encodings (M17n or Multilingualization)
|
774
796
|
#
|
@@ -1052,10 +1074,29 @@ class CSV
|
|
1052
1074
|
out_options[key] = value
|
1053
1075
|
end
|
1054
1076
|
end
|
1077
|
+
|
1055
1078
|
# build input and output wrappers
|
1056
|
-
input = new(input || ARGF,
|
1079
|
+
input = new(input || ARGF, **in_options)
|
1057
1080
|
output = new(output || $stdout, **out_options)
|
1058
1081
|
|
1082
|
+
# process headers
|
1083
|
+
need_manual_header_output =
|
1084
|
+
(in_options[:headers] and
|
1085
|
+
out_options[:headers] == true and
|
1086
|
+
out_options[:write_headers])
|
1087
|
+
if need_manual_header_output
|
1088
|
+
first_row = input.shift
|
1089
|
+
if first_row
|
1090
|
+
if first_row.is_a?(Row)
|
1091
|
+
headers = first_row.headers
|
1092
|
+
yield headers
|
1093
|
+
output << headers
|
1094
|
+
end
|
1095
|
+
yield first_row
|
1096
|
+
output << first_row
|
1097
|
+
end
|
1098
|
+
end
|
1099
|
+
|
1059
1100
|
# read, yield, write
|
1060
1101
|
input.each do |row|
|
1061
1102
|
yield row
|
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
|
-
#
|
8
|
-
#
|
9
|
-
#
|
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
|
-
#
|
12
|
-
#
|
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
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
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,39 +123,83 @@ 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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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(
|
74
|
-
# field(
|
75
|
-
# field(
|
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
|
+
# ---
|
76
172
|
#
|
77
|
-
#
|
78
|
-
#
|
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"
|
79
179
|
#
|
80
|
-
#
|
81
|
-
#
|
82
|
-
#
|
180
|
+
# Counts backward from the last column if +index+ is negative:
|
181
|
+
# row.field(-1) # => "0"
|
182
|
+
# row.field(-2) # => "foo"
|
83
183
|
#
|
184
|
+
# Returns +nil+ if +index+ is out of range:
|
185
|
+
# row.field(2) # => nil
|
186
|
+
# row.field(-3) # => nil
|
187
|
+
#
|
188
|
+
# ---
|
189
|
+
#
|
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"
|
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
|
@@ -97,16 +216,45 @@ class CSV
|
|
97
216
|
|
98
217
|
#
|
99
218
|
# :call-seq:
|
100
|
-
# fetch(
|
101
|
-
# fetch(
|
102
|
-
# fetch(
|
103
|
-
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
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
|
-
#
|
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
|
-
# []=
|
138
|
-
# [
|
139
|
-
# []=
|
140
|
-
#
|
141
|
-
#
|
142
|
-
#
|
143
|
-
#
|
144
|
-
#
|
145
|
-
#
|
146
|
-
#
|
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
|
-
# <<
|
171
|
-
# <<
|
172
|
-
# <<
|
173
|
-
#
|
174
|
-
#
|
175
|
-
#
|
176
|
-
#
|
177
|
-
# a
|
178
|
-
#
|
179
|
-
#
|
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
|
-
#
|
195
|
-
#
|
196
|
-
#
|
197
|
-
#
|
198
|
-
#
|
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,
|
210
|
-
# delete(
|
211
|
-
#
|
212
|
-
# Removes a
|
213
|
-
#
|
214
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
245
|
-
#
|
246
|
-
#
|
247
|
-
#
|
248
|
-
#
|
249
|
-
#
|
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(
|
277
|
-
# index(
|
278
|
-
#
|
279
|
-
#
|
280
|
-
#
|
281
|
-
#
|
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
|
-
#
|
292
|
-
#
|
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
|
-
#
|
302
|
-
#
|
303
|
-
#
|
304
|
-
#
|
305
|
-
#
|
306
|
-
#
|
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
|
-
#
|
320
|
-
#
|
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
|
-
#
|
329
|
-
#
|
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|
|
@@ -339,20 +661,35 @@ class CSV
|
|
339
661
|
|
340
662
|
alias_method :to_ary, :to_a
|
341
663
|
|
664
|
+
# :call-seq:
|
665
|
+
# row.to_csv -> csv_string
|
342
666
|
#
|
343
|
-
# Returns the row as a CSV String. Headers are not
|
344
|
-
#
|
345
|
-
#
|
346
|
-
#
|
667
|
+
# Returns the row as a \CSV String. Headers are not included:
|
668
|
+
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
669
|
+
# table = CSV.parse(source, headers: true)
|
670
|
+
# row = table[0]
|
671
|
+
# row.to_csv # => "foo,0\n"
|
347
672
|
def to_csv(**options)
|
348
673
|
fields.to_csv(**options)
|
349
674
|
end
|
350
675
|
alias_method :to_s, :to_csv
|
351
676
|
|
677
|
+
# :call-seq:
|
678
|
+
# row.dig(index_or_header, *identifiers) -> object
|
679
|
+
#
|
680
|
+
# Finds and returns the object in nested object that is specified
|
681
|
+
# by +index_or_header+ and +specifiers+.
|
352
682
|
#
|
353
|
-
#
|
354
|
-
#
|
683
|
+
# The nested objects may be instances of various classes.
|
684
|
+
# See {Dig Methods}[https://docs.ruby-lang.org/en/master/doc/dig_methods_rdoc.html].
|
355
685
|
#
|
686
|
+
# Examples:
|
687
|
+
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
688
|
+
# table = CSV.parse(source, headers: true)
|
689
|
+
# row = table[0]
|
690
|
+
# row.dig(1) # => "0"
|
691
|
+
# row.dig('Value') # => "0"
|
692
|
+
# row.dig(5) # => nil
|
356
693
|
def dig(index_or_header, *indexes)
|
357
694
|
value = field(index_or_header)
|
358
695
|
if value.nil?
|
@@ -367,9 +704,17 @@ class CSV
|
|
367
704
|
end
|
368
705
|
end
|
369
706
|
|
370
|
-
#
|
371
|
-
#
|
372
|
-
#
|
707
|
+
# :call-seq:
|
708
|
+
# row.inspect -> string
|
709
|
+
#
|
710
|
+
# Returns an ASCII-compatible \String showing:
|
711
|
+
# - Class \CSV::Row.
|
712
|
+
# - Header-value pairs.
|
713
|
+
# Example:
|
714
|
+
# source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
715
|
+
# table = CSV.parse(source, headers: true)
|
716
|
+
# row = table[0]
|
717
|
+
# row.inspect # => "#<CSV::Row \"Name\":\"foo\" \"Value\":\"0\">"
|
373
718
|
def inspect
|
374
719
|
str = ["#<", self.class.to_s]
|
375
720
|
each do |header, field|
|