diff options
Diffstat (limited to 'lib/csv/writer.rb')
-rw-r--r-- | lib/csv/writer.rb | 144 |
1 files changed, 144 insertions, 0 deletions
diff --git a/lib/csv/writer.rb b/lib/csv/writer.rb new file mode 100644 index 0000000000..2f2ab095d7 --- /dev/null +++ b/lib/csv/writer.rb @@ -0,0 +1,144 @@ +# 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 + prepare + if @options[:write_headers] and @headers + self << @headers + end + 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 + + line = row.collect(&@quote).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] + quote = lambda do |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 + if @options[:force_quotes] + @quote = quote + else + quotable_pattern = + Regexp.new("[\r\n".encode(@encoding) + + Regexp.escape(@column_separator) + + Regexp.escape(quote_character.encode(@encoding)) + + "]".encode(@encoding)) + @quote = lambda do |field| + if field.nil? # represent +nil+ fields as empty unquoted fields + "" + else + field = String(field) # Stringify fields + # represent empty fields as empty quoted fields + if field.empty? or quotable_pattern.match?(field) + quote.call(field) + else + field # unquoted field + end + end + end + end + 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 + end +end |