diff options
author | kou <kou@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2018-12-23 07:00:35 +0000 |
---|---|---|
committer | kou <kou@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2018-12-23 07:00:35 +0000 |
commit | e5d634260e7927db284fd7d2d656899443f5c53e (patch) | |
tree | 31f579715ae8c73ee8094c258b634f1186a0946a /lib/csv | |
parent | c20a1946a6d7b260f1f0f3038b7af081174d6cd9 (diff) |
Import CSV 3.0.2
This includes performance improvement especially writing. Writing is
about 2 times faster.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@66507 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib/csv')
-rw-r--r-- | lib/csv/csv.gemspec | 18 | ||||
-rw-r--r-- | lib/csv/fields_converter.rb | 78 | ||||
-rw-r--r-- | lib/csv/match_p.rb | 20 | ||||
-rw-r--r-- | lib/csv/parser.rb | 713 | ||||
-rw-r--r-- | lib/csv/table.rb | 31 | ||||
-rw-r--r-- | lib/csv/version.rb | 2 | ||||
-rw-r--r-- | lib/csv/writer.rb | 144 |
7 files changed, 997 insertions, 9 deletions
diff --git a/lib/csv/csv.gemspec b/lib/csv/csv.gemspec index fae5caae19..0c9d265584 100644 --- a/lib/csv/csv.gemspec +++ b/lib/csv/csv.gemspec @@ -18,12 +18,26 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/ruby/csv" spec.license = "BSD-2-Clause" - spec.files = ["lib/csv.rb", "lib/csv/table.rb", "lib/csv/core_ext/string.rb", "lib/csv/core_ext/array.rb", "lib/csv/row.rb", "lib/csv/version.rb"] - spec.files += ["README.md", "LICENSE.txt", "news.md"] + spec.files = [ + "LICENSE.txt", + "NEWS.md", + "README.md", + "lib/csv.rb", + "lib/csv/core_ext/array.rb", + "lib/csv/core_ext/string.rb", + "lib/csv/fields_converter.rb", + "lib/csv/match_p.rb", + "lib/csv/parser.rb", + "lib/csv/row.rb", + "lib/csv/table.rb", + "lib/csv/version.rb", + "lib/csv/writer.rb", + ] spec.require_paths = ["lib"] spec.required_ruby_version = ">= 2.3.0" spec.add_development_dependency "bundler" spec.add_development_dependency "rake" spec.add_development_dependency "benchmark-ips" + spec.add_development_dependency "simplecov" end diff --git a/lib/csv/fields_converter.rb b/lib/csv/fields_converter.rb new file mode 100644 index 0000000000..c2fa5798ff --- /dev/null +++ b/lib/csv/fields_converter.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +class CSV + class FieldsConverter + include Enumerable + + def initialize(options={}) + @converters = [] + @nil_value = options[:nil_value] + @empty_value = options[:empty_value] + @empty_value_is_empty_string = (@empty_value == "") + @accept_nil = options[:accept_nil] + @builtin_converters = options[:builtin_converters] + @need_static_convert = need_static_convert? + end + + def add_converter(name=nil, &converter) + if name.nil? # custom converter + @converters << converter + else # named converter + combo = @builtin_converters[name] + case combo + when Array # combo converter + combo.each do |sub_name| + add_converter(sub_name) + end + else # individual named converter + @converters << combo + end + end + end + + def each(&block) + @converters.each(&block) + end + + def empty? + @converters.empty? + end + + def convert(fields, headers, lineno) + return fields unless need_convert? + + fields.collect.with_index do |field, index| + if field.nil? + field = @nil_value + elsif field.empty? + field = @empty_value unless @empty_value_is_empty_string + end + @converters.each do |converter| + break if field.nil? and @accept_nil + if converter.arity == 1 # straight field converter + field = converter[field] + else # FieldInfo converter + if headers + header = headers[index] + else + header = nil + end + field = converter[field, FieldInfo.new(index, lineno, header)] + end + break unless field.is_a?(String) # short-circuit pipeline for speed + end + field # final state of each field, converted or original + end + end + + private + def need_static_convert? + not (@nil_value.nil? and @empty_value_is_empty_string) + end + + def need_convert? + @need_static_convert or + (not @converters.empty?) + end + end +end diff --git a/lib/csv/match_p.rb b/lib/csv/match_p.rb new file mode 100644 index 0000000000..775559a3eb --- /dev/null +++ b/lib/csv/match_p.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# This provides String#match? and Regexp#match? for Ruby 2.3. +unless String.method_defined?(:match?) + class CSV + module MatchP + refine String do + def match?(pattern) + self =~ pattern + end + end + + refine Regexp do + def match?(string) + self =~ string + end + end + end + end +end diff --git a/lib/csv/parser.rb b/lib/csv/parser.rb new file mode 100644 index 0000000000..2682c27ea3 --- /dev/null +++ b/lib/csv/parser.rb @@ -0,0 +1,713 @@ +# frozen_string_literal: true + +require "strscan" + +require_relative "match_p" +require_relative "row" +require_relative "table" + +using CSV::MatchP if CSV.const_defined?(:MatchP) + +class CSV + class Parser + class InvalidEncoding < StandardError + end + + class Scanner < StringScanner + alias_method :scan_all, :scan + + def initialize(*args) + super + @keeps = [] + end + + def keep_start + @keeps.push(pos) + end + + def keep_end + start = @keeps.pop + string[start, pos - start] + end + + def keep_back + self.pos = @keeps.pop + end + + def keep_drop + @keeps.pop + end + end + + class InputsScanner + def initialize(inputs, encoding, chunk_size: 8192) + @inputs = inputs.dup + @encoding = encoding + @chunk_size = chunk_size + @last_scanner = @inputs.empty? + @keeps = [] + read_chunk + end + + def scan(pattern) + value = @scanner.scan(pattern) + return value if @last_scanner + + if value + read_chunk if @scanner.eos? + return value + else + nil + end + end + + def scan_all(pattern) + value = @scanner.scan(pattern) + return value if @last_scanner + + return nil if value.nil? + while @scanner.eos? and read_chunk and (sub_value = @scanner.scan(pattern)) + value << sub_value + end + value + end + + def eos? + @scanner.eos? + end + + def keep_start + @keeps.push([@scanner.pos, nil]) + end + + def keep_end + start, buffer = @keeps.pop + keep = @scanner.string[start, @scanner.pos - start] + if buffer + buffer << keep + keep = buffer + end + keep + end + + def keep_back + start, buffer = @keeps.pop + if buffer + string = @scanner.string + keep = string[start, string.size - start] + if keep and not keep.empty? + @inputs.unshift(StringIO.new(keep)) + @last_scanner = false + end + @scanner = StringScanner.new(buffer) + else + @scanner.pos = start + end + end + + def keep_drop + @keeps.pop + end + + def rest + @scanner.rest + end + + private + def read_chunk + return false if @last_scanner + + unless @keeps.empty? + keep = @keeps.last + keep_start = keep[0] + string = @scanner.string + keep_data = string[keep_start, @scanner.pos - keep_start] + if keep_data + keep_buffer = keep[1] + if keep_buffer + keep_buffer << keep_data + else + keep[1] = keep_data.dup + end + end + keep[0] = 0 + end + + input = @inputs.first + case input + when StringIO + string = input.string + raise InvalidEncoding unless string.valid_encoding? + @scanner = StringScanner.new(string) + @inputs.shift + @last_scanner = @inputs.empty? + true + else + chunk = input.gets(nil, @chunk_size) + if chunk + raise InvalidEncoding unless chunk.valid_encoding? + @scanner = StringScanner.new(chunk) + if input.respond_to?(:eof?) and input.eof? + @inputs.shift + @last_scanner = @inputs.empty? + end + true + else + @scanner = StringScanner.new("".encode(@encoding)) + @inputs.shift + @last_scanner = @inputs.empty? + if @last_scanner + false + else + read_chunk + end + end + end + end + end + + def initialize(input, options) + @input = input + @options = options + @samples = [] + + prepare + end + + def column_separator + @column_separator + end + + def row_separator + @row_separator + end + + def quote_character + @quote_character + end + + def field_size_limit + @field_size_limit + end + + def skip_lines + @skip_lines + end + + def unconverted_fields? + @unconverted_fields + end + + def headers + @headers + end + + def header_row? + @use_headers and @headers.nil? + end + + def return_headers? + @return_headers + end + + def skip_blanks? + @skip_blanks + end + + def liberal_parsing? + @liberal_parsing + end + + def lineno + @lineno + end + + def line + last_line + end + + def parse(&block) + return to_enum(__method__) unless block_given? + + if @return_headers and @headers + headers = Row.new(@headers, @raw_headers, true) + if @unconverted_fields + headers = add_unconverted_fields(headers, []) + end + yield headers + end + + row = [] + begin + @scanner = build_scanner + skip_needless_lines + start_row + while true + @quoted_column_value = false + @unquoted_column_value = false + value = parse_column_value + if value and @field_size_limit and value.size >= @field_size_limit + raise MalformedCSVError.new("Field size exceeded", @lineno + 1) + end + if parse_column_end + row << value + elsif parse_row_end + if row.empty? and value.nil? + emit_row([], &block) unless @skip_blanks + else + row << value + emit_row(row, &block) + row = [] + end + skip_needless_lines + start_row + elsif @scanner.eos? + return if row.empty? and value.nil? + row << value + emit_row(row, &block) + return + else + if @quoted_column_value + message = "Do not allow except col_sep_split_separator " + + "after quoted fields" + raise MalformedCSVError.new(message, @lineno + 1) + elsif @unquoted_column_value and @scanner.scan(@cr_or_lf) + message = "Unquoted fields do not allow \\r or \\n" + raise MalformedCSVError.new(message, @lineno + 1) + elsif @scanner.rest.start_with?(@quote_character) + message = "Illegal quoting" + raise MalformedCSVError.new(message, @lineno + 1) + else + raise MalformedCSVError.new("TODO: Meaningful message", + @lineno + 1) + end + end + end + rescue InvalidEncoding + message = "Invalid byte sequence in #{@encoding}" + raise MalformedCSVError.new(message, @lineno + 1) + end + end + + private + def prepare + prepare_variable + prepare_regexp + prepare_line + prepare_header + prepare_parser + end + + def prepare_variable + @encoding = @options[:encoding] + @liberal_parsing = @options[:liberal_parsing] + @unconverted_fields = @options[:unconverted_fields] + @field_size_limit = @options[:field_size_limit] + @skip_blanks = @options[:skip_blanks] + @fields_converter = @options[:fields_converter] + @header_fields_converter = @options[:header_fields_converter] + end + + def prepare_regexp + @column_separator = @options[:column_separator].to_s.encode(@encoding) + @row_separator = + resolve_row_separator(@options[:row_separator]).encode(@encoding) + @quote_character = @options[:quote_character].to_s.encode(@encoding) + if @quote_character.length != 1 + raise ArgumentError, ":quote_char has to be a single character String" + end + + escaped_column_separator = Regexp.escape(@column_separator) + escaped_row_separator = Regexp.escape(@row_separator) + escaped_quote_character = Regexp.escape(@quote_character) + + skip_lines = @options[:skip_lines] + case skip_lines + when String + @skip_lines = skip_lines.encode(@encoding) + when Regexp, nil + @skip_lines = skip_lines + else + unless skip_lines.respond_to?(:match) + message = + ":skip_lines has to respond to \#match: #{skip_lines.inspect}" + raise ArgumentError, message + end + @skip_lines = skip_lines + end + + @column_end = Regexp.new(escaped_column_separator) + if @column_separator.size > 1 + @column_ends = @column_separator.each_char.collect do |char| + Regexp.new(Regexp.escape(char)) + end + else + @column_ends = nil + end + @row_end = Regexp.new(escaped_row_separator) + if @row_separator.size > 1 + @row_ends = @row_separator.each_char.collect do |char| + Regexp.new(Regexp.escape(char)) + end + else + @row_ends = nil + end + @quotes = Regexp.new(escaped_quote_character + + "+".encode(@encoding)) + @quoted_value = Regexp.new("[^".encode(@encoding) + + escaped_quote_character + + "]+".encode(@encoding)) + if @liberal_parsing + @unquoted_value = Regexp.new("[^".encode(@encoding) + + escaped_column_separator + + "\r\n]+".encode(@encoding)) + else + @unquoted_value = Regexp.new("[^".encode(@encoding) + + escaped_quote_character + + escaped_column_separator + + "\r\n]+".encode(@encoding)) + end + @cr_or_lf = Regexp.new("[\r\n]".encode(@encoding)) + @not_line_end = Regexp.new("[^\r\n]+".encode(@encoding)) + end + + def resolve_row_separator(separator) + if separator == :auto + cr = "\r".encode(@encoding) + lf = "\n".encode(@encoding) + if @input.is_a?(StringIO) + separator = detect_row_separator(@input.string, cr, lf) + elsif @input.respond_to?(:gets) + if @input.is_a?(File) + chunk_size = 32 * 1024 + else + chunk_size = 1024 + end + begin + while separator == :auto + # + # if we run out of data, it's probably a single line + # (ensure will set default value) + # + break unless sample = @input.gets(nil, chunk_size) + + # extend sample if we're unsure of the line ending + if sample.end_with?(cr) + sample << (@input.gets(nil, 1) || "") + end + + @samples << sample + + separator = detect_row_separator(sample, cr, lf) + end + rescue IOError + # do nothing: ensure will set default + end + end + separator = $INPUT_RECORD_SEPARATOR if separator == :auto + end + separator.to_s.encode(@encoding) + end + + def detect_row_separator(sample, cr, lf) + lf_index = sample.index(lf) + if lf_index + cr_index = sample[0, lf_index].index(cr) + else + cr_index = sample.index(cr) + end + if cr_index and lf_index + if cr_index + 1 == lf_index + cr + lf + elsif cr_index < lf_index + cr + else + lf + end + elsif cr_index + cr + elsif lf_index + lf + else + :auto + end + end + + def prepare_line + @lineno = 0 + @last_line = nil + @scanner = nil + end + + def last_line + if @scanner + @last_line ||= @scanner.keep_end + else + @last_line + end + end + + def prepare_header + @return_headers = @options[:return_headers] + + headers = @options[:headers] + case headers + when Array + @raw_headers = headers + @use_headers = true + when String + @raw_headers = parse_headers(headers) + @use_headers = true + when nil, false + @raw_headers = nil + @use_headers = false + else + @raw_headers = nil + @use_headers = true + end + if @raw_headers + @headers = adjust_headers(@raw_headers) + else + @headers = nil + end + end + + def parse_headers(row) + CSV.parse_line(row, + col_sep: @column_separator, + row_sep: @row_separator, + quote_char: @quote_character) + end + + def adjust_headers(headers) + adjusted_headers = @header_fields_converter.convert(headers, nil, @lineno) + adjusted_headers.each {|h| h.freeze if h.is_a? String} + adjusted_headers + end + + def prepare_parser + @may_quoted = may_quoted? + end + + def may_quoted? + if @input.is_a?(StringIO) + sample = @input.string + else + return false if @samples.empty? + sample = @samples.first + end + sample[0, 128].index(@quote_character) + end + + SCANNER_TEST = (ENV["CSV_PARSER_SCANNER_TEST"] == "yes") + if SCANNER_TEST + class UnoptimizedStringIO + def initialize(string) + @io = StringIO.new(string) + end + + def gets(*args) + @io.gets(*args) + end + + def eof? + @io.eof? + end + end + + def build_scanner + inputs = @samples.collect do |sample| + UnoptimizedStringIO.new(sample) + end + if @input.is_a?(StringIO) + inputs << UnoptimizedStringIO.new(@input.string) + else + inputs << @input + end + InputsScanner.new(inputs, @encoding, chunk_size: 1) + end + else + def build_scanner + string = nil + if @samples.empty? and @input.is_a?(StringIO) + string = @input.string + elsif @samples.size == 1 and @input.respond_to?(:eof?) and @input.eof? + string = @samples[0] + end + if string + unless string.valid_encoding? + message = "Invalid byte sequence in #{@encoding}" + raise MalformedCSVError.new(message, @lineno + 1) + end + Scanner.new(string) + else + inputs = @samples.collect do |sample| + StringIO.new(sample) + end + inputs << @input + InputsScanner.new(inputs, @encoding) + end + end + end + + def skip_needless_lines + return unless @skip_lines + + while true + @scanner.keep_start + line = @scanner.scan_all(@not_line_end) || "".encode(@encoding) + line << @row_separator if parse_row_end + if skip_line?(line) + @scanner.keep_drop + else + @scanner.keep_back + return + end + end + end + + def skip_line?(line) + case @skip_lines + when String + line.include?(@skip_lines) + when Regexp + @skip_lines.match?(line) + else + @skip_lines.match(line) + end + end + + def parse_column_value + if @liberal_parsing + quoted_value = parse_quoted_column_value + if quoted_value + unquoted_value = parse_unquoted_column_value + if unquoted_value + @quote_character + quoted_value + @quote_character + unquoted_value + else + quoted_value + end + else + parse_unquoted_column_value + end + elsif @may_quoted + parse_quoted_column_value || + parse_unquoted_column_value + else + parse_unquoted_column_value || + parse_quoted_column_value + end + end + + def parse_unquoted_column_value + value = @scanner.scan_all(@unquoted_value) + @unquoted_column_value = true if value + value + end + + def parse_quoted_column_value + quotes = @scanner.scan_all(@quotes) + return nil unless quotes + + @quoted_column_value = true + n_quotes = quotes.size + if (n_quotes % 2).zero? + quotes[0, (n_quotes - 2) / 2] + else + value = quotes[0, (n_quotes - 1) / 2] + while true + quoted_value = @scanner.scan_all(@quoted_value) + value << quoted_value if quoted_value + quotes = @scanner.scan_all(@quotes) + unless quotes + message = "Unclosed quoted field" + raise MalformedCSVError.new(message, @lineno + 1) + end + n_quotes = quotes.size + if n_quotes == 1 + break + elsif (n_quotes % 2) == 1 + value << quotes[0, (n_quotes - 1) / 2] + break + else + value << quotes[0, n_quotes / 2] + end + end + value + end + end + + def parse_column_end + return true if @scanner.scan(@column_end) + return false unless @column_ends + + @scanner.keep_start + if @column_ends.all? {|column_end| @scanner.scan(column_end)} + @scanner.keep_drop + true + else + @scanner.keep_back + false + end + end + + def parse_row_end + return true if @scanner.scan(@row_end) + return false unless @row_ends + @scanner.keep_start + if @row_ends.all? {|row_end| @scanner.scan(row_end)} + @scanner.keep_drop + true + else + @scanner.keep_back + false + end + end + + def start_row + if @last_line + @last_line = nil + else + @scanner.keep_drop + end + @scanner.keep_start + end + + def emit_row(row, &block) + @lineno += 1 + + raw_row = row + if @use_headers + if @headers.nil? + @headers = adjust_headers(row) + return unless @return_headers + row = Row.new(@headers, row, true) + else + row = Row.new(@headers, + @fields_converter.convert(raw_row, @headers, @lineno)) + end + else + # convert fields, if needed... + row = @fields_converter.convert(raw_row, nil, @lineno) + end + + # inject unconverted fields and accessor, if requested... + if @unconverted_fields and not row.respond_to?(:unconverted_fields) + add_unconverted_fields(row, raw_row) + end + + yield(row) + end + + # This method injects an instance variable <tt>unconverted_fields</tt> into + # +row+ and an accessor method for +row+ called unconverted_fields(). The + # variable is set to the contents of +fields+. + def add_unconverted_fields(row, fields) + class << row + attr_reader :unconverted_fields + end + row.instance_variable_set(:@unconverted_fields, fields) + row + end + end +end diff --git a/lib/csv/table.rb b/lib/csv/table.rb index 17a7c542e4..b13d1ada10 100644 --- a/lib/csv/table.rb +++ b/lib/csv/table.rb @@ -16,6 +16,11 @@ class CSV # Construct a new CSV::Table from +array_of_rows+, which are expected # to be CSV::Row objects. All rows are assumed to have the same headers. # + # The optional +headers+ parameter can be set to Array of headers. + # If headers aren't set, headers are fetched from CSV::Row objects. + # Otherwise, headers() method will return headers being set in + # headers arugument. + # # A CSV::Table object supports the following Array methods through # delegation: # @@ -23,8 +28,17 @@ class CSV # * length() # * size() # - def initialize(array_of_rows) + def initialize(array_of_rows, headers: nil) @table = array_of_rows + @headers = headers + unless @headers + if @table.empty? + @headers = [] + else + @headers = @table.first.headers + end + end + @mode = :col_or_row end @@ -122,11 +136,7 @@ class CSV # other rows). An empty Array is returned for empty tables. # def headers - if @table.empty? - Array.new - else - @table.first.headers - end + @headers.dup end # @@ -171,6 +181,10 @@ class CSV @table[index_or_header] = value end else # set column + unless index_or_header.is_a? Integer + index = @headers.index(index_or_header) || @headers.size + @headers[index] = index_or_header + end if value.is_a? Array # multiple values @table.each_with_index do |row, i| if row.header_row? @@ -258,6 +272,11 @@ class CSV (@mode == :col_or_row and index_or_header.is_a? Integer) @table.delete_at(index_or_header) else # by header + if index_or_header.is_a? Integer + @headers.delete_at(index_or_header) + else + @headers.delete(index_or_header) + end @table.map { |row| row.delete(index_or_header).last } end end diff --git a/lib/csv/version.rb b/lib/csv/version.rb index d62a093418..d6b59b3097 100644 --- a/lib/csv/version.rb +++ b/lib/csv/version.rb @@ -2,5 +2,5 @@ class CSV # The version of the installed library. - VERSION = "3.0.1" + VERSION = "3.0.2" end 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 |