csv 3.2.8 → 3.3.6
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 +165 -0
- data/README.md +3 -3
- data/doc/csv/recipes/filtering.rdoc +85 -17
- data/doc/csv/recipes/generating.rdoc +1 -1
- data/doc/csv/recipes/parsing.rdoc +12 -3
- data/lib/csv/core_ext/array.rb +1 -1
- data/lib/csv/core_ext/string.rb +1 -1
- data/lib/csv/fields_converter.rb +8 -1
- data/lib/csv/parser.rb +48 -29
- data/lib/csv/row.rb +26 -2
- data/lib/csv/version.rb +1 -1
- data/lib/csv/writer.rb +1 -2
- data/lib/csv.rb +175 -68
- metadata +6 -64
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d2a1d74b0246a143f03c2e786ea5484cb3e5aebe370453141a9d69539eae900b
|
|
4
|
+
data.tar.gz: d8bb00a2e6842e28ca6b443e416f8f03186b098dcc81fde6ba5429eed138a755
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d26ab99eb2708d787022832ca171def6ef4647183667be03831d9a91c5320545146adac9e5471e92637e6e905c27c06499739fc5e510541c026c15388b9e316
|
|
7
|
+
data.tar.gz: f8e7b076653e2e3f04503db617e94f14c66064aa1a8ff790a342c018eb6c598eb84127b0818e0ed8f2688ec6404b163c80e6c58cbf7d3c9411583ad8cd2cd0bd
|
data/NEWS.md
CHANGED
|
@@ -1,5 +1,170 @@
|
|
|
1
1
|
# News
|
|
2
2
|
|
|
3
|
+
## 3.3.6 - 2026-07-27
|
|
4
|
+
|
|
5
|
+
### Improvements
|
|
6
|
+
|
|
7
|
+
* `CSV::Row#to_h`: Added support for block.
|
|
8
|
+
* GH-356
|
|
9
|
+
* Patch by Vlad
|
|
10
|
+
|
|
11
|
+
* Ensured using `path_or_io` for parameter name that accepts path or
|
|
12
|
+
IO.
|
|
13
|
+
* GH-358
|
|
14
|
+
* Patch by Yuto Urushima
|
|
15
|
+
|
|
16
|
+
* Changed to not using enumerator in `CSV`.
|
|
17
|
+
* GH-361
|
|
18
|
+
* GH-363
|
|
19
|
+
* Reported by Cas Donoghue
|
|
20
|
+
|
|
21
|
+
### Fixes
|
|
22
|
+
|
|
23
|
+
* Fixed a bug that `\r` in unquoted fields are rejected when row
|
|
24
|
+
separator doesn't contain `\r`
|
|
25
|
+
* GH-60
|
|
26
|
+
* GH-346
|
|
27
|
+
* Patch by Jas
|
|
28
|
+
|
|
29
|
+
* Fixed a typo in documentation.
|
|
30
|
+
* GH-350
|
|
31
|
+
* Patch by tmr111116
|
|
32
|
+
|
|
33
|
+
### Thanks
|
|
34
|
+
|
|
35
|
+
* Jas
|
|
36
|
+
* tmr111116
|
|
37
|
+
* Vlad
|
|
38
|
+
* Yuto Urushima
|
|
39
|
+
* Cas Donoghue
|
|
40
|
+
|
|
41
|
+
## 3.3.5 - 2025-06-01
|
|
42
|
+
|
|
43
|
+
### Improvements
|
|
44
|
+
|
|
45
|
+
* docs: Fixed `StringScanner` document URL.
|
|
46
|
+
* GH-343
|
|
47
|
+
* Patch by Petrik de Heus
|
|
48
|
+
|
|
49
|
+
### Thanks
|
|
50
|
+
|
|
51
|
+
* Petrik de Heus
|
|
52
|
+
|
|
53
|
+
## 3.3.4 - 2025-04-13
|
|
54
|
+
|
|
55
|
+
### Improvements
|
|
56
|
+
|
|
57
|
+
* `csv-filter`: Removed an experimental command line tool.
|
|
58
|
+
* GH-341
|
|
59
|
+
|
|
60
|
+
## 3.3.3 - 2025-03-20
|
|
61
|
+
|
|
62
|
+
### Improvements
|
|
63
|
+
|
|
64
|
+
* `csv-filter`: Added an experimental command line tool to filter a CSV.
|
|
65
|
+
* Patch by Burdette Lamar
|
|
66
|
+
|
|
67
|
+
### Fixes
|
|
68
|
+
|
|
69
|
+
* Fixed wrong EOF detection for `ARGF`
|
|
70
|
+
* GH-328
|
|
71
|
+
* Reported by Takeshi Nishimatsu
|
|
72
|
+
|
|
73
|
+
* Fixed a regression bug that `CSV.open` rejects integer mode.
|
|
74
|
+
* GH-336
|
|
75
|
+
* Reported by Dave Burgess
|
|
76
|
+
|
|
77
|
+
### Thanks
|
|
78
|
+
|
|
79
|
+
* Takeshi Nishimatsu
|
|
80
|
+
|
|
81
|
+
* Burdette Lamar
|
|
82
|
+
|
|
83
|
+
* Dave Burgess
|
|
84
|
+
|
|
85
|
+
## 3.3.2 - 2024-12-21
|
|
86
|
+
|
|
87
|
+
### Fixes
|
|
88
|
+
|
|
89
|
+
* Fixed a parse bug with a quoted line with `col_sep` and an empty
|
|
90
|
+
line. This was introduced in 3.3.1.
|
|
91
|
+
* GH-324
|
|
92
|
+
* Reported by stoodfarback
|
|
93
|
+
|
|
94
|
+
### Thanks
|
|
95
|
+
|
|
96
|
+
* stoodfarback
|
|
97
|
+
|
|
98
|
+
## 3.3.1 - 2024-12-15
|
|
99
|
+
|
|
100
|
+
### Improvements
|
|
101
|
+
|
|
102
|
+
* `CSV.open`: Changed to detect BOM by default. Note that this isn't
|
|
103
|
+
enabled on Windows because Ruby may have a bug. See also:
|
|
104
|
+
https://bugs.ruby-lang.org/issues/20526
|
|
105
|
+
* GH-301
|
|
106
|
+
* Reported by Junichi Ito
|
|
107
|
+
|
|
108
|
+
* Improved performance.
|
|
109
|
+
* GH-311
|
|
110
|
+
* GH-312
|
|
111
|
+
* Patch by Vladimir Kochnev
|
|
112
|
+
|
|
113
|
+
* `CSV.open`: Added support for `StringIO` as an input.
|
|
114
|
+
* GH-300
|
|
115
|
+
* GH-302
|
|
116
|
+
* Patch by Marcelo
|
|
117
|
+
|
|
118
|
+
* Added a built-in time converter. You can use it by `converters:
|
|
119
|
+
:time`.
|
|
120
|
+
* GH-313
|
|
121
|
+
* Patch by Bart de Water
|
|
122
|
+
|
|
123
|
+
* Added `CSV::TSV` for tab-separated values.
|
|
124
|
+
* GH-272
|
|
125
|
+
* GH-319
|
|
126
|
+
* Reported by kojix2
|
|
127
|
+
* Patch by Jas
|
|
128
|
+
|
|
129
|
+
### Thanks
|
|
130
|
+
|
|
131
|
+
* Junichi Ito
|
|
132
|
+
|
|
133
|
+
* Vladimir Kochnev
|
|
134
|
+
|
|
135
|
+
* Marcelo
|
|
136
|
+
|
|
137
|
+
* Bart de Water
|
|
138
|
+
|
|
139
|
+
* kojix2
|
|
140
|
+
|
|
141
|
+
* Jas
|
|
142
|
+
|
|
143
|
+
## 3.3.0 - 2024-03-22
|
|
144
|
+
|
|
145
|
+
### Fixes
|
|
146
|
+
|
|
147
|
+
* Fixed a regression parse bug in 3.2.9 that parsing with
|
|
148
|
+
`:skip_lines` may cause wrong result.
|
|
149
|
+
|
|
150
|
+
## 3.2.9 - 2024-03-22
|
|
151
|
+
|
|
152
|
+
### Fixes
|
|
153
|
+
|
|
154
|
+
* Fixed a parse bug that wrong result may be happen when:
|
|
155
|
+
|
|
156
|
+
* `:skip_lines` is used
|
|
157
|
+
* `:row_separator` is `"\r\n"`
|
|
158
|
+
* There is a line that includes `\n` as a column value
|
|
159
|
+
|
|
160
|
+
Reported by Ryo Tsukamoto.
|
|
161
|
+
|
|
162
|
+
GH-296
|
|
163
|
+
|
|
164
|
+
### Thanks
|
|
165
|
+
|
|
166
|
+
* Ryo Tsukamoto
|
|
167
|
+
|
|
3
168
|
## 3.2.8 - 2023-11-08
|
|
4
169
|
|
|
5
170
|
### Improvements
|
data/README.md
CHANGED
|
@@ -30,8 +30,8 @@ end
|
|
|
30
30
|
|
|
31
31
|
## Documentation
|
|
32
32
|
|
|
33
|
-
- [API](https://ruby
|
|
34
|
-
- [Recipes](https://ruby
|
|
33
|
+
- [API](https://ruby.github.io/csv/): all classes, methods, and constants.
|
|
34
|
+
- [Recipes](https://ruby.github.io/csv/doc/csv/recipes/recipes_rdoc.html): specific code for specific tasks.
|
|
35
35
|
|
|
36
36
|
## Development
|
|
37
37
|
|
|
@@ -52,4 +52,4 @@ Please do not submit issues and PRs that aim to introduce RuboCop in this reposi
|
|
|
52
52
|
|
|
53
53
|
The gem is available as open source under the terms of the [2-Clause BSD License](https://opensource.org/licenses/BSD-2-Clause).
|
|
54
54
|
|
|
55
|
-
See LICENSE.txt for details.
|
|
55
|
+
See [LICENSE.txt](LICENSE.txt) for details.
|
|
@@ -11,16 +11,20 @@ All code snippets on this page assume that the following has been executed:
|
|
|
11
11
|
|
|
12
12
|
- {Source and Output Formats}[#label-Source+and+Output+Formats]
|
|
13
13
|
- {Filtering String to String}[#label-Filtering+String+to+String]
|
|
14
|
-
- {Recipe: Filter String to String
|
|
14
|
+
- {Recipe: Filter String to String parsing Headers}[#label-Recipe-3A+Filter+String+to+String+parsing+Headers]
|
|
15
|
+
- {Recipe: Filter String to String parsing and writing Headers}[#label-Recipe-3A+Filter+String+to+String+parsing+and+writing+Headers]
|
|
15
16
|
- {Recipe: Filter String to String Without Headers}[#label-Recipe-3A+Filter+String+to+String+Without+Headers]
|
|
16
17
|
- {Filtering String to IO Stream}[#label-Filtering+String+to+IO+Stream]
|
|
17
|
-
- {Recipe: Filter String to IO Stream
|
|
18
|
+
- {Recipe: Filter String to IO Stream parsing Headers}[#label-Recipe-3A+Filter+String+to+IO+Stream+parsing+Headers]
|
|
19
|
+
- {Recipe: Filter String to IO Stream parsing and writing Headers}[#label-Recipe-3A+Filter+String+to+IO+Stream+parsing+and+writing+Headers]
|
|
18
20
|
- {Recipe: Filter String to IO Stream Without Headers}[#label-Recipe-3A+Filter+String+to+IO+Stream+Without+Headers]
|
|
19
21
|
- {Filtering IO Stream to String}[#label-Filtering+IO+Stream+to+String]
|
|
20
|
-
- {Recipe: Filter IO Stream to String
|
|
22
|
+
- {Recipe: Filter IO Stream to String parsing Headers}[#label-Recipe-3A+Filter+IO+Stream+to+String+parsing+Headers]
|
|
23
|
+
- {Recipe: Filter IO Stream to String parsing and writing Headers}[#label-Recipe-3A+Filter+IO+Stream+to+String+parsing+and+writing+Headers]
|
|
21
24
|
- {Recipe: Filter IO Stream to String Without Headers}[#label-Recipe-3A+Filter+IO+Stream+to+String+Without+Headers]
|
|
22
25
|
- {Filtering IO Stream to IO Stream}[#label-Filtering+IO+Stream+to+IO+Stream]
|
|
23
|
-
- {Recipe: Filter IO Stream to IO Stream
|
|
26
|
+
- {Recipe: Filter IO Stream to IO Stream parsing Headers}[#label-Recipe-3A+Filter+IO+Stream+to+IO+Stream+parsing+Headers]
|
|
27
|
+
- {Recipe: Filter IO Stream to IO Stream parsing and writing Headers}[#label-Recipe-3A+Filter+IO+Stream+to+IO+Stream+parsing+and+writing+Headers]
|
|
24
28
|
- {Recipe: Filter IO Stream to IO Stream Without Headers}[#label-Recipe-3A+Filter+IO+Stream+to+IO+Stream+Without+Headers]
|
|
25
29
|
|
|
26
30
|
=== Source and Output Formats
|
|
@@ -33,14 +37,27 @@ The input and output \CSV data may be any mixture of \Strings and \IO streams.
|
|
|
33
37
|
|
|
34
38
|
You can filter one \String to another, with or without headers.
|
|
35
39
|
|
|
36
|
-
===== Recipe: Filter \String to \String
|
|
40
|
+
===== Recipe: Filter \String to \String parsing Headers
|
|
37
41
|
|
|
38
42
|
Use class method CSV.filter with option +headers+ to filter a \String to another \String:
|
|
39
43
|
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
|
40
44
|
out_string = ''
|
|
41
45
|
CSV.filter(in_string, out_string, headers: true) do |row|
|
|
42
|
-
row[
|
|
43
|
-
row[
|
|
46
|
+
row['Name'] = row['Name'].upcase
|
|
47
|
+
row['Value'] *= 4
|
|
48
|
+
end
|
|
49
|
+
out_string # => "FOO,0000\nBAR,1111\nBAZ,2222\n"
|
|
50
|
+
|
|
51
|
+
===== Recipe: Filter \String to \String parsing and writing Headers
|
|
52
|
+
|
|
53
|
+
Use class method CSV.filter with option +headers+ and +out_write_headers+ to filter a \String to another \String including header row:
|
|
54
|
+
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
|
55
|
+
out_string = ''
|
|
56
|
+
CSV.filter(in_string, out_string, headers: true, out_write_headers: true) do |row|
|
|
57
|
+
unless row.is_a?(Array)
|
|
58
|
+
row['Name'] = row['Name'].upcase
|
|
59
|
+
row['Value'] *= 4
|
|
60
|
+
end
|
|
44
61
|
end
|
|
45
62
|
out_string # => "Name,Value\nFOO,0000\nBAR,1111\nBAZ,2222\n"
|
|
46
63
|
|
|
@@ -59,15 +76,30 @@ Use class method CSV.filter without option +headers+ to filter a \String to anot
|
|
|
59
76
|
|
|
60
77
|
You can filter a \String to an \IO stream, with or without headers.
|
|
61
78
|
|
|
62
|
-
===== Recipe: Filter \String to \IO Stream
|
|
79
|
+
===== Recipe: Filter \String to \IO Stream parsing Headers
|
|
63
80
|
|
|
64
81
|
Use class method CSV.filter with option +headers+ to filter a \String to an \IO stream:
|
|
65
82
|
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
|
66
83
|
path = 't.csv'
|
|
67
84
|
File.open(path, 'w') do |out_io|
|
|
68
85
|
CSV.filter(in_string, out_io, headers: true) do |row|
|
|
69
|
-
row[
|
|
70
|
-
row[
|
|
86
|
+
row['Name'] = row['Name'].upcase
|
|
87
|
+
row['Value'] *= 4
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
p File.read(path) # => "FOO,0000\nBAR,1111\nBAZ,2222\n"
|
|
91
|
+
|
|
92
|
+
===== Recipe: Filter \String to \IO Stream parsing and writing Headers
|
|
93
|
+
|
|
94
|
+
Use class method CSV.filter with option +headers+ and +out_write_headers+ to filter a \String to an \IO stream including header row:
|
|
95
|
+
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
|
96
|
+
path = 't.csv'
|
|
97
|
+
File.open(path, 'w') do |out_io|
|
|
98
|
+
CSV.filter(in_string, out_io, headers: true, out_write_headers: true ) do |row|
|
|
99
|
+
unless row.is_a?(Array)
|
|
100
|
+
row['Name'] = row['Name'].upcase
|
|
101
|
+
row['Value'] *= 4
|
|
102
|
+
end
|
|
71
103
|
end
|
|
72
104
|
end
|
|
73
105
|
p File.read(path) # => "Name,Value\nFOO,0000\nBAR,1111\nBAZ,2222\n"
|
|
@@ -89,17 +121,34 @@ Use class method CSV.filter without option +headers+ to filter a \String to an \
|
|
|
89
121
|
|
|
90
122
|
You can filter an \IO stream to a \String, with or without headers.
|
|
91
123
|
|
|
92
|
-
===== Recipe: Filter \IO Stream to \String
|
|
124
|
+
===== Recipe: Filter \IO Stream to \String parsing Headers
|
|
93
125
|
|
|
94
126
|
Use class method CSV.filter with option +headers+ to filter an \IO stream to a \String:
|
|
95
127
|
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
|
96
128
|
path = 't.csv'
|
|
97
129
|
File.write(path, in_string)
|
|
98
130
|
out_string = ''
|
|
99
|
-
File.open(path
|
|
131
|
+
File.open(path) do |in_io|
|
|
100
132
|
CSV.filter(in_io, out_string, headers: true) do |row|
|
|
101
|
-
row[
|
|
102
|
-
row[
|
|
133
|
+
row['Name'] = row['Name'].upcase
|
|
134
|
+
row['Value'] *= 4
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
out_string # => "FOO,0000\nBAR,1111\nBAZ,2222\n"
|
|
138
|
+
|
|
139
|
+
===== Recipe: Filter \IO Stream to \String parsing and writing Headers
|
|
140
|
+
|
|
141
|
+
Use class method CSV.filter with option +headers+ and +out_write_headers+ to filter an \IO stream to a \String including header row:
|
|
142
|
+
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
|
143
|
+
path = 't.csv'
|
|
144
|
+
File.write(path, in_string)
|
|
145
|
+
out_string = ''
|
|
146
|
+
File.open(path) do |in_io|
|
|
147
|
+
CSV.filter(in_io, out_string, headers: true, out_write_headers: true) do |row|
|
|
148
|
+
unless row.is_a?(Array)
|
|
149
|
+
row['Name'] = row['Name'].upcase
|
|
150
|
+
row['Value'] *= 4
|
|
151
|
+
end
|
|
103
152
|
end
|
|
104
153
|
end
|
|
105
154
|
out_string # => "Name,Value\nFOO,0000\nBAR,1111\nBAZ,2222\n"
|
|
@@ -123,7 +172,7 @@ Use class method CSV.filter without option +headers+ to filter an \IO stream to
|
|
|
123
172
|
|
|
124
173
|
You can filter an \IO stream to another \IO stream, with or without headers.
|
|
125
174
|
|
|
126
|
-
===== Recipe: Filter \IO Stream to \IO Stream
|
|
175
|
+
===== Recipe: Filter \IO Stream to \IO Stream parsing Headers
|
|
127
176
|
|
|
128
177
|
Use class method CSV.filter with option +headers+ to filter an \IO stream to another \IO stream:
|
|
129
178
|
in_path = 't.csv'
|
|
@@ -133,8 +182,27 @@ Use class method CSV.filter with option +headers+ to filter an \IO stream to ano
|
|
|
133
182
|
File.open(in_path) do |in_io|
|
|
134
183
|
File.open(out_path, 'w') do |out_io|
|
|
135
184
|
CSV.filter(in_io, out_io, headers: true) do |row|
|
|
136
|
-
row[
|
|
137
|
-
row[
|
|
185
|
+
row['Name'] = row['Name'].upcase
|
|
186
|
+
row['Value'] *= 4
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
p File.read(out_path) # => "FOO,0000\nBAR,1111\nBAZ,2222\n"
|
|
191
|
+
|
|
192
|
+
===== Recipe: Filter \IO Stream to \IO Stream parsing and writing Headers
|
|
193
|
+
|
|
194
|
+
Use class method CSV.filter with option +headers+ and +out_write_headers+ to filter an \IO stream to another \IO stream including header row:
|
|
195
|
+
in_path = 't.csv'
|
|
196
|
+
in_string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
|
197
|
+
File.write(in_path, in_string)
|
|
198
|
+
out_path = 'u.csv'
|
|
199
|
+
File.open(in_path) do |in_io|
|
|
200
|
+
File.open(out_path, 'w') do |out_io|
|
|
201
|
+
CSV.filter(in_io, out_io, headers: true, out_write_headers: true) do |row|
|
|
202
|
+
unless row.is_a?(Array)
|
|
203
|
+
row['Name'] = row['Name'].upcase
|
|
204
|
+
row['Value'] *= 4
|
|
205
|
+
end
|
|
138
206
|
end
|
|
139
207
|
end
|
|
140
208
|
end
|
|
@@ -165,7 +165,7 @@ This example defines and uses two custom write converters to strip and upcase ge
|
|
|
165
165
|
=== RFC 4180 Compliance
|
|
166
166
|
|
|
167
167
|
By default, \CSV generates data that is compliant with
|
|
168
|
-
{RFC 4180}[https://
|
|
168
|
+
{RFC 4180}[https://www.rfc-editor.org/rfc/rfc4180]
|
|
169
169
|
with respect to:
|
|
170
170
|
- Column separator.
|
|
171
171
|
- Quote character.
|
|
@@ -45,6 +45,7 @@ All code snippets on this page assume that the following has been executed:
|
|
|
45
45
|
- {Recipe: Convert Fields to Numerics}[#label-Recipe-3A+Convert+Fields+to+Numerics]
|
|
46
46
|
- {Recipe: Convert Fields to Dates}[#label-Recipe-3A+Convert+Fields+to+Dates]
|
|
47
47
|
- {Recipe: Convert Fields to DateTimes}[#label-Recipe-3A+Convert+Fields+to+DateTimes]
|
|
48
|
+
- {Recipe: Convert Fields to Times}[#label-Recipe-3A+Convert+Fields+to+Times]
|
|
48
49
|
- {Recipe: Convert Assorted Fields to Objects}[#label-Recipe-3A+Convert+Assorted+Fields+to+Objects]
|
|
49
50
|
- {Recipe: Convert Fields to Other Objects}[#label-Recipe-3A+Convert+Fields+to+Other+Objects]
|
|
50
51
|
- {Recipe: Filter Field Strings}[#label-Recipe-3A+Filter+Field+Strings]
|
|
@@ -110,7 +111,7 @@ You can parse \CSV data from a \File, with or without headers.
|
|
|
110
111
|
|
|
111
112
|
===== Recipe: Parse from \File with Headers
|
|
112
113
|
|
|
113
|
-
Use
|
|
114
|
+
Use class method CSV.read with option +headers+ to read a file all at once:
|
|
114
115
|
string = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
|
|
115
116
|
path = 't.csv'
|
|
116
117
|
File.write(path, string)
|
|
@@ -191,7 +192,7 @@ Output:
|
|
|
191
192
|
=== RFC 4180 Compliance
|
|
192
193
|
|
|
193
194
|
By default, \CSV parses data that is compliant with
|
|
194
|
-
{RFC 4180}[https://
|
|
195
|
+
{RFC 4180}[https://www.rfc-editor.org/rfc/rfc4180]
|
|
195
196
|
with respect to:
|
|
196
197
|
- Row separator.
|
|
197
198
|
- Column separator.
|
|
@@ -339,6 +340,7 @@ There are built-in field converters for converting to objects of certain classes
|
|
|
339
340
|
- \Integer
|
|
340
341
|
- \Date
|
|
341
342
|
- \DateTime
|
|
343
|
+
- \Time
|
|
342
344
|
|
|
343
345
|
Other built-in field converters include:
|
|
344
346
|
- +:numeric+: converts to \Integer and \Float.
|
|
@@ -381,6 +383,13 @@ Convert fields to \DateTime objects using built-in converter +:date_time+:
|
|
|
381
383
|
parsed = CSV.parse(source, headers: true, converters: :date_time)
|
|
382
384
|
parsed.map {|row| row['DateTime'].class} # => [DateTime, DateTime, DateTime]
|
|
383
385
|
|
|
386
|
+
===== Recipe: Convert Fields to Times
|
|
387
|
+
|
|
388
|
+
Convert fields to \Time objects using built-in converter +:time+:
|
|
389
|
+
source = "Name,Time\nfoo,2001-02-03\nbar,2001-02-04\nbaz,2020-05-07T14:59:00-05:00\n"
|
|
390
|
+
parsed = CSV.parse(source, headers: true, converters: :time)
|
|
391
|
+
parsed.map {|row| row['Time'].class} # => [Time, Time, Time]
|
|
392
|
+
|
|
384
393
|
===== Recipe: Convert Assorted Fields to Objects
|
|
385
394
|
|
|
386
395
|
Convert assorted fields to objects using built-in converter +:all+:
|
|
@@ -542,4 +551,4 @@ Output:
|
|
|
542
551
|
#<struct CSV::FieldInfo index=0, line=2, header=nil>
|
|
543
552
|
#<struct CSV::FieldInfo index=1, line=2, header=nil>
|
|
544
553
|
#<struct CSV::FieldInfo index=0, line=3, header=nil>
|
|
545
|
-
#<struct CSV::FieldInfo index=1, line=3, header=nil>
|
|
554
|
+
#<struct CSV::FieldInfo index=1, line=3, header=nil>
|
data/lib/csv/core_ext/array.rb
CHANGED
data/lib/csv/core_ext/string.rb
CHANGED
data/lib/csv/fields_converter.rb
CHANGED
|
@@ -4,6 +4,13 @@ class CSV
|
|
|
4
4
|
# Note: Don't use this class directly. This is an internal class.
|
|
5
5
|
class FieldsConverter
|
|
6
6
|
include Enumerable
|
|
7
|
+
|
|
8
|
+
NO_QUOTED_FIELDS = [] # :nodoc:
|
|
9
|
+
def NO_QUOTED_FIELDS.[](_index)
|
|
10
|
+
false
|
|
11
|
+
end
|
|
12
|
+
NO_QUOTED_FIELDS.freeze
|
|
13
|
+
|
|
7
14
|
#
|
|
8
15
|
# A CSV::FieldsConverter is a data structure for storing the
|
|
9
16
|
# fields converter properties to be passed as a parameter
|
|
@@ -44,7 +51,7 @@ class CSV
|
|
|
44
51
|
@converters.empty?
|
|
45
52
|
end
|
|
46
53
|
|
|
47
|
-
def convert(fields, headers, lineno, quoted_fields)
|
|
54
|
+
def convert(fields, headers, lineno, quoted_fields=NO_QUOTED_FIELDS)
|
|
48
55
|
return fields unless need_convert?
|
|
49
56
|
|
|
50
57
|
fields.collect.with_index do |field, index|
|
data/lib/csv/parser.rb
CHANGED
|
@@ -18,6 +18,19 @@ class CSV
|
|
|
18
18
|
# into your Encoding.
|
|
19
19
|
#
|
|
20
20
|
|
|
21
|
+
class << self
|
|
22
|
+
ARGF_OBJECT_ID = ARGF.object_id
|
|
23
|
+
# Convenient method to check whether the give input reached EOF
|
|
24
|
+
# or not.
|
|
25
|
+
def eof?(input)
|
|
26
|
+
# We can't use input != ARGF in Ractor. Because ARGF isn't a
|
|
27
|
+
# shareable object.
|
|
28
|
+
input.object_id != ARGF_OBJECT_ID and
|
|
29
|
+
input.respond_to?(:eof) and
|
|
30
|
+
input.eof?
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
21
34
|
# Raised when encoding is invalid.
|
|
22
35
|
class InvalidEncoding < StandardError
|
|
23
36
|
end
|
|
@@ -34,7 +47,7 @@ class CSV
|
|
|
34
47
|
# Uses StringScanner (the official strscan gem). Strscan provides lexical
|
|
35
48
|
# scanning operations on a String. We inherit its object and take advantage
|
|
36
49
|
# on the methods. For more information, please visit:
|
|
37
|
-
# https://ruby-
|
|
50
|
+
# https://docs.ruby-lang.org/en/master/StringScanner.html
|
|
38
51
|
#
|
|
39
52
|
class Scanner < StringScanner
|
|
40
53
|
alias_method :scan_all, :scan
|
|
@@ -168,7 +181,7 @@ class CSV
|
|
|
168
181
|
end
|
|
169
182
|
|
|
170
183
|
def eos?
|
|
171
|
-
@scanner.eos?
|
|
184
|
+
@last_scanner and @scanner.eos?
|
|
172
185
|
end
|
|
173
186
|
|
|
174
187
|
def keep_start
|
|
@@ -220,6 +233,15 @@ class CSV
|
|
|
220
233
|
end
|
|
221
234
|
# trace(__method__, :repos, start, buffer)
|
|
222
235
|
@scanner.pos = start
|
|
236
|
+
last_scanner, last_start, last_buffer = @keeps.last
|
|
237
|
+
# Drop the last buffer when the last buffer is the same data
|
|
238
|
+
# in the last keep. If we keep it, we have duplicated data
|
|
239
|
+
# by the next keep_back.
|
|
240
|
+
if last_scanner == @scanner and
|
|
241
|
+
last_buffer and
|
|
242
|
+
last_buffer == last_scanner.string.byteslice(last_start, start)
|
|
243
|
+
@keeps.last[2] = nil
|
|
244
|
+
end
|
|
223
245
|
end
|
|
224
246
|
read_chunk if @scanner.eos?
|
|
225
247
|
end
|
|
@@ -303,7 +325,7 @@ class CSV
|
|
|
303
325
|
raise InvalidEncoding unless chunk.valid_encoding?
|
|
304
326
|
# trace(__method__, :chunk, chunk)
|
|
305
327
|
@scanner = StringScanner.new(chunk)
|
|
306
|
-
if
|
|
328
|
+
if Parser.eof?(input)
|
|
307
329
|
@inputs.shift
|
|
308
330
|
@last_scanner = @inputs.empty?
|
|
309
331
|
end
|
|
@@ -400,13 +422,7 @@ class CSV
|
|
|
400
422
|
|
|
401
423
|
begin
|
|
402
424
|
@scanner ||= build_scanner
|
|
403
|
-
|
|
404
|
-
parse_no_quote(&block)
|
|
405
|
-
elsif @need_robust_parsing
|
|
406
|
-
parse_quotable_robust(&block)
|
|
407
|
-
else
|
|
408
|
-
parse_quotable_loose(&block)
|
|
409
|
-
end
|
|
425
|
+
__send__(@parse_method, &block)
|
|
410
426
|
rescue InvalidEncoding
|
|
411
427
|
if @scanner
|
|
412
428
|
ignore_broken_line
|
|
@@ -428,6 +444,11 @@ class CSV
|
|
|
428
444
|
end
|
|
429
445
|
end
|
|
430
446
|
|
|
447
|
+
def eof?
|
|
448
|
+
return false if @scanner.nil?
|
|
449
|
+
@scanner.eos?
|
|
450
|
+
end
|
|
451
|
+
|
|
431
452
|
def use_headers?
|
|
432
453
|
@use_headers
|
|
433
454
|
end
|
|
@@ -450,7 +471,6 @@ class CSV
|
|
|
450
471
|
end
|
|
451
472
|
|
|
452
473
|
def prepare_variable
|
|
453
|
-
@need_robust_parsing = false
|
|
454
474
|
@encoding = @options[:encoding]
|
|
455
475
|
liberal_parsing = @options[:liberal_parsing]
|
|
456
476
|
if liberal_parsing
|
|
@@ -463,7 +483,6 @@ class CSV
|
|
|
463
483
|
@double_quote_outside_quote = false
|
|
464
484
|
@backslash_quote = false
|
|
465
485
|
end
|
|
466
|
-
@need_robust_parsing = true
|
|
467
486
|
else
|
|
468
487
|
@liberal_parsing = false
|
|
469
488
|
@backslash_quote = false
|
|
@@ -545,7 +564,6 @@ class CSV
|
|
|
545
564
|
@rstrip_value = Regexp.new(@escaped_strip +
|
|
546
565
|
"+\\z".encode(@encoding))
|
|
547
566
|
end
|
|
548
|
-
@need_robust_parsing = true
|
|
549
567
|
elsif @strip
|
|
550
568
|
strip_values = " \t\f\v"
|
|
551
569
|
@escaped_strip = strip_values.encode(@encoding)
|
|
@@ -553,7 +571,6 @@ class CSV
|
|
|
553
571
|
@strip_value = Regexp.new("[#{strip_values}]+".encode(@encoding))
|
|
554
572
|
@rstrip_value = Regexp.new("[#{strip_values}]+\\z".encode(@encoding))
|
|
555
573
|
end
|
|
556
|
-
@need_robust_parsing = true
|
|
557
574
|
end
|
|
558
575
|
end
|
|
559
576
|
|
|
@@ -663,7 +680,7 @@ class CSV
|
|
|
663
680
|
def prepare_unquoted
|
|
664
681
|
return if @quote_character.nil?
|
|
665
682
|
|
|
666
|
-
no_unquoted_values =
|
|
683
|
+
no_unquoted_values = Regexp.escape(@row_separator).encode(@encoding)
|
|
667
684
|
no_unquoted_values << @escaped_first_column_separator
|
|
668
685
|
unless @liberal_parsing
|
|
669
686
|
no_unquoted_values << @escaped_quote_character
|
|
@@ -758,7 +775,7 @@ class CSV
|
|
|
758
775
|
case headers
|
|
759
776
|
when Array
|
|
760
777
|
@raw_headers = headers
|
|
761
|
-
quoted_fields =
|
|
778
|
+
quoted_fields = FieldsConverter::NO_QUOTED_FIELDS
|
|
762
779
|
@use_headers = true
|
|
763
780
|
when String
|
|
764
781
|
@raw_headers, quoted_fields = parse_headers(headers)
|
|
@@ -799,6 +816,13 @@ class CSV
|
|
|
799
816
|
|
|
800
817
|
def prepare_parser
|
|
801
818
|
@may_quoted = may_quoted?
|
|
819
|
+
if @quote_character.nil?
|
|
820
|
+
@parse_method = :parse_no_quote
|
|
821
|
+
elsif @liberal_parsing or @strip
|
|
822
|
+
@parse_method = :parse_quotable_robust
|
|
823
|
+
else
|
|
824
|
+
@parse_method = :parse_quotable_loose
|
|
825
|
+
end
|
|
802
826
|
end
|
|
803
827
|
|
|
804
828
|
def may_quoted?
|
|
@@ -863,10 +887,7 @@ class CSV
|
|
|
863
887
|
string = nil
|
|
864
888
|
if @samples.empty? and @input.is_a?(StringIO)
|
|
865
889
|
string = @input.read
|
|
866
|
-
elsif @samples.size == 1 and
|
|
867
|
-
@input != ARGF and
|
|
868
|
-
@input.respond_to?(:eof?) and
|
|
869
|
-
@input.eof?
|
|
890
|
+
elsif @samples.size == 1 and Parser.eof?(@input)
|
|
870
891
|
string = @samples[0]
|
|
871
892
|
end
|
|
872
893
|
if string
|
|
@@ -935,11 +956,9 @@ class CSV
|
|
|
935
956
|
if line.empty?
|
|
936
957
|
next if @skip_blanks
|
|
937
958
|
row = []
|
|
938
|
-
quoted_fields = []
|
|
939
959
|
else
|
|
940
960
|
line = strip_value(line)
|
|
941
961
|
row = line.split(@split_column_separator, -1)
|
|
942
|
-
quoted_fields = [false] * row.size
|
|
943
962
|
if @max_field_size
|
|
944
963
|
row.each do |column|
|
|
945
964
|
validate_field_size(column)
|
|
@@ -953,7 +972,7 @@ class CSV
|
|
|
953
972
|
end
|
|
954
973
|
end
|
|
955
974
|
@last_line = original_line
|
|
956
|
-
emit_row(row,
|
|
975
|
+
emit_row(row, &block)
|
|
957
976
|
end
|
|
958
977
|
end
|
|
959
978
|
|
|
@@ -975,10 +994,10 @@ class CSV
|
|
|
975
994
|
next
|
|
976
995
|
end
|
|
977
996
|
row = []
|
|
978
|
-
quoted_fields =
|
|
997
|
+
quoted_fields = FieldsConverter::NO_QUOTED_FIELDS
|
|
979
998
|
elsif line.include?(@cr) or line.include?(@lf)
|
|
980
999
|
@scanner.keep_back
|
|
981
|
-
@
|
|
1000
|
+
@parse_method = :parse_quotable_robust
|
|
982
1001
|
return parse_quotable_robust(&block)
|
|
983
1002
|
else
|
|
984
1003
|
row = line.split(@split_column_separator, -1)
|
|
@@ -1002,7 +1021,7 @@ class CSV
|
|
|
1002
1021
|
row[i] = column[1..-2]
|
|
1003
1022
|
else
|
|
1004
1023
|
@scanner.keep_back
|
|
1005
|
-
@
|
|
1024
|
+
@parse_method = :parse_quotable_robust
|
|
1006
1025
|
return parse_quotable_robust(&block)
|
|
1007
1026
|
end
|
|
1008
1027
|
validate_field_size(row[i])
|
|
@@ -1037,13 +1056,13 @@ class CSV
|
|
|
1037
1056
|
quoted_fields << @quoted_column_value
|
|
1038
1057
|
elsif parse_row_end
|
|
1039
1058
|
if row.empty? and value.nil?
|
|
1040
|
-
emit_row([],
|
|
1059
|
+
emit_row([], &block) unless @skip_blanks
|
|
1041
1060
|
else
|
|
1042
1061
|
row << value
|
|
1043
1062
|
quoted_fields << @quoted_column_value
|
|
1044
1063
|
emit_row(row, quoted_fields, &block)
|
|
1045
1064
|
row = []
|
|
1046
|
-
quoted_fields
|
|
1065
|
+
quoted_fields.clear
|
|
1047
1066
|
end
|
|
1048
1067
|
skip_needless_lines
|
|
1049
1068
|
start_row
|
|
@@ -1248,7 +1267,7 @@ class CSV
|
|
|
1248
1267
|
@scanner.keep_start
|
|
1249
1268
|
end
|
|
1250
1269
|
|
|
1251
|
-
def emit_row(row, quoted_fields, &block)
|
|
1270
|
+
def emit_row(row, quoted_fields=FieldsConverter::NO_QUOTED_FIELDS, &block)
|
|
1252
1271
|
@lineno += 1
|
|
1253
1272
|
|
|
1254
1273
|
raw_row = row
|
data/lib/csv/row.rb
CHANGED
|
@@ -637,6 +637,7 @@ class CSV
|
|
|
637
637
|
|
|
638
638
|
# :call-seq:
|
|
639
639
|
# row.to_h -> hash
|
|
640
|
+
# row.to_h {|key, value| ... } -> hash
|
|
640
641
|
#
|
|
641
642
|
# Returns the new \Hash formed by adding each header-value pair in +self+
|
|
642
643
|
# as a key-value pair in the \Hash.
|
|
@@ -650,11 +651,34 @@ class CSV
|
|
|
650
651
|
# table = CSV.parse(source, headers: true)
|
|
651
652
|
# row = table[0]
|
|
652
653
|
# row.to_h # => {"Name"=>"Foo"}
|
|
654
|
+
#
|
|
655
|
+
# If a block is given, will call it with (key, value) arguments and use result as a hash entry:
|
|
656
|
+
# source = "Name,Value\nfoo,1\nbar,2\nbaz,3\n"
|
|
657
|
+
# table = CSV.parse(source, headers: true)
|
|
658
|
+
# row = table[0]
|
|
659
|
+
# row.to_h { |key, value| [key, "#{key}-#{value}"] } # => {"Name"=>"Name-foo", "Value"=>"Value-1"}
|
|
653
660
|
def to_h
|
|
654
661
|
hash = {}
|
|
655
|
-
|
|
656
|
-
|
|
662
|
+
|
|
663
|
+
if block_given?
|
|
664
|
+
each do |key, _value|
|
|
665
|
+
result = yield(key, self[key])
|
|
666
|
+
result_array = Array.try_convert(result)
|
|
667
|
+
raise TypeError, "wrong element type #{result.class} (expected array)" if result_array.nil?
|
|
668
|
+
raise ArgumentError, "wrong array length (expected 2, was #{result_array.size})" unless result_array.size == 2
|
|
669
|
+
|
|
670
|
+
key, value = result_array
|
|
671
|
+
next if hash.key?(key)
|
|
672
|
+
|
|
673
|
+
key.freeze if key.is_a?(String) && !key.frozen?
|
|
674
|
+
hash[key] = value
|
|
675
|
+
end
|
|
676
|
+
else
|
|
677
|
+
each do |key, _value|
|
|
678
|
+
hash[key] = self[key] unless hash.key?(key)
|
|
679
|
+
end
|
|
657
680
|
end
|
|
681
|
+
|
|
658
682
|
hash
|
|
659
683
|
end
|
|
660
684
|
alias_method :to_hash, :to_h
|
data/lib/csv/version.rb
CHANGED
data/lib/csv/writer.rb
CHANGED
data/lib/csv.rb
CHANGED
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
|
|
92
92
|
require "forwardable"
|
|
93
93
|
require "date"
|
|
94
|
+
require "time"
|
|
94
95
|
require "stringio"
|
|
95
96
|
|
|
96
97
|
require_relative "csv/fields_converter"
|
|
@@ -521,6 +522,7 @@ require_relative "csv/writer"
|
|
|
521
522
|
# - <tt>:float</tt>: converts each \String-embedded float into a true \Float.
|
|
522
523
|
# - <tt>:date</tt>: converts each \String-embedded date into a true \Date.
|
|
523
524
|
# - <tt>:date_time</tt>: converts each \String-embedded date-time into a true \DateTime
|
|
525
|
+
# - <tt>:time</tt>: converts each \String-embedded time into a true \Time
|
|
524
526
|
# .
|
|
525
527
|
# This example creates a converter proc, then stores it:
|
|
526
528
|
# strip_converter = proc {|field| field.strip }
|
|
@@ -631,6 +633,7 @@ require_relative "csv/writer"
|
|
|
631
633
|
# [:numeric, [:integer, :float]]
|
|
632
634
|
# [:date, Proc]
|
|
633
635
|
# [:date_time, Proc]
|
|
636
|
+
# [:time, Proc]
|
|
634
637
|
# [:all, [:date_time, :numeric]]
|
|
635
638
|
#
|
|
636
639
|
# Each of these converters transcodes values to UTF-8 before attempting conversion.
|
|
@@ -675,6 +678,15 @@ require_relative "csv/writer"
|
|
|
675
678
|
# csv = CSV.parse_line(data, converters: :date_time)
|
|
676
679
|
# csv # => [#<DateTime: 2020-05-07T14:59:00-05:00 ((2458977j,71940s,0n),-18000s,2299161j)>, "x"]
|
|
677
680
|
#
|
|
681
|
+
# Converter +time+ converts each field that Time::parse accepts:
|
|
682
|
+
# data = '2020-05-07T14:59:00-05:00,x'
|
|
683
|
+
# # Without the converter
|
|
684
|
+
# csv = CSV.parse_line(data)
|
|
685
|
+
# csv # => ["2020-05-07T14:59:00-05:00", "x"]
|
|
686
|
+
# # With the converter
|
|
687
|
+
# csv = CSV.parse_line(data, converters: :time)
|
|
688
|
+
# csv # => [2020-05-07 14:59:00 -0500, "x"]
|
|
689
|
+
#
|
|
678
690
|
# Converter +:numeric+ converts with both +:date_time+ and +:numeric+..
|
|
679
691
|
#
|
|
680
692
|
# As seen above, method #convert adds \converters to a \CSV instance,
|
|
@@ -871,10 +883,10 @@ class CSV
|
|
|
871
883
|
# A Regexp used to find and convert some common Date formats.
|
|
872
884
|
DateMatcher = / \A(?: (\w+,?\s+)?\w+\s+\d{1,2},?\s+\d{2,4} |
|
|
873
885
|
\d{4}-\d{2}-\d{2} )\z /x
|
|
874
|
-
# A Regexp used to find and convert some common
|
|
886
|
+
# A Regexp used to find and convert some common (Date)Time formats.
|
|
875
887
|
DateTimeMatcher =
|
|
876
888
|
/ \A(?: (\w+,?\s+)?\w+\s+\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2},?\s+\d{2,4} |
|
|
877
|
-
# ISO-8601 and RFC-3339 (space instead of T) recognized by
|
|
889
|
+
# ISO-8601 and RFC-3339 (space instead of T) recognized by (Date)Time.parse
|
|
878
890
|
\d{4}-\d{2}-\d{2}
|
|
879
891
|
(?:[T\s]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?(?:[+-]\d{2}(?::\d{2})|Z)?)?)?
|
|
880
892
|
)\z /x
|
|
@@ -912,6 +924,14 @@ class CSV
|
|
|
912
924
|
f
|
|
913
925
|
end
|
|
914
926
|
},
|
|
927
|
+
time: lambda { |f|
|
|
928
|
+
begin
|
|
929
|
+
e = f.encode(ConverterEncoding)
|
|
930
|
+
e.match?(DateTimeMatcher) ? Time.parse(e) : f
|
|
931
|
+
rescue # encoding conversion or parse errors
|
|
932
|
+
f
|
|
933
|
+
end
|
|
934
|
+
},
|
|
915
935
|
all: [:date_time, :numeric],
|
|
916
936
|
}
|
|
917
937
|
|
|
@@ -1198,7 +1218,44 @@ class CSV
|
|
|
1198
1218
|
# * Argument +in_string_or_io+ must be a \String or an \IO stream.
|
|
1199
1219
|
# * Argument +out_string_or_io+ must be a \String or an \IO stream.
|
|
1200
1220
|
# * Arguments <tt>**options</tt> must be keyword options.
|
|
1201
|
-
#
|
|
1221
|
+
#
|
|
1222
|
+
# - Each option defined as an {option for parsing}[#class-CSV-label-Options+for+Parsing]
|
|
1223
|
+
# is used for parsing the filter input.
|
|
1224
|
+
# - Each option defined as an {option for generating}[#class-CSV-label-Options+for+Generating]
|
|
1225
|
+
# is used for generator the filter input.
|
|
1226
|
+
#
|
|
1227
|
+
# However, there are three options that may be used for both parsing and generating:
|
|
1228
|
+
# +col_sep+, +quote_char+, and +row_sep+.
|
|
1229
|
+
#
|
|
1230
|
+
# Therefore for method +filter+ (and method +filter+ only),
|
|
1231
|
+
# there are special options that allow these parsing and generating options
|
|
1232
|
+
# to be specified separately:
|
|
1233
|
+
#
|
|
1234
|
+
# - Options +input_col_sep+ and +output_col_sep+
|
|
1235
|
+
# (and their aliases +in_col_sep+ and +out_col_sep+)
|
|
1236
|
+
# specify the column separators for parsing and generating.
|
|
1237
|
+
# - Options +input_quote_char+ and +output_quote_char+
|
|
1238
|
+
# (and their aliases +in_quote_char+ and +out_quote_char+)
|
|
1239
|
+
# specify the quote characters for parsing and generting.
|
|
1240
|
+
# - Options +input_row_sep+ and +output_row_sep+
|
|
1241
|
+
# (and their aliases +in_row_sep+ and +out_row_sep+)
|
|
1242
|
+
# specify the row separators for parsing and generating.
|
|
1243
|
+
#
|
|
1244
|
+
# Example options (for column separators):
|
|
1245
|
+
#
|
|
1246
|
+
# CSV.filter # Default for both parsing and generating.
|
|
1247
|
+
# CSV.filter(in_col_sep: ';') # ';' for parsing, default for generating.
|
|
1248
|
+
# CSV.filter(out_col_sep: '|') # Default for parsing, '|' for generating.
|
|
1249
|
+
# CSV.filter(in_col_sep: ';', out_col_sep: '|') # ';' for parsing, '|' for generating.
|
|
1250
|
+
#
|
|
1251
|
+
# Note that for a special option (e.g., +input_col_sep+)
|
|
1252
|
+
# and its corresponding "regular" option (e.g., +col_sep+),
|
|
1253
|
+
# the two are mutually overriding.
|
|
1254
|
+
#
|
|
1255
|
+
# Another example (possibly surprising):
|
|
1256
|
+
#
|
|
1257
|
+
# CSV.filter(in_col_sep: ';', col_sep: '|') # '|' for both parsing(!) and generating.
|
|
1258
|
+
#
|
|
1202
1259
|
def filter(input=nil, output=nil, **options)
|
|
1203
1260
|
# parse options for input, output, or both
|
|
1204
1261
|
in_options, out_options = Hash.new, {row_sep: InputRecordSeparator.value}
|
|
@@ -1320,7 +1377,7 @@ class CSV
|
|
|
1320
1377
|
# * Arguments <tt>**options</tt> must be keyword options.
|
|
1321
1378
|
# See {Options for Parsing}[#class-CSV-label-Options+for+Parsing].
|
|
1322
1379
|
# * This method optionally accepts an additional <tt>:encoding</tt> option
|
|
1323
|
-
# that you can use to specify the Encoding of the data read from +
|
|
1380
|
+
# that you can use to specify the Encoding of the data read from +path_or_io+.
|
|
1324
1381
|
# You must provide this unless your data is in the encoding
|
|
1325
1382
|
# given by <tt>Encoding::default_external</tt>.
|
|
1326
1383
|
# Parsing will use this to determine how to parse the data.
|
|
@@ -1329,9 +1386,9 @@ class CSV
|
|
|
1329
1386
|
# encoding: 'UTF-32BE:UTF-8'
|
|
1330
1387
|
# would read +UTF-32BE+ data from the file
|
|
1331
1388
|
# but transcode it to +UTF-8+ before parsing.
|
|
1332
|
-
def foreach(
|
|
1333
|
-
return to_enum(__method__,
|
|
1334
|
-
open(
|
|
1389
|
+
def foreach(path_or_io, mode="r", **options, &block)
|
|
1390
|
+
return to_enum(__method__, path_or_io, mode, **options) unless block_given?
|
|
1391
|
+
open(path_or_io, mode, **options) do |csv|
|
|
1335
1392
|
csv.each(&block)
|
|
1336
1393
|
end
|
|
1337
1394
|
end
|
|
@@ -1508,10 +1565,8 @@ class CSV
|
|
|
1508
1565
|
|
|
1509
1566
|
#
|
|
1510
1567
|
# :call-seq:
|
|
1511
|
-
# open(
|
|
1512
|
-
# open(
|
|
1513
|
-
# open(file_path, mode = "rb", **options ) { |csv| ... } -> object
|
|
1514
|
-
# open(io, mode = "rb", **options ) { |csv| ... } -> object
|
|
1568
|
+
# open(path_or_io, mode = "rb", **options ) -> new_csv
|
|
1569
|
+
# open(path_or_io, mode = "rb", **options ) { |csv| ... } -> object
|
|
1515
1570
|
#
|
|
1516
1571
|
# possible options elements:
|
|
1517
1572
|
# keyword form:
|
|
@@ -1520,14 +1575,14 @@ class CSV
|
|
|
1520
1575
|
# :undef => :replace # replace undefined conversion
|
|
1521
1576
|
# :replace => string # replacement string ("?" or "\uFFFD" if not specified)
|
|
1522
1577
|
#
|
|
1523
|
-
# * Argument +
|
|
1578
|
+
# * Argument +path_or_io+, must be a file path or an \IO stream.
|
|
1524
1579
|
# :include: ../doc/csv/arguments/io.rdoc
|
|
1525
1580
|
# * Argument +mode+, if given, must be a \File mode.
|
|
1526
1581
|
# See {Access Modes}[https://docs.ruby-lang.org/en/master/File.html#class-File-label-Access+Modes].
|
|
1527
1582
|
# * Arguments <tt>**options</tt> must be keyword options.
|
|
1528
1583
|
# See {Options for Generating}[#class-CSV-label-Options+for+Generating].
|
|
1529
1584
|
# * This method optionally accepts an additional <tt>:encoding</tt> option
|
|
1530
|
-
# that you can use to specify the Encoding of the data read from +
|
|
1585
|
+
# that you can use to specify the Encoding of the data read from +path_or_io+.
|
|
1531
1586
|
# You must provide this unless your data is in the encoding
|
|
1532
1587
|
# given by <tt>Encoding::default_external</tt>.
|
|
1533
1588
|
# Parsing will use this to determine how to parse the data.
|
|
@@ -1544,6 +1599,9 @@ class CSV
|
|
|
1544
1599
|
# path = 't.csv'
|
|
1545
1600
|
# File.write(path, string)
|
|
1546
1601
|
#
|
|
1602
|
+
# string_io = StringIO.new
|
|
1603
|
+
# string_io << "foo,0\nbar,1\nbaz,2\n"
|
|
1604
|
+
#
|
|
1547
1605
|
# ---
|
|
1548
1606
|
#
|
|
1549
1607
|
# With no block given, returns a new \CSV object.
|
|
@@ -1556,6 +1614,9 @@ class CSV
|
|
|
1556
1614
|
# csv = CSV.open(File.open(path))
|
|
1557
1615
|
# csv # => #<CSV io_type:File io_path:"t.csv" encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
|
|
1558
1616
|
#
|
|
1617
|
+
# Create a \CSV object using a \StringIO:
|
|
1618
|
+
# csv = CSV.open(string_io)
|
|
1619
|
+
# csv # => #<CSV io_type:StringIO encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
|
|
1559
1620
|
# ---
|
|
1560
1621
|
#
|
|
1561
1622
|
# With a block given, calls the block with the created \CSV object;
|
|
@@ -1573,15 +1634,25 @@ class CSV
|
|
|
1573
1634
|
# Output:
|
|
1574
1635
|
# #<CSV io_type:File io_path:"t.csv" encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
|
|
1575
1636
|
#
|
|
1637
|
+
# Using a \StringIO:
|
|
1638
|
+
# csv = CSV.open(string_io) {|csv| p csv}
|
|
1639
|
+
# csv # => #<CSV io_type:StringIO encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
|
|
1640
|
+
# Output:
|
|
1641
|
+
# #<CSV io_type:StringIO encoding:UTF-8 lineno:0 col_sep:"," row_sep:"\n" quote_char:"\"">
|
|
1576
1642
|
# ---
|
|
1577
1643
|
#
|
|
1578
1644
|
# Raises an exception if the argument is not a \String object or \IO object:
|
|
1579
1645
|
# # Raises TypeError (no implicit conversion of Symbol into String)
|
|
1580
1646
|
# CSV.open(:foo)
|
|
1581
|
-
def open(
|
|
1647
|
+
def open(path_or_io, mode="r", **options)
|
|
1582
1648
|
# wrap a File opened with the remaining +args+ with no newline
|
|
1583
1649
|
# decorator
|
|
1584
|
-
file_opts =
|
|
1650
|
+
file_opts = {}
|
|
1651
|
+
may_enable_bom_detection_automatically(path_or_io,
|
|
1652
|
+
mode,
|
|
1653
|
+
options,
|
|
1654
|
+
file_opts)
|
|
1655
|
+
file_opts.merge!(options)
|
|
1585
1656
|
unless file_opts.key?(:newline)
|
|
1586
1657
|
file_opts[:universal_newline] ||= false
|
|
1587
1658
|
end
|
|
@@ -1590,14 +1661,19 @@ class CSV
|
|
|
1590
1661
|
options.delete(:replace)
|
|
1591
1662
|
options.delete_if {|k, _| /newline\z/.match?(k)}
|
|
1592
1663
|
|
|
1593
|
-
|
|
1594
|
-
f =
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1664
|
+
if path_or_io.is_a?(StringIO)
|
|
1665
|
+
f = create_stringio(path_or_io.string, mode, **file_opts)
|
|
1666
|
+
else
|
|
1667
|
+
begin
|
|
1668
|
+
f = File.open(path_or_io, mode, **file_opts)
|
|
1669
|
+
rescue ArgumentError => e
|
|
1670
|
+
raise unless /needs binmode/.match?(e.message) and mode == "r"
|
|
1671
|
+
mode = "rb"
|
|
1672
|
+
file_opts = {encoding: Encoding.default_external}.merge(file_opts)
|
|
1673
|
+
retry
|
|
1674
|
+
end
|
|
1600
1675
|
end
|
|
1676
|
+
|
|
1601
1677
|
begin
|
|
1602
1678
|
csv = new(f, **options)
|
|
1603
1679
|
rescue Exception
|
|
@@ -1729,6 +1805,23 @@ class CSV
|
|
|
1729
1805
|
# Raises an exception if the argument is not a \String object or \IO object:
|
|
1730
1806
|
# # Raises NoMethodError (undefined method `close' for :foo:Symbol)
|
|
1731
1807
|
# CSV.parse(:foo)
|
|
1808
|
+
#
|
|
1809
|
+
# ---
|
|
1810
|
+
#
|
|
1811
|
+
# Please make sure if your text contains \BOM or not. CSV.parse will not remove
|
|
1812
|
+
# \BOM automatically. You might want to remove \BOM before calling CSV.parse :
|
|
1813
|
+
# # remove BOM on calling File.open
|
|
1814
|
+
# File.open(path, encoding: 'bom|utf-8') do |file|
|
|
1815
|
+
# CSV.parse(file, headers: true) do |row|
|
|
1816
|
+
# # you can get value by column name because BOM is removed
|
|
1817
|
+
# p row['Name']
|
|
1818
|
+
# end
|
|
1819
|
+
# end
|
|
1820
|
+
#
|
|
1821
|
+
# Output:
|
|
1822
|
+
# # "foo"
|
|
1823
|
+
# # "bar"
|
|
1824
|
+
# # "baz"
|
|
1732
1825
|
def parse(str, **options, &block)
|
|
1733
1826
|
csv = new(str, **options)
|
|
1734
1827
|
|
|
@@ -1808,10 +1901,10 @@ class CSV
|
|
|
1808
1901
|
|
|
1809
1902
|
#
|
|
1810
1903
|
# :call-seq:
|
|
1811
|
-
# read(
|
|
1812
|
-
# read(
|
|
1904
|
+
# read(path_or_io, **options) -> array_of_arrays
|
|
1905
|
+
# read(path_or_io, headers: true, **options) -> csv_table
|
|
1813
1906
|
#
|
|
1814
|
-
# Opens the given +
|
|
1907
|
+
# Opens the given +path_or_io+ with the given +options+ (see CSV.open),
|
|
1815
1908
|
# reads the source (see CSV#read), and returns the result,
|
|
1816
1909
|
# which will be either an \Array of Arrays or a CSV::Table.
|
|
1817
1910
|
#
|
|
@@ -1826,22 +1919,22 @@ class CSV
|
|
|
1826
1919
|
# path = 't.csv'
|
|
1827
1920
|
# File.write(path, string)
|
|
1828
1921
|
# CSV.read(path, headers: true) # => #<CSV::Table mode:col_or_row row_count:4>
|
|
1829
|
-
def read(
|
|
1830
|
-
open(
|
|
1922
|
+
def read(path_or_io, **options)
|
|
1923
|
+
open(path_or_io, **options) { |csv| csv.read }
|
|
1831
1924
|
end
|
|
1832
1925
|
|
|
1833
1926
|
# :call-seq:
|
|
1834
|
-
# CSV.readlines(
|
|
1927
|
+
# CSV.readlines(path_or_io, **options)
|
|
1835
1928
|
#
|
|
1836
1929
|
# Alias for CSV.read.
|
|
1837
|
-
def readlines(
|
|
1838
|
-
read(
|
|
1930
|
+
def readlines(path_or_io, **options)
|
|
1931
|
+
read(path_or_io, **options)
|
|
1839
1932
|
end
|
|
1840
1933
|
|
|
1841
1934
|
# :call-seq:
|
|
1842
|
-
# CSV.table(
|
|
1935
|
+
# CSV.table(path_or_io, **options)
|
|
1843
1936
|
#
|
|
1844
|
-
# Calls CSV.read with +
|
|
1937
|
+
# Calls CSV.read with +path_or_io+, +options+, and certain default options:
|
|
1845
1938
|
# - +headers+: +true+
|
|
1846
1939
|
# - +converters+: +:numeric+
|
|
1847
1940
|
# - +header_converters+: +:symbol+
|
|
@@ -1853,14 +1946,50 @@ class CSV
|
|
|
1853
1946
|
# path = 't.csv'
|
|
1854
1947
|
# File.write(path, string)
|
|
1855
1948
|
# CSV.table(path) # => #<CSV::Table mode:col_or_row row_count:4>
|
|
1856
|
-
def table(
|
|
1949
|
+
def table(path_or_io, **options)
|
|
1857
1950
|
default_options = {
|
|
1858
1951
|
headers: true,
|
|
1859
1952
|
converters: :numeric,
|
|
1860
1953
|
header_converters: :symbol,
|
|
1861
1954
|
}
|
|
1862
1955
|
options = default_options.merge(options)
|
|
1863
|
-
read(
|
|
1956
|
+
read(path_or_io, **options)
|
|
1957
|
+
end
|
|
1958
|
+
|
|
1959
|
+
ON_WINDOWS = /mingw|mswin/.match?(RUBY_PLATFORM)
|
|
1960
|
+
private_constant :ON_WINDOWS
|
|
1961
|
+
|
|
1962
|
+
private
|
|
1963
|
+
def may_enable_bom_detection_automatically(path_or_io,
|
|
1964
|
+
mode,
|
|
1965
|
+
options,
|
|
1966
|
+
file_opts)
|
|
1967
|
+
if path_or_io.is_a?(StringIO)
|
|
1968
|
+
# Support to StringIO was dropped for Ruby 2.6 and earlier without BOM support:
|
|
1969
|
+
# https://github.com/ruby/stringio/pull/47
|
|
1970
|
+
return if RUBY_VERSION < "2.7"
|
|
1971
|
+
else
|
|
1972
|
+
# "bom|utf-8" may be buggy on Windows:
|
|
1973
|
+
# https://bugs.ruby-lang.org/issues/20526
|
|
1974
|
+
return if ON_WINDOWS
|
|
1975
|
+
end
|
|
1976
|
+
return unless Encoding.default_external == Encoding::UTF_8
|
|
1977
|
+
return if options.key?(:encoding)
|
|
1978
|
+
return if options.key?(:external_encoding)
|
|
1979
|
+
return if mode.is_a?(String) and mode.include?(":")
|
|
1980
|
+
file_opts[:encoding] = "bom|utf-8"
|
|
1981
|
+
end
|
|
1982
|
+
|
|
1983
|
+
if RUBY_VERSION < "2.7"
|
|
1984
|
+
def create_stringio(str, mode, opts)
|
|
1985
|
+
opts.delete_if {|k, _| k == :universal_newline or DEFAULT_OPTIONS.key?(k)}
|
|
1986
|
+
raise ArgumentError, "Unsupported options parsing StringIO: #{opts.keys}" unless opts.empty?
|
|
1987
|
+
StringIO.new(str, mode)
|
|
1988
|
+
end
|
|
1989
|
+
else
|
|
1990
|
+
def create_stringio(str, mode, opts)
|
|
1991
|
+
StringIO.new(str, mode, **opts)
|
|
1992
|
+
end
|
|
1864
1993
|
end
|
|
1865
1994
|
end
|
|
1866
1995
|
|
|
@@ -1981,8 +2110,6 @@ class CSV
|
|
|
1981
2110
|
strip: strip,
|
|
1982
2111
|
}
|
|
1983
2112
|
@parser = nil
|
|
1984
|
-
@parser_enumerator = nil
|
|
1985
|
-
@eof_error = nil
|
|
1986
2113
|
|
|
1987
2114
|
@writer_options = {
|
|
1988
2115
|
encoding: @encoding,
|
|
@@ -2000,6 +2127,12 @@ class CSV
|
|
|
2000
2127
|
writer if @writer_options[:write_headers]
|
|
2001
2128
|
end
|
|
2002
2129
|
|
|
2130
|
+
class TSV < CSV
|
|
2131
|
+
def initialize(data, **options)
|
|
2132
|
+
super(data, **({col_sep: "\t"}.merge(options)))
|
|
2133
|
+
end
|
|
2134
|
+
end
|
|
2135
|
+
|
|
2003
2136
|
# :call-seq:
|
|
2004
2137
|
# csv.col_sep -> string
|
|
2005
2138
|
#
|
|
@@ -2192,7 +2325,7 @@ class CSV
|
|
|
2192
2325
|
attr_reader :encoding
|
|
2193
2326
|
|
|
2194
2327
|
# :call-seq:
|
|
2195
|
-
# csv.
|
|
2328
|
+
# csv.lineno -> integer
|
|
2196
2329
|
#
|
|
2197
2330
|
# Returns the count of the rows parsed or generated.
|
|
2198
2331
|
#
|
|
@@ -2295,24 +2428,13 @@ class CSV
|
|
|
2295
2428
|
end
|
|
2296
2429
|
|
|
2297
2430
|
def eof?
|
|
2298
|
-
|
|
2299
|
-
begin
|
|
2300
|
-
parser_enumerator.peek
|
|
2301
|
-
false
|
|
2302
|
-
rescue MalformedCSVError => error
|
|
2303
|
-
@eof_error = error
|
|
2304
|
-
false
|
|
2305
|
-
rescue StopIteration
|
|
2306
|
-
true
|
|
2307
|
-
end
|
|
2431
|
+
parser.eof?
|
|
2308
2432
|
end
|
|
2309
2433
|
alias_method :eof, :eof?
|
|
2310
2434
|
|
|
2311
2435
|
# Rewinds the underlying IO object and resets CSV's lineno() counter.
|
|
2312
2436
|
def rewind
|
|
2313
2437
|
@parser = nil
|
|
2314
|
-
@parser_enumerator = nil
|
|
2315
|
-
@eof_error = nil
|
|
2316
2438
|
@writer.rewind if @writer
|
|
2317
2439
|
@io.rewind
|
|
2318
2440
|
end
|
|
@@ -2552,13 +2674,7 @@ class CSV
|
|
|
2552
2674
|
# p row
|
|
2553
2675
|
# end
|
|
2554
2676
|
def each(&block)
|
|
2555
|
-
|
|
2556
|
-
begin
|
|
2557
|
-
while true
|
|
2558
|
-
yield(parser_enumerator.next)
|
|
2559
|
-
end
|
|
2560
|
-
rescue StopIteration
|
|
2561
|
-
end
|
|
2677
|
+
parser.parse(&block)
|
|
2562
2678
|
end
|
|
2563
2679
|
|
|
2564
2680
|
# :call-seq:
|
|
@@ -2666,15 +2782,10 @@ class CSV
|
|
|
2666
2782
|
# # Raises IOError (not opened for reading)
|
|
2667
2783
|
# csv.shift
|
|
2668
2784
|
def shift
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
raise eof_error
|
|
2672
|
-
end
|
|
2673
|
-
begin
|
|
2674
|
-
parser_enumerator.next
|
|
2675
|
-
rescue StopIteration
|
|
2676
|
-
nil
|
|
2785
|
+
parser.parse do |row|
|
|
2786
|
+
return row
|
|
2677
2787
|
end
|
|
2788
|
+
nil
|
|
2678
2789
|
end
|
|
2679
2790
|
alias_method :gets, :shift
|
|
2680
2791
|
alias_method :readline, :shift
|
|
@@ -2836,10 +2947,6 @@ class CSV
|
|
|
2836
2947
|
fields_converter: parser_fields_converter)
|
|
2837
2948
|
end
|
|
2838
2949
|
|
|
2839
|
-
def parser_enumerator
|
|
2840
|
-
@parser_enumerator ||= parser.parse
|
|
2841
|
-
end
|
|
2842
|
-
|
|
2843
2950
|
def writer
|
|
2844
2951
|
@writer ||= Writer.new(@io, writer_options)
|
|
2845
2952
|
end
|
metadata
CHANGED
|
@@ -1,72 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: csv
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.3.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- James Edward Gray II
|
|
8
8
|
- Kouhei Sutou
|
|
9
|
-
autorequire:
|
|
10
9
|
bindir: bin
|
|
11
10
|
cert_chain: []
|
|
12
|
-
date:
|
|
13
|
-
dependencies:
|
|
14
|
-
- !ruby/object:Gem::Dependency
|
|
15
|
-
name: bundler
|
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
|
17
|
-
requirements:
|
|
18
|
-
- - ">="
|
|
19
|
-
- !ruby/object:Gem::Version
|
|
20
|
-
version: '0'
|
|
21
|
-
type: :development
|
|
22
|
-
prerelease: false
|
|
23
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
24
|
-
requirements:
|
|
25
|
-
- - ">="
|
|
26
|
-
- !ruby/object:Gem::Version
|
|
27
|
-
version: '0'
|
|
28
|
-
- !ruby/object:Gem::Dependency
|
|
29
|
-
name: rake
|
|
30
|
-
requirement: !ruby/object:Gem::Requirement
|
|
31
|
-
requirements:
|
|
32
|
-
- - ">="
|
|
33
|
-
- !ruby/object:Gem::Version
|
|
34
|
-
version: '0'
|
|
35
|
-
type: :development
|
|
36
|
-
prerelease: false
|
|
37
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
38
|
-
requirements:
|
|
39
|
-
- - ">="
|
|
40
|
-
- !ruby/object:Gem::Version
|
|
41
|
-
version: '0'
|
|
42
|
-
- !ruby/object:Gem::Dependency
|
|
43
|
-
name: benchmark_driver
|
|
44
|
-
requirement: !ruby/object:Gem::Requirement
|
|
45
|
-
requirements:
|
|
46
|
-
- - ">="
|
|
47
|
-
- !ruby/object:Gem::Version
|
|
48
|
-
version: '0'
|
|
49
|
-
type: :development
|
|
50
|
-
prerelease: false
|
|
51
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
52
|
-
requirements:
|
|
53
|
-
- - ">="
|
|
54
|
-
- !ruby/object:Gem::Version
|
|
55
|
-
version: '0'
|
|
56
|
-
- !ruby/object:Gem::Dependency
|
|
57
|
-
name: test-unit
|
|
58
|
-
requirement: !ruby/object:Gem::Requirement
|
|
59
|
-
requirements:
|
|
60
|
-
- - ">="
|
|
61
|
-
- !ruby/object:Gem::Version
|
|
62
|
-
version: 3.4.8
|
|
63
|
-
type: :development
|
|
64
|
-
prerelease: false
|
|
65
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
-
requirements:
|
|
67
|
-
- - ">="
|
|
68
|
-
- !ruby/object:Gem::Version
|
|
69
|
-
version: 3.4.8
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
70
13
|
description: The CSV library provides a complete interface to CSV files and data.
|
|
71
14
|
It offers tools to enable you to read and write to and from Strings or IO objects,
|
|
72
15
|
as needed.
|
|
@@ -127,8 +70,8 @@ homepage: https://github.com/ruby/csv
|
|
|
127
70
|
licenses:
|
|
128
71
|
- Ruby
|
|
129
72
|
- BSD-2-Clause
|
|
130
|
-
metadata:
|
|
131
|
-
|
|
73
|
+
metadata:
|
|
74
|
+
changelog_uri: https://github.com/ruby/csv/releases/tag/v3.3.6
|
|
132
75
|
rdoc_options:
|
|
133
76
|
- "--main"
|
|
134
77
|
- README.md
|
|
@@ -145,8 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
145
88
|
- !ruby/object:Gem::Version
|
|
146
89
|
version: '0'
|
|
147
90
|
requirements: []
|
|
148
|
-
rubygems_version:
|
|
149
|
-
signing_key:
|
|
91
|
+
rubygems_version: 4.0.16
|
|
150
92
|
specification_version: 4
|
|
151
93
|
summary: CSV Reading and Writing
|
|
152
94
|
test_files: []
|