2018-12-23 02:00:35 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2021-09-11 18:34:15 -04:00
|
|
|
require_relative "input_record_separator"
|
2018-12-23 02:00:35 -05:00
|
|
|
require_relative "match_p"
|
|
|
|
require_relative "row"
|
|
|
|
|
|
|
|
using CSV::MatchP if CSV.const_defined?(:MatchP)
|
|
|
|
|
|
|
|
class CSV
|
2019-10-12 01:03:21 -04:00
|
|
|
# Note: Don't use this class directly. This is an internal class.
|
2018-12-23 02:00:35 -05:00
|
|
|
class Writer
|
2019-10-12 01:03:21 -04:00
|
|
|
#
|
|
|
|
# A CSV::Writer receives an output, prepares the header, format and output.
|
|
|
|
# It allows us to write new rows in the object and rewind it.
|
|
|
|
#
|
2018-12-23 02:00:35 -05:00
|
|
|
attr_reader :lineno
|
|
|
|
attr_reader :headers
|
|
|
|
|
|
|
|
def initialize(output, options)
|
|
|
|
@output = output
|
|
|
|
@options = options
|
|
|
|
@lineno = 0
|
2019-07-08 00:03:50 -04:00
|
|
|
@fields_converter = nil
|
2018-12-23 02:00:35 -05:00
|
|
|
prepare
|
|
|
|
if @options[:write_headers] and @headers
|
|
|
|
self << @headers
|
|
|
|
end
|
2019-04-14 17:01:51 -04:00
|
|
|
@fields_converter = @options[:fields_converter]
|
2018-12-23 02:00:35 -05:00
|
|
|
end
|
|
|
|
|
2019-10-12 01:03:21 -04:00
|
|
|
#
|
|
|
|
# Adds a new row
|
|
|
|
#
|
2018-12-23 02:00:35 -05:00
|
|
|
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
|
|
|
|
|
2019-04-14 17:01:51 -04:00
|
|
|
row = @fields_converter.convert(row, nil, lineno) if @fields_converter
|
|
|
|
|
2020-07-15 17:10:38 -04:00
|
|
|
i = -1
|
2019-01-25 01:49:59 -05:00
|
|
|
converted_row = row.collect do |field|
|
2020-07-15 17:10:38 -04:00
|
|
|
i += 1
|
|
|
|
quote(field, i)
|
2019-01-25 01:49:59 -05:00
|
|
|
end
|
|
|
|
line = converted_row.join(@column_separator) + @row_separator
|
2018-12-23 02:00:35 -05:00
|
|
|
if @output_encoding
|
|
|
|
line = line.encode(@output_encoding)
|
|
|
|
end
|
|
|
|
@output << line
|
|
|
|
|
|
|
|
self
|
|
|
|
end
|
|
|
|
|
2019-10-12 01:03:21 -04:00
|
|
|
#
|
|
|
|
# Winds back to the beginning
|
|
|
|
#
|
2018-12-23 02:00:35 -05:00
|
|
|
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
|
|
|
|
|
2020-07-15 17:10:38 -04:00
|
|
|
def prepare_force_quotes_fields(force_quotes)
|
|
|
|
@force_quotes_fields = {}
|
|
|
|
force_quotes.each do |name_or_index|
|
|
|
|
case name_or_index
|
|
|
|
when Integer
|
|
|
|
index = name_or_index
|
|
|
|
@force_quotes_fields[index] = true
|
|
|
|
when String, Symbol
|
|
|
|
name = name_or_index.to_s
|
|
|
|
if @headers.nil?
|
|
|
|
message = ":headers is required when you use field name " +
|
|
|
|
"in :force_quotes: " +
|
|
|
|
"#{name_or_index.inspect}: #{force_quotes.inspect}"
|
|
|
|
raise ArgumentError, message
|
|
|
|
end
|
|
|
|
index = @headers.index(name)
|
|
|
|
next if index.nil?
|
|
|
|
@force_quotes_fields[index] = true
|
|
|
|
else
|
|
|
|
message = ":force_quotes element must be " +
|
|
|
|
"field index or field name: " +
|
|
|
|
"#{name_or_index.inspect}: #{force_quotes.inspect}"
|
|
|
|
raise ArgumentError, message
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-12-23 02:00:35 -05:00
|
|
|
def prepare_format
|
|
|
|
@column_separator = @options[:column_separator].to_s.encode(@encoding)
|
|
|
|
row_separator = @options[:row_separator]
|
|
|
|
if row_separator == :auto
|
2021-09-11 18:34:15 -04:00
|
|
|
@row_separator = InputRecordSeparator.value.encode(@encoding)
|
2018-12-23 02:00:35 -05:00
|
|
|
else
|
|
|
|
@row_separator = row_separator.to_s.encode(@encoding)
|
|
|
|
end
|
2019-01-25 01:49:59 -05:00
|
|
|
@quote_character = @options[:quote_character]
|
2020-07-15 17:10:38 -04:00
|
|
|
force_quotes = @options[:force_quotes]
|
|
|
|
if force_quotes.is_a?(Array)
|
|
|
|
prepare_force_quotes_fields(force_quotes)
|
|
|
|
@force_quotes = false
|
|
|
|
elsif force_quotes
|
|
|
|
@force_quotes_fields = nil
|
|
|
|
@force_quotes = true
|
|
|
|
else
|
|
|
|
@force_quotes_fields = nil
|
|
|
|
@force_quotes = false
|
|
|
|
end
|
2019-01-25 01:49:59 -05:00
|
|
|
unless @force_quotes
|
|
|
|
@quotable_pattern =
|
2018-12-23 02:00:35 -05:00
|
|
|
Regexp.new("[\r\n".encode(@encoding) +
|
|
|
|
Regexp.escape(@column_separator) +
|
2019-01-25 01:49:59 -05:00
|
|
|
Regexp.escape(@quote_character.encode(@encoding)) +
|
2018-12-23 02:00:35 -05:00
|
|
|
"]".encode(@encoding))
|
|
|
|
end
|
2019-01-25 01:49:59 -05:00
|
|
|
@quote_empty = @options.fetch(:quote_empty, true)
|
2018-12-23 02:00:35 -05:00
|
|
|
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
|
2019-01-25 01:49:59 -05:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2020-07-15 17:10:38 -04:00
|
|
|
def quote(field, i)
|
2019-01-25 01:49:59 -05:00
|
|
|
if @force_quotes
|
|
|
|
quote_field(field)
|
2020-07-15 17:10:38 -04:00
|
|
|
elsif @force_quotes_fields and @force_quotes_fields[i]
|
|
|
|
quote_field(field)
|
2019-01-25 01:49:59 -05:00
|
|
|
else
|
|
|
|
if field.nil? # represent +nil+ fields as empty unquoted fields
|
|
|
|
""
|
|
|
|
else
|
|
|
|
field = String(field) # Stringify fields
|
|
|
|
# represent empty fields as empty quoted fields
|
2020-06-03 23:08:05 -04:00
|
|
|
if (@quote_empty and field.empty?) or (field.valid_encoding? and @quotable_pattern.match?(field))
|
2019-01-25 01:49:59 -05:00
|
|
|
quote_field(field)
|
|
|
|
else
|
|
|
|
field # unquoted field
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2018-12-23 02:00:35 -05:00
|
|
|
end
|
|
|
|
end
|