diff options
Diffstat (limited to 'lib/csv')
| -rw-r--r-- | lib/csv/core_ext/array.rb | 9 | ||||
| -rw-r--r-- | lib/csv/core_ext/string.rb | 9 | ||||
| -rw-r--r-- | lib/csv/csv.gemspec | 44 | ||||
| -rw-r--r-- | lib/csv/delete_suffix.rb | 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 | 1092 | ||||
| -rw-r--r-- | lib/csv/row.rb | 388 | ||||
| -rw-r--r-- | lib/csv/table.rb | 402 | ||||
| -rw-r--r-- | lib/csv/version.rb | 6 | ||||
| -rw-r--r-- | lib/csv/writer.rb | 157 |
11 files changed, 0 insertions, 2223 deletions
diff --git a/lib/csv/core_ext/array.rb b/lib/csv/core_ext/array.rb deleted file mode 100644 index 94df7d5c35..0000000000 --- a/lib/csv/core_ext/array.rb +++ /dev/null @@ -1,9 +0,0 @@ -class Array # :nodoc: - # Equivalent to CSV::generate_line(self, options) - # - # ["CSV", "data"].to_csv - # #=> "CSV,data\n" - def to_csv(**options) - CSV.generate_line(self, options) - end -end diff --git a/lib/csv/core_ext/string.rb b/lib/csv/core_ext/string.rb deleted file mode 100644 index 8f2070f3bd..0000000000 --- a/lib/csv/core_ext/string.rb +++ /dev/null @@ -1,9 +0,0 @@ -class String # :nodoc: - # Equivalent to CSV::parse_line(self, options) - # - # "CSV,data".parse_csv - # #=> ["CSV", "data"] - def parse_csv(**options) - CSV.parse_line(self, options) - end -end diff --git a/lib/csv/csv.gemspec b/lib/csv/csv.gemspec deleted file mode 100644 index 98110bc13c..0000000000 --- a/lib/csv/csv.gemspec +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -begin - require_relative "lib/csv/version" -rescue LoadError - # for Ruby core repository - require_relative "version" -end - -Gem::Specification.new do |spec| - spec.name = "csv" - spec.version = CSV::VERSION - spec.authors = ["James Edward Gray II", "Kouhei Sutou"] - spec.email = [nil, "kou@cozmixng.org"] - - spec.summary = "CSV Reading and Writing" - spec.description = "The CSV library provides a complete interface to CSV files and data. It offers tools to enable you to read and write to and from Strings or IO objects, as needed." - spec.homepage = "https://github.com/ruby/csv" - spec.license = "BSD-2-Clause" - - 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/delete_suffix.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_driver" - spec.add_development_dependency "simplecov" -end diff --git a/lib/csv/delete_suffix.rb b/lib/csv/delete_suffix.rb deleted file mode 100644 index d457718997..0000000000 --- a/lib/csv/delete_suffix.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -# This provides String#delete_suffix? for Ruby 2.4. -unless String.method_defined?(:delete_suffix) - class CSV - module DeleteSuffix - refine String do - def delete_suffix(suffix) - if end_with?(suffix) - self[0...-suffix.size] - else - self - end - end - end - end - end -end diff --git a/lib/csv/fields_converter.rb b/lib/csv/fields_converter.rb deleted file mode 100644 index c2fa5798ff..0000000000 --- a/lib/csv/fields_converter.rb +++ /dev/null @@ -1,78 +0,0 @@ -# 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 deleted file mode 100644 index 775559a3eb..0000000000 --- a/lib/csv/match_p.rb +++ /dev/null @@ -1,20 +0,0 @@ -# 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 deleted file mode 100644 index 2ef2a28ff3..0000000000 --- a/lib/csv/parser.rb +++ /dev/null @@ -1,1092 +0,0 @@ -# frozen_string_literal: true - -require "strscan" - -require_relative "delete_suffix" -require_relative "match_p" -require_relative "row" -require_relative "table" - -using CSV::DeleteSuffix if CSV.const_defined?(:DeleteSuffix) -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 each_line(row_separator) - position = pos - rest.each_line(row_separator) do |line| - position += line.bytesize - self.pos = position - yield(line) - end - 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 each_line(row_separator) - buffer = nil - input = @scanner.rest - position = @scanner.pos - offset = 0 - n_row_separator_chars = row_separator.size - while true - input.each_line(row_separator) do |line| - @scanner.pos += line.bytesize - if buffer - if n_row_separator_chars == 2 and - buffer.end_with?(row_separator[0]) and - line.start_with?(row_separator[1]) - buffer << line[0] - line = line[1..-1] - position += buffer.bytesize + offset - @scanner.pos = position - offset = 0 - yield(buffer) - buffer = nil - next if line.empty? - else - buffer << line - line = buffer - buffer = nil - end - end - if line.end_with?(row_separator) - position += line.bytesize + offset - @scanner.pos = position - offset = 0 - yield(line) - else - buffer = line - end - end - break unless read_chunk - input = @scanner.rest - position = @scanner.pos - offset = -buffer.bytesize if buffer - end - yield(buffer) if buffer - 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.byteslice(start, string.bytesize - 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 - read_chunk if @scanner.eos? - 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.byteslice(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 and @raw_headers - headers = Row.new(@headers, @raw_headers, true) - if @unconverted_fields - headers = add_unconverted_fields(headers, []) - end - yield headers - end - - begin - @scanner ||= build_scanner - if quote_character.nil? - parse_no_quote(&block) - elsif @need_robust_parsing - parse_quotable_robust(&block) - else - parse_quotable_loose(&block) - end - rescue InvalidEncoding - if @scanner - ignore_broken_line - lineno = @lineno - else - lineno = @lineno + 1 - end - message = "Invalid byte sequence in #{@encoding}" - raise MalformedCSVError.new(message, lineno) - end - end - - def use_headers? - @use_headers - end - - private - def prepare - prepare_variable - prepare_quote_character - prepare_backslash - prepare_skip_lines - prepare_strip - prepare_separators - prepare_quoted - prepare_unquoted - prepare_line - prepare_header - prepare_parser - end - - def prepare_variable - @need_robust_parsing = false - @encoding = @options[:encoding] - liberal_parsing = @options[:liberal_parsing] - if liberal_parsing - @liberal_parsing = true - if liberal_parsing.is_a?(Hash) - @double_quote_outside_quote = - liberal_parsing[:double_quote_outside_quote] - @backslash_quote = liberal_parsing[:backslash_quote] - else - @double_quote_outside_quote = false - @backslash_quote = false - end - @need_robust_parsing = true - else - @liberal_parsing = false - @backslash_quote = false - end - @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_quote_character - @quote_character = @options[:quote_character] - if @quote_character.nil? - @escaped_quote_character = nil - @escaped_quote = nil - else - @quote_character = @quote_character.to_s.encode(@encoding) - if @quote_character.length != 1 - message = ":quote_char has to be nil or a single character String" - raise ArgumentError, message - end - @double_quote_character = @quote_character * 2 - @escaped_quote_character = Regexp.escape(@quote_character) - @escaped_quote = Regexp.new(@escaped_quote_character) - end - end - - def prepare_backslash - return unless @backslash_quote - - @backslash_character = "\\".encode(@encoding) - - @escaped_backslash_character = Regexp.escape(@backslash_character) - @escaped_backslash = Regexp.new(@escaped_backslash_character) - if @quote_character.nil? - @backslash_quote_character = nil - else - @backslash_quote_character = - @backslash_character + @escaped_quote_character - end - end - - def prepare_skip_lines - 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 - end - - def prepare_strip - @strip = @options[:strip] - @escaped_strip = nil - @strip_value = nil - if @strip.is_a?(String) - case @strip.length - when 0 - raise ArgumentError, ":strip must not be an empty String" - when 1 - # ok - else - raise ArgumentError, ":strip doesn't support 2 or more characters yet" - end - @strip = @strip.encode(@encoding) - @escaped_strip = Regexp.escape(@strip) - if @quote_character - @strip_value = Regexp.new(@escaped_strip + - "+".encode(@encoding)) - end - @need_robust_parsing = true - elsif @strip - strip_values = " \t\f\v" - @escaped_strip = strip_values.encode(@encoding) - if @quote_character - @strip_value = Regexp.new("[#{strip_values}]+".encode(@encoding)) - end - @need_robust_parsing = true - end - end - - begin - StringScanner.new("x").scan("x") - rescue TypeError - @@string_scanner_scan_accept_string = false - else - @@string_scanner_scan_accept_string = true - end - - def prepare_separators - @column_separator = @options[:column_separator].to_s.encode(@encoding) - @row_separator = - resolve_row_separator(@options[:row_separator]).encode(@encoding) - - @escaped_column_separator = Regexp.escape(@column_separator) - @escaped_first_column_separator = Regexp.escape(@column_separator[0]) - if @column_separator.size > 1 - @column_end = Regexp.new(@escaped_column_separator) - @column_ends = @column_separator.each_char.collect do |char| - Regexp.new(Regexp.escape(char)) - end - @first_column_separators = Regexp.new(@escaped_first_column_separator + - "+".encode(@encoding)) - else - if @@string_scanner_scan_accept_string - @column_end = @column_separator - else - @column_end = Regexp.new(@escaped_column_separator) - end - @column_ends = nil - @first_column_separators = nil - end - - escaped_row_separator = Regexp.escape(@row_separator) - @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 - - @cr = "\r".encode(@encoding) - @lf = "\n".encode(@encoding) - @cr_or_lf = Regexp.new("[\r\n]".encode(@encoding)) - @not_line_end = Regexp.new("[^\r\n]+".encode(@encoding)) - end - - def prepare_quoted - if @quote_character - @quotes = Regexp.new(@escaped_quote_character + - "+".encode(@encoding)) - no_quoted_values = @escaped_quote_character.dup - if @backslash_quote - no_quoted_values << @escaped_backslash_character - end - @quoted_value = Regexp.new("[^".encode(@encoding) + - no_quoted_values + - "]+".encode(@encoding)) - end - if @escaped_strip - @split_column_separator = Regexp.new(@escaped_strip + - "*".encode(@encoding) + - @escaped_column_separator + - @escaped_strip + - "*".encode(@encoding)) - else - if @column_separator == " ".encode(@encoding) - @split_column_separator = Regexp.new(@escaped_column_separator) - else - @split_column_separator = @column_separator - end - end - end - - def prepare_unquoted - return if @quote_character.nil? - - no_unquoted_values = "\r\n".encode(@encoding) - no_unquoted_values << @escaped_first_column_separator - unless @liberal_parsing - no_unquoted_values << @escaped_quote_character - end - if @escaped_strip - no_unquoted_values << @escaped_strip - end - @unquoted_value = Regexp.new("[^".encode(@encoding) + - no_unquoted_values + - "]+".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? - return false if @quote_character.nil? - - 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 each_line(*args, &block) - @io.each_line(*args, &block) - 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 - chunk_size = ENV["CSV_PARSER_SCANNER_TEST_CHUNK_SIZE"] || "1" - InputsScanner.new(inputs, - @encoding, - chunk_size: Integer(chunk_size, 10)) - 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? - index = string.lines(@row_separator).index do |line| - !line.valid_encoding? - end - if index - message = "Invalid byte sequence in #{@encoding}" - raise MalformedCSVError.new(message, @lineno + index + 1) - end - 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) - @lineno += 1 - @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_no_quote(&block) - @scanner.each_line(@row_separator) do |line| - next if @skip_lines and skip_line?(line) - original_line = line - line = line.delete_suffix(@row_separator) - - if line.empty? - next if @skip_blanks - row = [] - else - line = strip_value(line) - row = line.split(@split_column_separator, -1) - n_columns = row.size - i = 0 - while i < n_columns - row[i] = nil if row[i].empty? - i += 1 - end - end - @last_line = original_line - emit_row(row, &block) - end - end - - def parse_quotable_loose(&block) - @scanner.keep_start - @scanner.each_line(@row_separator) do |line| - if @skip_lines and skip_line?(line) - @scanner.keep_drop - @scanner.keep_start - next - end - original_line = line - line = line.delete_suffix(@row_separator) - - if line.empty? - if @skip_blanks - @scanner.keep_drop - @scanner.keep_start - next - end - row = [] - elsif line.include?(@cr) or line.include?(@lf) - @scanner.keep_back - @need_robust_parsing = true - return parse_quotable_robust(&block) - else - row = line.split(@split_column_separator, -1) - n_columns = row.size - i = 0 - while i < n_columns - column = row[i] - if column.empty? - row[i] = nil - else - n_quotes = column.count(@quote_character) - if n_quotes.zero? - # no quote - elsif n_quotes == 2 and - column.start_with?(@quote_character) and - column.end_with?(@quote_character) - row[i] = column[1..-2] - else - @scanner.keep_back - @need_robust_parsing = true - return parse_quotable_robust(&block) - end - end - i += 1 - end - end - @scanner.keep_drop - @scanner.keep_start - @last_line = original_line - emit_row(row, &block) - end - @scanner.keep_drop - end - - def parse_quotable_robust(&block) - row = [] - skip_needless_lines - start_row - while true - @quoted_column_value = false - @unquoted_column_value = false - @scanner.scan_all(@strip_value) if @strip_value - value = parse_column_value - if value - @scanner.scan_all(@strip_value) if @strip_value - if @field_size_limit and value.size >= @field_size_limit - ignore_broken_line - raise MalformedCSVError.new("Field size exceeded", @lineno) - end - 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? - break if row.empty? and value.nil? - row << value - emit_row(row, &block) - break - else - if @quoted_column_value - ignore_broken_line - message = "Any value after quoted field isn't allowed" - raise MalformedCSVError.new(message, @lineno) - elsif @unquoted_column_value and - (new_line = @scanner.scan(@cr_or_lf)) - ignore_broken_line - message = "Unquoted fields do not allow new line " + - "<#{new_line.inspect}>" - raise MalformedCSVError.new(message, @lineno) - elsif @scanner.rest.start_with?(@quote_character) - ignore_broken_line - message = "Illegal quoting" - raise MalformedCSVError.new(message, @lineno) - elsif (new_line = @scanner.scan(@cr_or_lf)) - ignore_broken_line - message = "New line must be <#{@row_separator.inspect}> " + - "not <#{new_line.inspect}>" - raise MalformedCSVError.new(message, @lineno) - else - ignore_broken_line - raise MalformedCSVError.new("TODO: Meaningful message", - @lineno) - end - end - 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 - if @double_quote_outside_quote - unquoted_value = unquoted_value.gsub(@quote_character * 2, - @quote_character) - if quoted_value.empty? # %Q{""...} case - return @quote_character + unquoted_value - end - end - @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) - return nil unless value - - @unquoted_column_value = true - if @first_column_separators - while true - @scanner.keep_start - is_column_end = @column_ends.all? do |column_end| - @scanner.scan(column_end) - end - @scanner.keep_back - break if is_column_end - sub_separator = @scanner.scan_all(@first_column_separators) - break if sub_separator.nil? - value << sub_separator - sub_value = @scanner.scan_all(@unquoted_value) - break if sub_value.nil? - value << sub_value - end - end - value.gsub!(@backslash_quote_character, @quote_character) if @backslash_quote - 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 - if @backslash_quote - if @scanner.scan(@escaped_backslash) - if @scanner.scan(@escaped_quote) - value << @quote_character - else - value << @backslash_character - end - next - end - end - - quotes = @scanner.scan_all(@quotes) - unless quotes - ignore_broken_line - message = "Unclosed quoted field" - raise MalformedCSVError.new(message, @lineno) - 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 strip_value(value) - return value unless @strip - return nil if value.nil? - - case @strip - when String - size = value.size - while value.start_with?(@strip) - size -= 1 - value = value[1, size] - end - while value.end_with?(@strip) - size -= 1 - value = value[0, size] - end - else - value.strip! - end - value - end - - def ignore_broken_line - @scanner.scan_all(@not_line_end) - @scanner.scan_all(@cr_or_lf) - @lineno += 1 - 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/row.rb b/lib/csv/row.rb deleted file mode 100644 index c79d75cd8a..0000000000 --- a/lib/csv/row.rb +++ /dev/null @@ -1,388 +0,0 @@ -# frozen_string_literal: true - -require "forwardable" - -class CSV - # - # A CSV::Row is part Array and part Hash. It retains an order for the fields - # and allows duplicates just as an Array would, but also allows you to access - # fields by name just as you could if they were in a Hash. - # - # All rows returned by CSV will be constructed from this class, if header row - # processing is activated. - # - class Row - # - # Construct a new CSV::Row from +headers+ and +fields+, which are expected - # to be Arrays. If one Array is shorter than the other, it will be padded - # with +nil+ objects. - # - # The optional +header_row+ parameter can be set to +true+ to indicate, via - # CSV::Row.header_row?() and CSV::Row.field_row?(), that this is a header - # row. Otherwise, the row is assumes to be a field row. - # - # A CSV::Row object supports the following Array methods through delegation: - # - # * empty?() - # * length() - # * size() - # - def initialize(headers, fields, header_row = false) - @header_row = header_row - headers.each { |h| h.freeze if h.is_a? String } - - # handle extra headers or fields - @row = if headers.size >= fields.size - headers.zip(fields) - else - fields.zip(headers).each(&:reverse!) - end - end - - # Internal data format used to compare equality. - attr_reader :row - protected :row - - ### Array Delegation ### - - extend Forwardable - def_delegators :@row, :empty?, :length, :size - - def initialize_copy(other) - super - @row = @row.dup - end - - # Returns +true+ if this is a header row. - def header_row? - @header_row - end - - # Returns +true+ if this is a field row. - def field_row? - not header_row? - end - - # Returns the headers of this row. - def headers - @row.map(&:first) - end - - # - # :call-seq: - # field( header ) - # field( header, offset ) - # field( index ) - # - # This method will return the field value by +header+ or +index+. If a field - # is not found, +nil+ is returned. - # - # When provided, +offset+ ensures that a header match occurs on or later - # than the +offset+ index. You can use this to find duplicate headers, - # without resorting to hard-coding exact indices. - # - def field(header_or_index, minimum_index = 0) - # locate the pair - finder = (header_or_index.is_a?(Integer) || header_or_index.is_a?(Range)) ? :[] : :assoc - pair = @row[minimum_index..-1].send(finder, header_or_index) - - # return the field if we have a pair - if pair.nil? - nil - else - header_or_index.is_a?(Range) ? pair.map(&:last) : pair.last - end - end - alias_method :[], :field - - # - # :call-seq: - # fetch( header ) - # fetch( header ) { |row| ... } - # fetch( header, default ) - # - # This method will fetch the field value by +header+. It has the same - # behavior as Hash#fetch: if there is a field with the given +header+, its - # value is returned. Otherwise, if a block is given, it is yielded the - # +header+ and its result is returned; if a +default+ is given as the - # second argument, it is returned; otherwise a KeyError is raised. - # - def fetch(header, *varargs) - raise ArgumentError, "Too many arguments" if varargs.length > 1 - pair = @row.assoc(header) - if pair - pair.last - else - if block_given? - yield header - elsif varargs.empty? - raise KeyError, "key not found: #{header}" - else - varargs.first - end - end - end - - # Returns +true+ if there is a field with the given +header+. - def has_key?(header) - !!@row.assoc(header) - end - alias_method :include?, :has_key? - alias_method :key?, :has_key? - alias_method :member?, :has_key? - alias_method :header?, :has_key? - - # - # :call-seq: - # []=( header, value ) - # []=( header, offset, value ) - # []=( index, value ) - # - # Looks up the field by the semantics described in CSV::Row.field() and - # assigns the +value+. - # - # Assigning past the end of the row with an index will set all pairs between - # to <tt>[nil, nil]</tt>. Assigning to an unused header appends the new - # pair. - # - def []=(*args) - value = args.pop - - if args.first.is_a? Integer - if @row[args.first].nil? # extending past the end with index - @row[args.first] = [nil, value] - @row.map! { |pair| pair.nil? ? [nil, nil] : pair } - else # normal index assignment - @row[args.first][1] = value - end - else - index = index(*args) - if index.nil? # appending a field - self << [args.first, value] - else # normal header assignment - @row[index][1] = value - end - end - end - - # - # :call-seq: - # <<( field ) - # <<( header_and_field_array ) - # <<( header_and_field_hash ) - # - # If a two-element Array is provided, it is assumed to be a header and field - # and the pair is appended. A Hash works the same way with the key being - # the header and the value being the field. Anything else is assumed to be - # a lone field which is appended with a +nil+ header. - # - # This method returns the row for chaining. - # - def <<(arg) - if arg.is_a?(Array) and arg.size == 2 # appending a header and name - @row << arg - elsif arg.is_a?(Hash) # append header and name pairs - arg.each { |pair| @row << pair } - else # append field value - @row << [nil, arg] - end - - self # for chaining - end - - # - # A shortcut for appending multiple fields. Equivalent to: - # - # args.each { |arg| csv_row << arg } - # - # This method returns the row for chaining. - # - def push(*args) - args.each { |arg| self << arg } - - self # for chaining - end - - # - # :call-seq: - # delete( header ) - # delete( header, offset ) - # delete( index ) - # - # Used to remove a pair from the row by +header+ or +index+. The pair is - # located as described in CSV::Row.field(). The deleted pair is returned, - # or +nil+ if a pair could not be found. - # - def delete(header_or_index, minimum_index = 0) - if header_or_index.is_a? Integer # by index - @row.delete_at(header_or_index) - elsif i = index(header_or_index, minimum_index) # by header - @row.delete_at(i) - else - [ ] - end - end - - # - # The provided +block+ is passed a header and field for each pair in the row - # and expected to return +true+ or +false+, depending on whether the pair - # should be deleted. - # - # This method returns the row for chaining. - # - # If no block is given, an Enumerator is returned. - # - def delete_if(&block) - return enum_for(__method__) { size } unless block_given? - - @row.delete_if(&block) - - self # for chaining - end - - # - # This method accepts any number of arguments which can be headers, indices, - # Ranges of either, or two-element Arrays containing a header and offset. - # Each argument will be replaced with a field lookup as described in - # CSV::Row.field(). - # - # If called with no arguments, all fields are returned. - # - def fields(*headers_and_or_indices) - if headers_and_or_indices.empty? # return all fields--no arguments - @row.map(&:last) - else # or work like values_at() - all = [] - headers_and_or_indices.each do |h_or_i| - if h_or_i.is_a? Range - index_begin = h_or_i.begin.is_a?(Integer) ? h_or_i.begin : - index(h_or_i.begin) - index_end = h_or_i.end.is_a?(Integer) ? h_or_i.end : - index(h_or_i.end) - new_range = h_or_i.exclude_end? ? (index_begin...index_end) : - (index_begin..index_end) - all.concat(fields.values_at(new_range)) - else - all << field(*Array(h_or_i)) - end - end - return all - end - end - alias_method :values_at, :fields - - # - # :call-seq: - # index( header ) - # index( header, offset ) - # - # This method will return the index of a field with the provided +header+. - # The +offset+ can be used to locate duplicate header names, as described in - # CSV::Row.field(). - # - def index(header, minimum_index = 0) - # find the pair - index = headers[minimum_index..-1].index(header) - # return the index at the right offset, if we found one - index.nil? ? nil : index + minimum_index - end - - # - # Returns +true+ if +data+ matches a field in this row, and +false+ - # otherwise. - # - def field?(data) - fields.include? data - end - - include Enumerable - - # - # Yields each pair of the row as header and field tuples (much like - # iterating over a Hash). This method returns the row for chaining. - # - # If no block is given, an Enumerator is returned. - # - # Support for Enumerable. - # - def each(&block) - return enum_for(__method__) { size } unless block_given? - - @row.each(&block) - - self # for chaining - end - - alias_method :each_pair, :each - - # - # Returns +true+ if this row contains the same headers and fields in the - # same order as +other+. - # - def ==(other) - return @row == other.row if other.is_a? CSV::Row - @row == other - end - - # - # Collapses the row into a simple Hash. Be warned that this discards field - # order and clobbers duplicate fields. - # - def to_h - hash = {} - each do |key, _value| - hash[key] = self[key] unless hash.key?(key) - end - hash - end - alias_method :to_hash, :to_h - - alias_method :to_ary, :to_a - - # - # Returns the row as a CSV String. Headers are not used. Equivalent to: - # - # csv_row.fields.to_csv( options ) - # - def to_csv(**options) - fields.to_csv(options) - end - alias_method :to_s, :to_csv - - # - # Extracts the nested value specified by the sequence of +index+ or +header+ objects by calling dig at each step, - # returning nil if any intermediate step is nil. - # - def dig(index_or_header, *indexes) - value = field(index_or_header) - if value.nil? - nil - elsif indexes.empty? - value - else - unless value.respond_to?(:dig) - raise TypeError, "#{value.class} does not have \#dig method" - end - value.dig(*indexes) - end - end - - # A summary of fields, by header, in an ASCII compatible String. - def inspect - str = ["#<", self.class.to_s] - each do |header, field| - str << " " << (header.is_a?(Symbol) ? header.to_s : header.inspect) << - ":" << field.inspect - end - str << ">" - begin - str.join('') - rescue # any encoding error - str.map do |s| - e = Encoding::Converter.asciicompat_encoding(s.encoding) - e ? s.encode(e) : s.force_encoding("ASCII-8BIT") - end.join('') - end - end - end -end diff --git a/lib/csv/table.rb b/lib/csv/table.rb deleted file mode 100644 index 71eb885de5..0000000000 --- a/lib/csv/table.rb +++ /dev/null @@ -1,402 +0,0 @@ -# frozen_string_literal: true - -require "forwardable" - -class CSV - # - # A CSV::Table is a two-dimensional data structure for representing CSV - # documents. Tables allow you to work with the data by row or column, - # manipulate the data, and even convert the results back to CSV, if needed. - # - # All tables returned by CSV will be constructed from this class, if header - # row processing is activated. - # - class Table - # - # 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 argument. - # - # A CSV::Table object supports the following Array methods through - # delegation: - # - # * empty?() - # * length() - # * size() - # - 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 - - # The current access mode for indexing and iteration. - attr_reader :mode - - # Internal data format used to compare equality. - attr_reader :table - protected :table - - ### Array Delegation ### - - extend Forwardable - def_delegators :@table, :empty?, :length, :size - - # - # Returns a duplicate table object, in column mode. This is handy for - # chaining in a single call without changing the table mode, but be aware - # that this method can consume a fair amount of memory for bigger data sets. - # - # This method returns the duplicate table for chaining. Don't chain - # destructive methods (like []=()) this way though, since you are working - # with a duplicate. - # - def by_col - self.class.new(@table.dup).by_col! - end - - # - # Switches the mode of this table to column mode. All calls to indexing and - # iteration methods will work with columns until the mode is changed again. - # - # This method returns the table and is safe to chain. - # - def by_col! - @mode = :col - - self - end - - # - # Returns a duplicate table object, in mixed mode. This is handy for - # chaining in a single call without changing the table mode, but be aware - # that this method can consume a fair amount of memory for bigger data sets. - # - # This method returns the duplicate table for chaining. Don't chain - # destructive methods (like []=()) this way though, since you are working - # with a duplicate. - # - def by_col_or_row - self.class.new(@table.dup).by_col_or_row! - end - - # - # Switches the mode of this table to mixed mode. All calls to indexing and - # iteration methods will use the default intelligent indexing system until - # the mode is changed again. In mixed mode an index is assumed to be a row - # reference while anything else is assumed to be column access by headers. - # - # This method returns the table and is safe to chain. - # - def by_col_or_row! - @mode = :col_or_row - - self - end - - # - # Returns a duplicate table object, in row mode. This is handy for chaining - # in a single call without changing the table mode, but be aware that this - # method can consume a fair amount of memory for bigger data sets. - # - # This method returns the duplicate table for chaining. Don't chain - # destructive methods (like []=()) this way though, since you are working - # with a duplicate. - # - def by_row - self.class.new(@table.dup).by_row! - end - - # - # Switches the mode of this table to row mode. All calls to indexing and - # iteration methods will work with rows until the mode is changed again. - # - # This method returns the table and is safe to chain. - # - def by_row! - @mode = :row - - self - end - - # - # Returns the headers for the first row of this table (assumed to match all - # other rows). The headers Array passed to CSV::Table.new is returned for - # empty tables. - # - def headers - if @table.empty? - @headers.dup - else - @table.first.headers - end - end - - # - # In the default mixed mode, this method returns rows for index access and - # columns for header access. You can force the index association by first - # calling by_col!() or by_row!(). - # - # Columns are returned as an Array of values. Altering that Array has no - # effect on the table. - # - def [](index_or_header) - if @mode == :row or # by index - (@mode == :col_or_row and (index_or_header.is_a?(Integer) or index_or_header.is_a?(Range))) - @table[index_or_header] - else # by header - @table.map { |row| row[index_or_header] } - end - end - - # - # In the default mixed mode, this method assigns rows for index access and - # columns for header access. You can force the index association by first - # calling by_col!() or by_row!(). - # - # Rows may be set to an Array of values (which will inherit the table's - # headers()) or a CSV::Row. - # - # Columns may be set to a single value, which is copied to each row of the - # column, or an Array of values. Arrays of values are assigned to rows top - # to bottom in row major order. Excess values are ignored and if the Array - # does not have a value for each row the extra rows will receive a +nil+. - # - # Assigning to an existing column or row clobbers the data. Assigning to - # new columns creates them at the right end of the table. - # - def []=(index_or_header, value) - if @mode == :row or # by index - (@mode == :col_or_row and index_or_header.is_a? Integer) - if value.is_a? Array - @table[index_or_header] = Row.new(headers, value) - else - @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? - row[index_or_header] = index_or_header - else - row[index_or_header] = value[i] - end - end - else # repeated value - @table.each do |row| - if row.header_row? - row[index_or_header] = index_or_header - else - row[index_or_header] = value - end - end - end - end - end - - # - # The mixed mode default is to treat a list of indices as row access, - # returning the rows indicated. Anything else is considered columnar - # access. For columnar access, the return set has an Array for each row - # with the values indicated by the headers in each Array. You can force - # column or row mode using by_col!() or by_row!(). - # - # You cannot mix column and row access. - # - def values_at(*indices_or_headers) - if @mode == :row or # by indices - ( @mode == :col_or_row and indices_or_headers.all? do |index| - index.is_a?(Integer) or - ( index.is_a?(Range) and - index.first.is_a?(Integer) and - index.last.is_a?(Integer) ) - end ) - @table.values_at(*indices_or_headers) - else # by headers - @table.map { |row| row.values_at(*indices_or_headers) } - end - end - - # - # Adds a new row to the bottom end of this table. You can provide an Array, - # which will be converted to a CSV::Row (inheriting the table's headers()), - # or a CSV::Row. - # - # This method returns the table for chaining. - # - def <<(row_or_array) - if row_or_array.is_a? Array # append Array - @table << Row.new(headers, row_or_array) - else # append Row - @table << row_or_array - end - - self # for chaining - end - - # - # A shortcut for appending multiple rows. Equivalent to: - # - # rows.each { |row| self << row } - # - # This method returns the table for chaining. - # - def push(*rows) - rows.each { |row| self << row } - - self # for chaining - end - - # - # Removes and returns the indicated columns or rows. In the default mixed - # mode indices refer to rows and everything else is assumed to be a column - # headers. Use by_col!() or by_row!() to force the lookup. - # - def delete(*indexes_or_headers) - if indexes_or_headers.empty? - raise ArgumentError, "wrong number of arguments (given 0, expected 1+)" - end - deleted_values = indexes_or_headers.map do |index_or_header| - if @mode == :row or # by index - (@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 - if indexes_or_headers.size == 1 - deleted_values[0] - else - deleted_values - end - end - - # - # Removes any column or row for which the block returns +true+. In the - # default mixed mode or row mode, iteration is the standard row major - # walking of rows. In column mode, iteration will +yield+ two element - # tuples containing the column name and an Array of values for that column. - # - # This method returns the table for chaining. - # - # If no block is given, an Enumerator is returned. - # - def delete_if(&block) - return enum_for(__method__) { @mode == :row or @mode == :col_or_row ? size : headers.size } unless block_given? - - if @mode == :row or @mode == :col_or_row # by index - @table.delete_if(&block) - else # by header - deleted = [] - headers.each do |header| - deleted << delete(header) if yield([header, self[header]]) - end - end - - self # for chaining - end - - include Enumerable - - # - # In the default mixed mode or row mode, iteration is the standard row major - # walking of rows. In column mode, iteration will +yield+ two element - # tuples containing the column name and an Array of values for that column. - # - # This method returns the table for chaining. - # - # If no block is given, an Enumerator is returned. - # - def each(&block) - return enum_for(__method__) { @mode == :col ? headers.size : size } unless block_given? - - if @mode == :col - headers.each { |header| yield([header, self[header]]) } - else - @table.each(&block) - end - - self # for chaining - end - - # Returns +true+ if all rows of this table ==() +other+'s rows. - def ==(other) - return @table == other.table if other.is_a? CSV::Table - @table == other - end - - # - # Returns the table as an Array of Arrays. Headers will be the first row, - # then all of the field rows will follow. - # - def to_a - array = [headers] - @table.each do |row| - array.push(row.fields) unless row.header_row? - end - - array - end - - # - # Returns the table as a complete CSV String. Headers will be listed first, - # then all of the field rows. - # - # This method assumes you want the Table.headers(), unless you explicitly - # pass <tt>:write_headers => false</tt>. - # - def to_csv(write_headers: true, **options) - array = write_headers ? [headers.to_csv(options)] : [] - @table.each do |row| - array.push(row.fields.to_csv(options)) unless row.header_row? - end - - array.join("") - end - alias_method :to_s, :to_csv - - # - # Extracts the nested value specified by the sequence of +index+ or +header+ objects by calling dig at each step, - # returning nil if any intermediate step is nil. - # - def dig(index_or_header, *index_or_headers) - value = self[index_or_header] - if value.nil? - nil - elsif index_or_headers.empty? - value - else - unless value.respond_to?(:dig) - raise TypeError, "#{value.class} does not have \#dig method" - end - value.dig(*index_or_headers) - end - end - - # Shows the mode and size of this table in a US-ASCII String. - def inspect - "#<#{self.class} mode:#{@mode} row_count:#{to_a.size}>".encode("US-ASCII") - end - end -end diff --git a/lib/csv/version.rb b/lib/csv/version.rb deleted file mode 100644 index ce55373f02..0000000000 --- a/lib/csv/version.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -class CSV - # The version of the installed library. - VERSION = "3.1.1" -end diff --git a/lib/csv/writer.rb b/lib/csv/writer.rb deleted file mode 100644 index 1682ac03ea..0000000000 --- a/lib/csv/writer.rb +++ /dev/null @@ -1,157 +0,0 @@ -# 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 |
