1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
# frozen_string_literal: true
require_relative "match_p"
require_relative "row"
using CSV::MatchP if CSV.const_defined?(:MatchP)
class CSV
class Writer
attr_reader :lineno
attr_reader :headers
def initialize(output, options)
@output = output
@options = options
@lineno = 0
@fields_converter = nil
prepare
if @options[:write_headers] and @headers
self << @headers
end
@fields_converter = @options[:fields_converter]
end
def <<(row)
case row
when Row
row = row.fields
when Hash
row = @headers.collect {|header| row[header]}
end
@headers ||= row if @use_headers
@lineno += 1
row = @fields_converter.convert(row, nil, lineno) if @fields_converter
converted_row = row.collect do |field|
quote(field)
end
line = converted_row.join(@column_separator) + @row_separator
if @output_encoding
line = line.encode(@output_encoding)
end
@output << line
self
end
def rewind
@lineno = 0
@headers = nil if @options[:headers].nil?
end
private
def prepare
@encoding = @options[:encoding]
prepare_header
prepare_format
prepare_output
end
def prepare_header
headers = @options[:headers]
case headers
when Array
@headers = headers
@use_headers = true
when String
@headers = CSV.parse_line(headers,
col_sep: @options[:column_separator],
row_sep: @options[:row_separator],
quote_char: @options[:quote_character])
@use_headers = true
when true
@headers = nil
@use_headers = true
else
@headers = nil
@use_headers = false
end
return unless @headers
converter = @options[:header_fields_converter]
@headers = converter.convert(@headers, nil, 0)
@headers.each do |header|
header.freeze if header.is_a?(String)
end
end
def prepare_format
@column_separator = @options[:column_separator].to_s.encode(@encoding)
row_separator = @options[:row_separator]
if row_separator == :auto
@row_separator = $INPUT_RECORD_SEPARATOR.encode(@encoding)
else
@row_separator = row_separator.to_s.encode(@encoding)
end
@quote_character = @options[:quote_character]
@force_quotes = @options[:force_quotes]
unless @force_quotes
@quotable_pattern =
Regexp.new("[\r\n".encode(@encoding) +
Regexp.escape(@column_separator) +
Regexp.escape(@quote_character.encode(@encoding)) +
"]".encode(@encoding))
end
@quote_empty = @options.fetch(:quote_empty, true)
end
def prepare_output
@output_encoding = nil
return unless @output.is_a?(StringIO)
output_encoding = @output.internal_encoding || @output.external_encoding
if @encoding != output_encoding
if @options[:force_encoding]
@output_encoding = output_encoding
else
compatible_encoding = Encoding.compatible?(@encoding, output_encoding)
if compatible_encoding
@output.set_encoding(compatible_encoding)
@output.seek(0, IO::SEEK_END)
end
end
end
end
def quote_field(field)
field = String(field)
encoded_quote_character = @quote_character.encode(field.encoding)
encoded_quote_character +
field.gsub(encoded_quote_character,
encoded_quote_character * 2) +
encoded_quote_character
end
def quote(field)
if @force_quotes
quote_field(field)
else
if field.nil? # represent +nil+ fields as empty unquoted fields
""
else
field = String(field) # Stringify fields
# represent empty fields as empty quoted fields
if (@quote_empty and field.empty?) or @quotable_pattern.match?(field)
quote_field(field)
else
field # unquoted field
end
end
end
end
end
end
|