csv 1.0.2 → 3.2.7
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 +868 -0
- data/README.md +6 -3
- data/doc/csv/arguments/io.rdoc +5 -0
- data/doc/csv/options/common/col_sep.rdoc +57 -0
- data/doc/csv/options/common/quote_char.rdoc +42 -0
- data/doc/csv/options/common/row_sep.rdoc +91 -0
- data/doc/csv/options/generating/force_quotes.rdoc +17 -0
- data/doc/csv/options/generating/quote_empty.rdoc +12 -0
- data/doc/csv/options/generating/write_converters.rdoc +25 -0
- data/doc/csv/options/generating/write_empty_value.rdoc +15 -0
- data/doc/csv/options/generating/write_headers.rdoc +29 -0
- data/doc/csv/options/generating/write_nil_value.rdoc +14 -0
- data/doc/csv/options/parsing/converters.rdoc +46 -0
- data/doc/csv/options/parsing/empty_value.rdoc +13 -0
- data/doc/csv/options/parsing/field_size_limit.rdoc +39 -0
- data/doc/csv/options/parsing/header_converters.rdoc +43 -0
- data/doc/csv/options/parsing/headers.rdoc +63 -0
- data/doc/csv/options/parsing/liberal_parsing.rdoc +38 -0
- data/doc/csv/options/parsing/nil_value.rdoc +12 -0
- data/doc/csv/options/parsing/return_headers.rdoc +22 -0
- data/doc/csv/options/parsing/skip_blanks.rdoc +31 -0
- data/doc/csv/options/parsing/skip_lines.rdoc +37 -0
- data/doc/csv/options/parsing/strip.rdoc +15 -0
- data/doc/csv/options/parsing/unconverted_fields.rdoc +27 -0
- 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/core_ext/array.rb +1 -1
- data/lib/csv/core_ext/string.rb +1 -1
- data/lib/csv/fields_converter.rb +89 -0
- data/lib/csv/input_record_separator.rb +18 -0
- data/lib/csv/parser.rb +1290 -0
- data/lib/csv/row.rb +505 -136
- data/lib/csv/table.rb +791 -114
- data/lib/csv/version.rb +1 -1
- data/lib/csv/writer.rb +210 -0
- data/lib/csv.rb +2432 -1329
- metadata +66 -13
- data/news.md +0 -112
data/lib/csv/version.rb
CHANGED
data/lib/csv/writer.rb
ADDED
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "input_record_separator"
|
4
|
+
require_relative "row"
|
5
|
+
|
6
|
+
class CSV
|
7
|
+
# Note: Don't use this class directly. This is an internal class.
|
8
|
+
class Writer
|
9
|
+
#
|
10
|
+
# A CSV::Writer receives an output, prepares the header, format and output.
|
11
|
+
# It allows us to write new rows in the object and rewind it.
|
12
|
+
#
|
13
|
+
attr_reader :lineno
|
14
|
+
attr_reader :headers
|
15
|
+
|
16
|
+
def initialize(output, options)
|
17
|
+
@output = output
|
18
|
+
@options = options
|
19
|
+
@lineno = 0
|
20
|
+
@fields_converter = nil
|
21
|
+
prepare
|
22
|
+
if @options[:write_headers] and @headers
|
23
|
+
self << @headers
|
24
|
+
end
|
25
|
+
@fields_converter = @options[:fields_converter]
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Adds a new row
|
30
|
+
#
|
31
|
+
def <<(row)
|
32
|
+
case row
|
33
|
+
when Row
|
34
|
+
row = row.fields
|
35
|
+
when Hash
|
36
|
+
row = @headers.collect {|header| row[header]}
|
37
|
+
end
|
38
|
+
|
39
|
+
@headers ||= row if @use_headers
|
40
|
+
@lineno += 1
|
41
|
+
|
42
|
+
if @fields_converter
|
43
|
+
quoted_fields = [false] * row.size
|
44
|
+
row = @fields_converter.convert(row, nil, lineno, quoted_fields)
|
45
|
+
end
|
46
|
+
|
47
|
+
i = -1
|
48
|
+
converted_row = row.collect do |field|
|
49
|
+
i += 1
|
50
|
+
quote(field, i)
|
51
|
+
end
|
52
|
+
line = converted_row.join(@column_separator) + @row_separator
|
53
|
+
if @output_encoding
|
54
|
+
line = line.encode(@output_encoding)
|
55
|
+
end
|
56
|
+
@output << line
|
57
|
+
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Winds back to the beginning
|
63
|
+
#
|
64
|
+
def rewind
|
65
|
+
@lineno = 0
|
66
|
+
@headers = nil if @options[:headers].nil?
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def prepare
|
71
|
+
@encoding = @options[:encoding]
|
72
|
+
|
73
|
+
prepare_header
|
74
|
+
prepare_format
|
75
|
+
prepare_output
|
76
|
+
end
|
77
|
+
|
78
|
+
def prepare_header
|
79
|
+
headers = @options[:headers]
|
80
|
+
case headers
|
81
|
+
when Array
|
82
|
+
@headers = headers
|
83
|
+
@use_headers = true
|
84
|
+
when String
|
85
|
+
@headers = CSV.parse_line(headers,
|
86
|
+
col_sep: @options[:column_separator],
|
87
|
+
row_sep: @options[:row_separator],
|
88
|
+
quote_char: @options[:quote_character])
|
89
|
+
@use_headers = true
|
90
|
+
when true
|
91
|
+
@headers = nil
|
92
|
+
@use_headers = true
|
93
|
+
else
|
94
|
+
@headers = nil
|
95
|
+
@use_headers = false
|
96
|
+
end
|
97
|
+
return unless @headers
|
98
|
+
|
99
|
+
converter = @options[:header_fields_converter]
|
100
|
+
@headers = converter.convert(@headers, nil, 0, [])
|
101
|
+
@headers.each do |header|
|
102
|
+
header.freeze if header.is_a?(String)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def prepare_force_quotes_fields(force_quotes)
|
107
|
+
@force_quotes_fields = {}
|
108
|
+
force_quotes.each do |name_or_index|
|
109
|
+
case name_or_index
|
110
|
+
when Integer
|
111
|
+
index = name_or_index
|
112
|
+
@force_quotes_fields[index] = true
|
113
|
+
when String, Symbol
|
114
|
+
name = name_or_index.to_s
|
115
|
+
if @headers.nil?
|
116
|
+
message = ":headers is required when you use field name " +
|
117
|
+
"in :force_quotes: " +
|
118
|
+
"#{name_or_index.inspect}: #{force_quotes.inspect}"
|
119
|
+
raise ArgumentError, message
|
120
|
+
end
|
121
|
+
index = @headers.index(name)
|
122
|
+
next if index.nil?
|
123
|
+
@force_quotes_fields[index] = true
|
124
|
+
else
|
125
|
+
message = ":force_quotes element must be " +
|
126
|
+
"field index or field name: " +
|
127
|
+
"#{name_or_index.inspect}: #{force_quotes.inspect}"
|
128
|
+
raise ArgumentError, message
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def prepare_format
|
134
|
+
@column_separator = @options[:column_separator].to_s.encode(@encoding)
|
135
|
+
row_separator = @options[:row_separator]
|
136
|
+
if row_separator == :auto
|
137
|
+
@row_separator = InputRecordSeparator.value.encode(@encoding)
|
138
|
+
else
|
139
|
+
@row_separator = row_separator.to_s.encode(@encoding)
|
140
|
+
end
|
141
|
+
@quote_character = @options[:quote_character]
|
142
|
+
force_quotes = @options[:force_quotes]
|
143
|
+
if force_quotes.is_a?(Array)
|
144
|
+
prepare_force_quotes_fields(force_quotes)
|
145
|
+
@force_quotes = false
|
146
|
+
elsif force_quotes
|
147
|
+
@force_quotes_fields = nil
|
148
|
+
@force_quotes = true
|
149
|
+
else
|
150
|
+
@force_quotes_fields = nil
|
151
|
+
@force_quotes = false
|
152
|
+
end
|
153
|
+
unless @force_quotes
|
154
|
+
@quotable_pattern =
|
155
|
+
Regexp.new("[\r\n".encode(@encoding) +
|
156
|
+
Regexp.escape(@column_separator) +
|
157
|
+
Regexp.escape(@quote_character.encode(@encoding)) +
|
158
|
+
"]".encode(@encoding))
|
159
|
+
end
|
160
|
+
@quote_empty = @options.fetch(:quote_empty, true)
|
161
|
+
end
|
162
|
+
|
163
|
+
def prepare_output
|
164
|
+
@output_encoding = nil
|
165
|
+
return unless @output.is_a?(StringIO)
|
166
|
+
|
167
|
+
output_encoding = @output.internal_encoding || @output.external_encoding
|
168
|
+
if @encoding != output_encoding
|
169
|
+
if @options[:force_encoding]
|
170
|
+
@output_encoding = output_encoding
|
171
|
+
else
|
172
|
+
compatible_encoding = Encoding.compatible?(@encoding, output_encoding)
|
173
|
+
if compatible_encoding
|
174
|
+
@output.set_encoding(compatible_encoding)
|
175
|
+
@output.seek(0, IO::SEEK_END)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
def quote_field(field)
|
182
|
+
field = String(field)
|
183
|
+
encoded_quote_character = @quote_character.encode(field.encoding)
|
184
|
+
encoded_quote_character +
|
185
|
+
field.gsub(encoded_quote_character,
|
186
|
+
encoded_quote_character * 2) +
|
187
|
+
encoded_quote_character
|
188
|
+
end
|
189
|
+
|
190
|
+
def quote(field, i)
|
191
|
+
if @force_quotes
|
192
|
+
quote_field(field)
|
193
|
+
elsif @force_quotes_fields and @force_quotes_fields[i]
|
194
|
+
quote_field(field)
|
195
|
+
else
|
196
|
+
if field.nil? # represent +nil+ fields as empty unquoted fields
|
197
|
+
""
|
198
|
+
else
|
199
|
+
field = String(field) # Stringify fields
|
200
|
+
# represent empty fields as empty quoted fields
|
201
|
+
if (@quote_empty and field.empty?) or (field.valid_encoding? and @quotable_pattern.match?(field))
|
202
|
+
quote_field(field)
|
203
|
+
else
|
204
|
+
field # unquoted field
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|