csv 3.0.0 → 3.1.0
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 +377 -0
- data/README.md +1 -0
- data/lib/csv/delete_suffix.rb +18 -0
- data/lib/csv/fields_converter.rb +78 -0
- data/lib/csv/match_p.rb +20 -0
- data/lib/csv/parser.rb +1092 -0
- data/lib/csv/row.rb +6 -6
- data/lib/csv/table.rb +28 -4
- data/lib/csv/version.rb +1 -1
- data/lib/csv/writer.rb +156 -0
- data/lib/csv.rb +330 -632
- metadata +24 -5
- data/news.md +0 -123
data/lib/csv/row.rb
CHANGED
@@ -48,6 +48,11 @@ class CSV
|
|
48
48
|
extend Forwardable
|
49
49
|
def_delegators :@row, :empty?, :length, :size
|
50
50
|
|
51
|
+
def initialize_copy(other)
|
52
|
+
super
|
53
|
+
@row = @row.dup
|
54
|
+
end
|
55
|
+
|
51
56
|
# Returns +true+ if this is a header row.
|
52
57
|
def header_row?
|
53
58
|
@header_row
|
@@ -125,6 +130,7 @@ class CSV
|
|
125
130
|
alias_method :include?, :has_key?
|
126
131
|
alias_method :key?, :has_key?
|
127
132
|
alias_method :member?, :has_key?
|
133
|
+
alias_method :header?, :has_key?
|
128
134
|
|
129
135
|
#
|
130
136
|
# :call-seq:
|
@@ -281,12 +287,6 @@ class CSV
|
|
281
287
|
index.nil? ? nil : index + minimum_index
|
282
288
|
end
|
283
289
|
|
284
|
-
# Returns +true+ if +name+ is a header for this row, and +false+ otherwise.
|
285
|
-
def header?(name)
|
286
|
-
headers.include? name
|
287
|
-
end
|
288
|
-
alias_method :include?, :header?
|
289
|
-
|
290
290
|
#
|
291
291
|
# Returns +true+ if +data+ matches a field in this row, and +false+
|
292
292
|
# otherwise.
|
data/lib/csv/table.rb
CHANGED
@@ -16,6 +16,11 @@ class CSV
|
|
16
16
|
# Construct a new CSV::Table from +array_of_rows+, which are expected
|
17
17
|
# to be CSV::Row objects. All rows are assumed to have the same headers.
|
18
18
|
#
|
19
|
+
# The optional +headers+ parameter can be set to Array of headers.
|
20
|
+
# If headers aren't set, headers are fetched from CSV::Row objects.
|
21
|
+
# Otherwise, headers() method will return headers being set in
|
22
|
+
# headers argument.
|
23
|
+
#
|
19
24
|
# A CSV::Table object supports the following Array methods through
|
20
25
|
# delegation:
|
21
26
|
#
|
@@ -23,8 +28,17 @@ class CSV
|
|
23
28
|
# * length()
|
24
29
|
# * size()
|
25
30
|
#
|
26
|
-
def initialize(array_of_rows)
|
31
|
+
def initialize(array_of_rows, headers: nil)
|
27
32
|
@table = array_of_rows
|
33
|
+
@headers = headers
|
34
|
+
unless @headers
|
35
|
+
if @table.empty?
|
36
|
+
@headers = []
|
37
|
+
else
|
38
|
+
@headers = @table.first.headers
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
28
42
|
@mode = :col_or_row
|
29
43
|
end
|
30
44
|
|
@@ -119,11 +133,12 @@ class CSV
|
|
119
133
|
|
120
134
|
#
|
121
135
|
# Returns the headers for the first row of this table (assumed to match all
|
122
|
-
# other rows).
|
136
|
+
# other rows). The headers Array passed to CSV::Table.new is returned for
|
137
|
+
# empty tables.
|
123
138
|
#
|
124
139
|
def headers
|
125
140
|
if @table.empty?
|
126
|
-
|
141
|
+
@headers.dup
|
127
142
|
else
|
128
143
|
@table.first.headers
|
129
144
|
end
|
@@ -171,6 +186,10 @@ class CSV
|
|
171
186
|
@table[index_or_header] = value
|
172
187
|
end
|
173
188
|
else # set column
|
189
|
+
unless index_or_header.is_a? Integer
|
190
|
+
index = @headers.index(index_or_header) || @headers.size
|
191
|
+
@headers[index] = index_or_header
|
192
|
+
end
|
174
193
|
if value.is_a? Array # multiple values
|
175
194
|
@table.each_with_index do |row, i|
|
176
195
|
if row.header_row?
|
@@ -258,6 +277,11 @@ class CSV
|
|
258
277
|
(@mode == :col_or_row and index_or_header.is_a? Integer)
|
259
278
|
@table.delete_at(index_or_header)
|
260
279
|
else # by header
|
280
|
+
if index_or_header.is_a? Integer
|
281
|
+
@headers.delete_at(index_or_header)
|
282
|
+
else
|
283
|
+
@headers.delete(index_or_header)
|
284
|
+
end
|
261
285
|
@table.map { |row| row.delete(index_or_header).last }
|
262
286
|
end
|
263
287
|
end
|
@@ -375,4 +399,4 @@ class CSV
|
|
375
399
|
"#<#{self.class} mode:#{@mode} row_count:#{to_a.size}>".encode("US-ASCII")
|
376
400
|
end
|
377
401
|
end
|
378
|
-
end
|
402
|
+
end
|
data/lib/csv/version.rb
CHANGED
data/lib/csv/writer.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "match_p"
|
4
|
+
require_relative "row"
|
5
|
+
|
6
|
+
using CSV::MatchP if CSV.const_defined?(:MatchP)
|
7
|
+
|
8
|
+
class CSV
|
9
|
+
class Writer
|
10
|
+
attr_reader :lineno
|
11
|
+
attr_reader :headers
|
12
|
+
|
13
|
+
def initialize(output, options)
|
14
|
+
@output = output
|
15
|
+
@options = options
|
16
|
+
@lineno = 0
|
17
|
+
prepare
|
18
|
+
if @options[:write_headers] and @headers
|
19
|
+
self << @headers
|
20
|
+
end
|
21
|
+
@fields_converter = @options[:fields_converter]
|
22
|
+
end
|
23
|
+
|
24
|
+
def <<(row)
|
25
|
+
case row
|
26
|
+
when Row
|
27
|
+
row = row.fields
|
28
|
+
when Hash
|
29
|
+
row = @headers.collect {|header| row[header]}
|
30
|
+
end
|
31
|
+
|
32
|
+
@headers ||= row if @use_headers
|
33
|
+
@lineno += 1
|
34
|
+
|
35
|
+
row = @fields_converter.convert(row, nil, lineno) if @fields_converter
|
36
|
+
|
37
|
+
converted_row = row.collect do |field|
|
38
|
+
quote(field)
|
39
|
+
end
|
40
|
+
line = converted_row.join(@column_separator) + @row_separator
|
41
|
+
if @output_encoding
|
42
|
+
line = line.encode(@output_encoding)
|
43
|
+
end
|
44
|
+
@output << line
|
45
|
+
|
46
|
+
self
|
47
|
+
end
|
48
|
+
|
49
|
+
def rewind
|
50
|
+
@lineno = 0
|
51
|
+
@headers = nil if @options[:headers].nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def prepare
|
56
|
+
@encoding = @options[:encoding]
|
57
|
+
|
58
|
+
prepare_header
|
59
|
+
prepare_format
|
60
|
+
prepare_output
|
61
|
+
end
|
62
|
+
|
63
|
+
def prepare_header
|
64
|
+
headers = @options[:headers]
|
65
|
+
case headers
|
66
|
+
when Array
|
67
|
+
@headers = headers
|
68
|
+
@use_headers = true
|
69
|
+
when String
|
70
|
+
@headers = CSV.parse_line(headers,
|
71
|
+
col_sep: @options[:column_separator],
|
72
|
+
row_sep: @options[:row_separator],
|
73
|
+
quote_char: @options[:quote_character])
|
74
|
+
@use_headers = true
|
75
|
+
when true
|
76
|
+
@headers = nil
|
77
|
+
@use_headers = true
|
78
|
+
else
|
79
|
+
@headers = nil
|
80
|
+
@use_headers = false
|
81
|
+
end
|
82
|
+
return unless @headers
|
83
|
+
|
84
|
+
converter = @options[:header_fields_converter]
|
85
|
+
@headers = converter.convert(@headers, nil, 0)
|
86
|
+
@headers.each do |header|
|
87
|
+
header.freeze if header.is_a?(String)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def prepare_format
|
92
|
+
@column_separator = @options[:column_separator].to_s.encode(@encoding)
|
93
|
+
row_separator = @options[:row_separator]
|
94
|
+
if row_separator == :auto
|
95
|
+
@row_separator = $INPUT_RECORD_SEPARATOR.encode(@encoding)
|
96
|
+
else
|
97
|
+
@row_separator = row_separator.to_s.encode(@encoding)
|
98
|
+
end
|
99
|
+
@quote_character = @options[:quote_character]
|
100
|
+
@force_quotes = @options[:force_quotes]
|
101
|
+
unless @force_quotes
|
102
|
+
@quotable_pattern =
|
103
|
+
Regexp.new("[\r\n".encode(@encoding) +
|
104
|
+
Regexp.escape(@column_separator) +
|
105
|
+
Regexp.escape(@quote_character.encode(@encoding)) +
|
106
|
+
"]".encode(@encoding))
|
107
|
+
end
|
108
|
+
@quote_empty = @options.fetch(:quote_empty, true)
|
109
|
+
end
|
110
|
+
|
111
|
+
def prepare_output
|
112
|
+
@output_encoding = nil
|
113
|
+
return unless @output.is_a?(StringIO)
|
114
|
+
|
115
|
+
output_encoding = @output.internal_encoding || @output.external_encoding
|
116
|
+
if @encoding != output_encoding
|
117
|
+
if @options[:force_encoding]
|
118
|
+
@output_encoding = output_encoding
|
119
|
+
else
|
120
|
+
compatible_encoding = Encoding.compatible?(@encoding, output_encoding)
|
121
|
+
if compatible_encoding
|
122
|
+
@output.set_encoding(compatible_encoding)
|
123
|
+
@output.seek(0, IO::SEEK_END)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def quote_field(field)
|
130
|
+
field = String(field)
|
131
|
+
encoded_quote_character = @quote_character.encode(field.encoding)
|
132
|
+
encoded_quote_character +
|
133
|
+
field.gsub(encoded_quote_character,
|
134
|
+
encoded_quote_character * 2) +
|
135
|
+
encoded_quote_character
|
136
|
+
end
|
137
|
+
|
138
|
+
def quote(field)
|
139
|
+
if @force_quotes
|
140
|
+
quote_field(field)
|
141
|
+
else
|
142
|
+
if field.nil? # represent +nil+ fields as empty unquoted fields
|
143
|
+
""
|
144
|
+
else
|
145
|
+
field = String(field) # Stringify fields
|
146
|
+
# represent empty fields as empty quoted fields
|
147
|
+
if (@quote_empty and field.empty?) or @quotable_pattern.match?(field)
|
148
|
+
quote_field(field)
|
149
|
+
else
|
150
|
+
field # unquoted field
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|