summaryrefslogtreecommitdiff
path: root/lib/csv
diff options
context:
space:
mode:
Diffstat (limited to 'lib/csv')
-rw-r--r--lib/csv/core_ext/array.rb9
-rw-r--r--lib/csv/core_ext/string.rb9
-rw-r--r--lib/csv/csv.gemspec64
-rw-r--r--lib/csv/delete_suffix.rb18
-rw-r--r--lib/csv/fields_converter.rb88
-rw-r--r--lib/csv/input_record_separator.rb31
-rw-r--r--lib/csv/match_p.rb20
-rw-r--r--lib/csv/parser.rb1172
-rw-r--r--lib/csv/row.rb624
-rw-r--r--lib/csv/table.rb621
-rw-r--r--lib/csv/version.rb6
-rw-r--r--lib/csv/writer.rb210
12 files changed, 0 insertions, 2872 deletions
diff --git a/lib/csv/core_ext/array.rb b/lib/csv/core_ext/array.rb
deleted file mode 100644
index 8beb06b082..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 9b1d31c2a4..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 11c5b0f2a6..0000000000
--- a/lib/csv/csv.gemspec
+++ /dev/null
@@ -1,64 +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.licenses = ["Ruby", "BSD-2-Clause"]
-
- lib_path = "lib"
- spec.require_paths = [lib_path]
- files = []
- lib_dir = File.join(__dir__, lib_path)
- if File.exist?(lib_dir)
- Dir.chdir(lib_dir) do
- Dir.glob("**/*.rb").each do |file|
- files << "lib/#{file}"
- end
- end
- end
- doc_dir = File.join(__dir__, "doc")
- if File.exist?(doc_dir)
- Dir.chdir(doc_dir) do
- Dir.glob("**/*.rdoc").each do |rdoc_file|
- files << "doc/#{rdoc_file}"
- end
- end
- end
- spec.files = files
- spec.rdoc_options.concat(["--main", "README.md"])
- rdoc_files = [
- "LICENSE.txt",
- "NEWS.md",
- "README.md",
- ]
- recipes_dir = File.join(doc_dir, "csv", "recipes")
- if File.exist?(recipes_dir)
- Dir.chdir(recipes_dir) do
- Dir.glob("**/*.rdoc").each do |recipe_file|
- rdoc_files << "doc/csv/recipes/#{recipe_file}"
- end
- end
- end
- spec.extra_rdoc_files = rdoc_files
-
- spec.required_ruby_version = ">= 2.5.0"
-
- # spec.add_dependency "stringio", ">= 0.1.3"
- spec.add_development_dependency "bundler"
- spec.add_development_dependency "rake"
- spec.add_development_dependency "benchmark_driver"
- spec.add_development_dependency "test-unit", ">= 3.4.8"
-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 b206118d99..0000000000
--- a/lib/csv/fields_converter.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-# frozen_string_literal: true
-
-class CSV
- # Note: Don't use this class directly. This is an internal class.
- class FieldsConverter
- include Enumerable
- #
- # A CSV::FieldsConverter is a data structure for storing the
- # fields converter properties to be passed as a parameter
- # when parsing a new file (e.g. CSV::Parser.new(@io, parser_options))
- #
-
- 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_name = options[:builtin_converters_name]
- @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.is_a?(String) and 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
-
- def builtin_converters
- @builtin_converters ||= ::CSV.const_get(@builtin_converters_name)
- end
- end
-end
diff --git a/lib/csv/input_record_separator.rb b/lib/csv/input_record_separator.rb
deleted file mode 100644
index bbf13479f7..0000000000
--- a/lib/csv/input_record_separator.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-require "English"
-require "stringio"
-
-class CSV
- module InputRecordSeparator
- class << self
- is_input_record_separator_deprecated = false
- verbose, $VERBOSE = $VERBOSE, true
- stderr, $stderr = $stderr, StringIO.new
- input_record_separator = $INPUT_RECORD_SEPARATOR
- begin
- $INPUT_RECORD_SEPARATOR = "\r\n"
- is_input_record_separator_deprecated = (not $stderr.string.empty?)
- ensure
- $INPUT_RECORD_SEPARATOR = input_record_separator
- $stderr = stderr
- $VERBOSE = verbose
- end
-
- if is_input_record_separator_deprecated
- def value
- "\n"
- end
- else
- def value
- $INPUT_RECORD_SEPARATOR
- end
- end
- 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 7e943acf21..0000000000
--- a/lib/csv/parser.rb
+++ /dev/null
@@ -1,1172 +0,0 @@
-# frozen_string_literal: true
-
-require "strscan"
-
-require_relative "delete_suffix"
-require_relative "input_record_separator"
-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
- # Note: Don't use this class directly. This is an internal class.
- class Parser
- #
- # A CSV::Parser is m17n aware. The parser works in the Encoding of the IO
- # or String object being read from or written to. Your data is never transcoded
- # (unless you ask Ruby to transcode it for you) and will literally be parsed in
- # the Encoding it is in. Thus CSV will return Arrays or Rows of Strings in the
- # Encoding of your data. This is accomplished by transcoding the parser itself
- # into your Encoding.
- #
-
- # Raised when encoding is invalid.
- class InvalidEncoding < StandardError
- end
-
- #
- # CSV::Scanner receives a CSV output, scans it and return the content.
- # It also controls the life cycle of the object with its methods +keep_start+,
- # +keep_end+, +keep_back+, +keep_drop+.
- #
- # Uses StringScanner (the official strscan gem). Strscan provides lexical
- # scanning operations on a String. We inherit its object and take advantage
- # on the methods. For more information, please visit:
- # https://ruby-doc.org/stdlib-2.6.1/libdoc/strscan/rdoc/StringScanner.html
- #
- 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.byteslice(start, pos - start)
- end
-
- def keep_back
- self.pos = @keeps.pop
- end
-
- def keep_drop
- @keeps.pop
- end
- end
-
- #
- # CSV::InputsScanner receives IO inputs, encoding and the chunk_size.
- # It also controls the life cycle of the object with its methods +keep_start+,
- # +keep_end+, +keep_back+, +keep_drop+.
- #
- # CSV::InputsScanner.scan() tries to match with pattern at the current position.
- # If there's a match, the scanner advances the “scan pointer” and returns the matched string.
- # Otherwise, the scanner returns nil.
- #
- # CSV::InputsScanner.rest() returns the “rest” of the string (i.e. everything after the scan pointer).
- # If there is no more data (eos? = true), it returns "".
- #
- class InputsScanner
- def initialize(inputs, encoding, row_separator, chunk_size: 8192)
- @inputs = inputs.dup
- @encoding = encoding
- @row_separator = row_separator
- @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.byteslice(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.read
- raise InvalidEncoding unless string.valid_encoding?
- @scanner = StringScanner.new(string)
- @inputs.shift
- @last_scanner = @inputs.empty?
- true
- else
- chunk = input.gets(@row_separator, @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
- # A set of tasks to prepare the file in order to parse it
- def prepare
- prepare_variable
- prepare_quote_character
- prepare_backslash
- prepare_skip_lines
- prepare_strip
- prepare_separators
- validate_strip_and_col_sep_options
- 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
- @rstrip_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))
- @rstrip_value = Regexp.new(@escaped_strip +
- "+\\z".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))
- @rstrip_value = Regexp.new("[#{strip_values}]+\\z".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]
- @column_separator = column_separator.to_s.encode(@encoding)
- if @column_separator.size < 1
- message = ":col_sep must be 1 or more characters: "
- message += column_separator.inspect
- raise ArgumentError, message
- end
- @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)
- @line_end = Regexp.new("\r\n|\n|\r".encode(@encoding))
- @not_line_end = Regexp.new("[^\r\n]+".encode(@encoding))
- end
-
- # This method verifies that there are no (obvious) ambiguities with the
- # provided +col_sep+ and +strip+ parsing options. For example, if +col_sep+
- # and +strip+ were both equal to +\t+, then there would be no clear way to
- # parse the input.
- def validate_strip_and_col_sep_options
- return unless @strip
-
- if @strip.is_a?(String)
- if @column_separator.start_with?(@strip) || @column_separator.end_with?(@strip)
- raise ArgumentError,
- "The provided strip (#{@escaped_strip}) and " \
- "col_sep (#{@escaped_column_separator}) options are incompatible."
- end
- else
- if Regexp.new("\\A[#{@escaped_strip}]|[#{@escaped_strip}]\\z").match?(@column_separator)
- raise ArgumentError,
- "The provided strip (true) and " \
- "col_sep (#{@escaped_column_separator}) options are incompatible."
- end
- end
- 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
- @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)
- pos = @input.pos
- separator = detect_row_separator(@input.read, cr, lf)
- @input.seek(pos)
- 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 = InputRecordSeparator.value 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)
- pos = @input.pos
- sample = @input.read
- @input.seek(pos)
- 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, "rb:#{string.encoding}")
- 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
-
- SCANNER_TEST_CHUNK_SIZE =
- Integer((ENV["CSV_PARSER_SCANNER_TEST_CHUNK_SIZE"] || "1"), 10)
- def build_scanner
- inputs = @samples.collect do |sample|
- UnoptimizedStringIO.new(sample)
- end
- if @input.is_a?(StringIO)
- inputs << UnoptimizedStringIO.new(@input.read)
- else
- inputs << @input
- end
- InputsScanner.new(inputs,
- @encoding,
- @row_separator,
- chunk_size: SCANNER_TEST_CHUNK_SIZE)
- end
- else
- def build_scanner
- string = nil
- if @samples.empty? and @input.is_a?(StringIO)
- string = @input.read
- elsif @samples.size == 1 and
- @input != ARGF 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, @row_separator)
- end
- end
- end
-
- def skip_needless_lines
- return unless @skip_lines
-
- until @scanner.eos?
- @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)
- line = line.delete_suffix(@row_separator)
- 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(@line_end))
- 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(@line_end))
- 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
- @scanner.scan_all(@strip_value) if @strip_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
- if @rstrip_value
- value.gsub!(@rstrip_value, "")
- end
- 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(@line_end)
- @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 0f465ea2a3..0000000000
--- a/lib/csv/row.rb
+++ /dev/null
@@ -1,624 +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
- #
- # Constructs 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 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_return_value = super
- @row = @row.collect(&:dup)
- super_return_value
- end
-
- # :call-seq:
- # row.header_row? -> true or false
- #
- # Returns +true+ if this is a header row, +false+ otherwise.
- def header_row?
- @header_row
- end
-
- # :call-seq:
- # row.field_row? -> true or false
- #
- # Returns +true+ if this is a field row, +false+ otherwise.
- def field_row?
- not header_row?
- end
-
- # :call-seq:
- # row.headers
- #
- # Returns the headers for this row:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # row = table.first
- # row.headers # => ["Name", "Value"]
- def headers
- @row.map(&:first)
- end
-
- # :call-seq:
- # field(index)
- # field(header)
- # field(header, offset)
- #
- # Returns the field value for the given +index+ or +header+.
- #
- # ---
- #
- # Fetch field value by \Integer index:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.field(0) # => "foo"
- # row.field(1) # => "bar"
- #
- # Counts backward from the last column if +index+ is negative:
- # row.field(-1) # => "0"
- # row.field(-2) # => "foo"
- #
- # Returns +nil+ if +index+ is out of range:
- # row.field(2) # => nil
- # row.field(-3) # => nil
- #
- # ---
- #
- # Fetch field value by header (first found):
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.field('Name') # => "Foo"
- #
- # Fetch field value by header, ignoring +offset+ leading fields:
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.field('Name', 2) # => "Baz"
- #
- # Returns +nil+ if the header does not exist.
- 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].public_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, default)
- # fetch(header) {|row| ... }
- #
- # Returns the field value as specified by +header+.
- #
- # ---
- #
- # With the single argument +header+, returns the field value
- # for that header (first found):
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.fetch('Name') # => "Foo"
- #
- # Raises exception +KeyError+ if the header does not exist.
- #
- # ---
- #
- # With arguments +header+ and +default+ given,
- # returns the field value for the header (first found)
- # if the header exists, otherwise returns +default+:
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.fetch('Name', '') # => "Foo"
- # row.fetch(:nosuch, '') # => ""
- #
- # ---
- #
- # With argument +header+ and a block given,
- # returns the field value for the header (first found)
- # if the header exists; otherwise calls the block
- # and returns its return value:
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.fetch('Name') {|header| fail 'Cannot happen' } # => "Foo"
- # row.fetch(:nosuch) {|header| "Header '#{header} not found'" } # => "Header 'nosuch not found'"
- 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
-
- # :call-seq:
- # row.has_key?(header)
- #
- # Returns +true+ if there is a field with the given +header+,
- # +false+ otherwise.
- 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:
- # row[index] = value -> value
- # row[header, offset] = value -> value
- # row[header] = value -> value
- #
- # Assigns the field value for the given +index+ or +header+;
- # returns +value+.
- #
- # ---
- #
- # Assign field value by \Integer index:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row[0] = 'Bat'
- # row[1] = 3
- # row # => #<CSV::Row "Name":"Bat" "Value":3>
- #
- # Counts backward from the last column if +index+ is negative:
- # row[-1] = 4
- # row[-2] = 'Bam'
- # row # => #<CSV::Row "Name":"Bam" "Value":4>
- #
- # Extends the row with <tt>nil:nil</tt> if positive +index+ is not in the row:
- # row[4] = 5
- # row # => #<CSV::Row "Name":"bad" "Value":4 nil:nil nil:nil nil:5>
- #
- # Raises IndexError if negative +index+ is too small (too far from zero).
- #
- # ---
- #
- # Assign field value by header (first found):
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row['Name'] = 'Bat'
- # row # => #<CSV::Row "Name":"Bat" "Name":"Bar" "Name":"Baz">
- #
- # Assign field value by header, ignoring +offset+ leading fields:
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row['Name', 2] = 4
- # row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":4>
- #
- # Append new field by (new) header:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row['New'] = 6
- # row# => #<CSV::Row "Name":"foo" "Value":"0" "New":6>
- 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:
- # row << [header, value] -> self
- # row << hash -> self
- # row << value -> self
- #
- # Adds a field to +self+; returns +self+:
- #
- # If the argument is a 2-element \Array <tt>[header, value]</tt>,
- # a field is added with the given +header+ and +value+:
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row << ['NAME', 'Bat']
- # row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" "NAME":"Bat">
- #
- # If the argument is a \Hash, each <tt>key-value</tt> pair is added
- # as a field with header +key+ and value +value+.
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row << {NAME: 'Bat', name: 'Bam'}
- # row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" NAME:"Bat" name:"Bam">
- #
- # Otherwise, the given +value+ is added as a field with no header.
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row << 'Bag'
- # row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" nil:"Bag">
- 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
-
- # :call-seq:
- # row.push(*values) ->self
- #
- # Appends each of the given +values+ to +self+ as a field; returns +self+:
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.push('Bat', 'Bam')
- # row # => #<CSV::Row "Name":"Foo" "Name":"Bar" "Name":"Baz" nil:"Bat" nil:"Bam">
- def push(*args)
- args.each { |arg| self << arg }
-
- self # for chaining
- end
-
- #
- # :call-seq:
- # delete(index) -> [header, value] or nil
- # delete(header) -> [header, value] or empty_array
- # delete(header, offset) -> [header, value] or empty_array
- #
- # Removes a specified field from +self+; returns the 2-element \Array
- # <tt>[header, value]</tt> if the field exists.
- #
- # If an \Integer argument +index+ is given,
- # removes and returns the field at offset +index+,
- # or returns +nil+ if the field does not exist:
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.delete(1) # => ["Name", "Bar"]
- # row.delete(50) # => nil
- #
- # Otherwise, if the single argument +header+ is given,
- # removes and returns the first-found field with the given header,
- # of returns a new empty \Array if the field does not exist:
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.delete('Name') # => ["Name", "Foo"]
- # row.delete('NAME') # => []
- #
- # If argument +header+ and \Integer argument +offset+ are given,
- # removes and returns the first-found field with the given header
- # whose +index+ is at least as large as +offset+:
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.delete('Name', 1) # => ["Name", "Bar"]
- # row.delete('NAME', 1) # => []
- 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
-
- # :call-seq:
- # row.delete_if {|header, value| ... } -> self
- #
- # Removes fields from +self+ as selected by the block; returns +self+.
- #
- # Removes each field for which the block returns a truthy value:
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.delete_if {|header, value| value.start_with?('B') } # => true
- # row # => #<CSV::Row "Name":"Foo">
- # row.delete_if {|header, value| header.start_with?('B') } # => false
- #
- # If no block is given, returns a new Enumerator:
- # row.delete_if # => #<Enumerator: #<CSV::Row "Name":"Foo">:delete_if>
- def delete_if(&block)
- return enum_for(__method__) { size } unless block_given?
-
- @row.delete_if(&block)
-
- self # for chaining
- end
-
- # :call-seq:
- # self.fields(*specifiers)
- #
- # Returns field values per the given +specifiers+, which may be any mixture of:
- # - \Integer index.
- # - \Range of \Integer indexes.
- # - 2-element \Array containing a header and offset.
- # - Header.
- # - \Range of headers.
- #
- # For +specifier+ in one of the first four cases above,
- # returns the result of <tt>self.field(specifier)</tt>; see #field.
- #
- # Although there may be any number of +specifiers+,
- # the examples here will illustrate one at a time.
- #
- # When the specifier is an \Integer +index+,
- # returns <tt>self.field(index)</tt>L
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.fields(1) # => ["Bar"]
- #
- # When the specifier is a \Range of \Integers +range+,
- # returns <tt>self.field(range)</tt>:
- # row.fields(1..2) # => ["Bar", "Baz"]
- #
- # When the specifier is a 2-element \Array +array+,
- # returns <tt>self.field(array)</tt>L
- # row.fields('Name', 1) # => ["Foo", "Bar"]
- #
- # When the specifier is a header +header+,
- # returns <tt>self.field(header)</tt>L
- # row.fields('Name') # => ["Foo"]
- #
- # When the specifier is a \Range of headers +range+,
- # forms a new \Range +new_range+ from the indexes of
- # <tt>range.start</tt> and <tt>range.end</tt>,
- # and returns <tt>self.field(new_range)</tt>:
- # source = "Name,NAME,name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.fields('Name'..'NAME') # => ["Foo", "Bar"]
- #
- # Returns all fields if no argument given:
- # row.fields # => ["Foo", "Bar", "Baz"]
- 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
-
- # :call-seq:
- # row.to_h -> hash
- #
- # Returns the new \Hash formed by adding each header-value pair in +self+
- # as a key-value pair in the \Hash.
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.to_h # => {"Name"=>"foo", "Value"=>"0"}
- #
- # Header order is preserved, but repeated headers are ignored:
- # source = "Name,Name,Name\nFoo,Bar,Baz\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.to_h # => {"Name"=>"Foo"}
- 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
-
- # :call-seq:
- # row.to_csv -> csv_string
- #
- # Returns the row as a \CSV String. Headers are not included:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.to_csv # => "foo,0\n"
- def to_csv(**options)
- fields.to_csv(**options)
- end
- alias_method :to_s, :to_csv
-
- # :call-seq:
- # row.dig(index_or_header, *identifiers) -> object
- #
- # Finds and returns the object in nested object that is specified
- # by +index_or_header+ and +specifiers+.
- #
- # The nested objects may be instances of various classes.
- # See {Dig Methods}[https://docs.ruby-lang.org/en/master/doc/dig_methods_rdoc.html].
- #
- # Examples:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.dig(1) # => "0"
- # row.dig('Value') # => "0"
- # row.dig(5) # => 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
-
- # :call-seq:
- # row.inspect -> string
- #
- # Returns an ASCII-compatible \String showing:
- # - Class \CSV::Row.
- # - Header-value pairs.
- # Example:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # row = table[0]
- # row.inspect # => "#<CSV::Row \"Name\":\"foo\" \"Value\":\"0\">"
- 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 1ce0dd6daf..0000000000
--- a/lib/csv/table.rb
+++ /dev/null
@@ -1,621 +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
- #
- # Constructs 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
-
- # :call-seq:
- # table[n] -> row
- # table[range] -> array_of_rows
- # table[header] -> array_of_fields
- #
- # Returns data from the table; does not modify the table.
- #
- # ---
- #
- # The expression <tt>table[n]</tt>, where +n+ is a non-negative \Integer,
- # returns the +n+th row of the table, if that row exists,
- # and if the access mode is <tt>:row</tt> or <tt>:col_or_row</tt>:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # table.by_row! # => #<CSV::Table mode:row row_count:4>
- # table[1] # => #<CSV::Row "Name":"bar" "Value":"1">
- # table.by_col_or_row! # => #<CSV::Table mode:col_or_row row_count:4>
- # table[1] # => #<CSV::Row "Name":"bar" "Value":"1">
- #
- # Counts backward from the last row if +n+ is negative:
- # table[-1] # => #<CSV::Row "Name":"baz" "Value":"2">
- #
- # Returns +nil+ if +n+ is too large or too small:
- # table[4] # => nil
- # table[-4] => nil
- #
- # Raises an exception if the access mode is <tt>:row</tt>
- # and +n+ is not an
- # {Integer-convertible object}[https://docs.ruby-lang.org/en/master/implicit_conversion_rdoc.html#label-Integer-Convertible+Objects].
- # table.by_row! # => #<CSV::Table mode:row row_count:4>
- # # Raises TypeError (no implicit conversion of String into Integer):
- # table['Name']
- #
- # ---
- #
- # The expression <tt>table[range]</tt>, where +range+ is a Range object,
- # returns rows from the table, beginning at row <tt>range.first</tt>,
- # if those rows exist, and if the access mode is <tt>:row</tt> or <tt>:col_or_row</tt>:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # table.by_row! # => #<CSV::Table mode:row row_count:4>
- # rows = table[1..2] # => #<CSV::Row "Name":"bar" "Value":"1">
- # rows # => [#<CSV::Row "Name":"bar" "Value":"1">, #<CSV::Row "Name":"baz" "Value":"2">]
- # table.by_col_or_row! # => #<CSV::Table mode:col_or_row row_count:4>
- # rows = table[1..2] # => #<CSV::Row "Name":"bar" "Value":"1">
- # rows # => [#<CSV::Row "Name":"bar" "Value":"1">, #<CSV::Row "Name":"baz" "Value":"2">]
- #
- # If there are too few rows, returns all from <tt>range.first</tt> to the end:
- # rows = table[1..50] # => #<CSV::Row "Name":"bar" "Value":"1">
- # rows # => [#<CSV::Row "Name":"bar" "Value":"1">, #<CSV::Row "Name":"baz" "Value":"2">]
- #
- # Special case: if <tt>range.start == table.size</tt>, returns an empty \Array:
- # table[table.size..50] # => []
- #
- # If <tt>range.end</tt> is negative, calculates the ending index from the end:
- # rows = table[0..-1]
- # rows # => [#<CSV::Row "Name":"foo" "Value":"0">, #<CSV::Row "Name":"bar" "Value":"1">, #<CSV::Row "Name":"baz" "Value":"2">]
- #
- # If <tt>range.start</tt> is negative, calculates the starting index from the end:
- # rows = table[-1..2]
- # rows # => [#<CSV::Row "Name":"baz" "Value":"2">]
- #
- # If <tt>range.start</tt> is larger than <tt>table.size</tt>, returns +nil+:
- # table[4..4] # => nil
- #
- # ---
- #
- # The expression <tt>table[header]</tt>, where +header+ is a \String,
- # returns column values (\Array of \Strings) if the column exists
- # and if the access mode is <tt>:col</tt> or <tt>:col_or_row</tt>:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # table.by_col! # => #<CSV::Table mode:col row_count:4>
- # table['Name'] # => ["foo", "bar", "baz"]
- # table.by_col_or_row! # => #<CSV::Table mode:col_or_row row_count:4>
- # col = table['Name']
- # col # => ["foo", "bar", "baz"]
- #
- # Modifying the returned column values does not modify the table:
- # col[0] = 'bat'
- # col # => ["bat", "bar", "baz"]
- # table['Name'] # => ["foo", "bar", "baz"]
- #
- # Returns an \Array of +nil+ values if there is no such column:
- # table['Nosuch'] # => [nil, nil, nil]
- 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
-
- # :call-seq:
- # table.values_at(*indexes) -> array_of_rows
- # table.values_at(*headers) -> array_of_columns_data
- #
- # If the access mode is <tt>:row</tt> or <tt>:col_or_row</tt>,
- # and each argument is either an \Integer or a \Range,
- # returns rows.
- # Otherwise, returns columns data.
- #
- # In either case, the returned values are in the order
- # specified by the arguments. Arguments may be repeated.
- #
- # ---
- #
- # Returns rows as an \Array of \CSV::Row objects.
- #
- # No argument:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # table.values_at # => []
- #
- # One index:
- # values = table.values_at(0)
- # values # => [#<CSV::Row "Name":"foo" "Value":"0">]
- #
- # Two indexes:
- # values = table.values_at(2, 0)
- # values # => [#<CSV::Row "Name":"baz" "Value":"2">, #<CSV::Row "Name":"foo" "Value":"0">]
- #
- # One \Range:
- # values = table.values_at(1..2)
- # values # => [#<CSV::Row "Name":"bar" "Value":"1">, #<CSV::Row "Name":"baz" "Value":"2">]
- #
- # \Ranges and indexes:
- # values = table.values_at(0..1, 1..2, 0, 2)
- # pp values
- # Output:
- # [#<CSV::Row "Name":"foo" "Value":"0">,
- # #<CSV::Row "Name":"bar" "Value":"1">,
- # #<CSV::Row "Name":"bar" "Value":"1">,
- # #<CSV::Row "Name":"baz" "Value":"2">,
- # #<CSV::Row "Name":"foo" "Value":"0">,
- # #<CSV::Row "Name":"baz" "Value":"2">]
- #
- # ---
- #
- # Returns columns data as row Arrays,
- # each consisting of the specified columns data for that row:
- # values = table.values_at('Name')
- # values # => [["foo"], ["bar"], ["baz"]]
- # values = table.values_at('Value', 'Name')
- # values # => [["0", "foo"], ["1", "bar"], ["2", "baz"]]
- 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
-
- # :call-seq:
- # table << row_or_array -> self
- #
- # If +row_or_array+ is a \CSV::Row object,
- # it is appended to the table:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # table << CSV::Row.new(table.headers, ['bat', 3])
- # table[3] # => #<CSV::Row "Name":"bat" "Value":3>
- #
- # If +row_or_array+ is an \Array, it is used to create a new
- # \CSV::Row object which is then appended to the table:
- # table << ['bam', 4]
- # table[4] # => #<CSV::Row "Name":"bam" "Value":4>
- 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
-
- #
- # :call-seq:
- # table.push(*rows_or_arrays) -> self
- #
- # A shortcut for appending multiple rows. Equivalent to:
- # rows.each {|row| self << row }
- #
- # Each argument may be either a \CSV::Row object or an \Array:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # rows = [
- # CSV::Row.new(table.headers, ['bat', 3]),
- # ['bam', 4]
- # ]
- # table.push(*rows)
- # table[3..4] # => [#<CSV::Row "Name":"bat" "Value":3>, #<CSV::Row "Name":"bam" "Value":4>]
- def push(*rows)
- rows.each { |row| self << row }
-
- self # for chaining
- end
-
- # :call-seq:
- # table.delete(*indexes) -> deleted_values
- # table.delete(*headers) -> deleted_values
- #
- # If the access mode is <tt>:row</tt> or <tt>:col_or_row</tt>,
- # and each argument is either an \Integer or a \Range,
- # returns deleted rows.
- # Otherwise, returns deleted columns data.
- #
- # In either case, the returned values are in the order
- # specified by the arguments. Arguments may be repeated.
- #
- # ---
- #
- # Returns rows as an \Array of \CSV::Row objects.
- #
- # One index:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # deleted_values = table.delete(0)
- # deleted_values # => [#<CSV::Row "Name":"foo" "Value":"0">]
- #
- # Two indexes:
- # table = CSV.parse(source, headers: true)
- # deleted_values = table.delete(2, 0)
- # deleted_values # => [#<CSV::Row "Name":"baz" "Value":"2">, #<CSV::Row "Name":"foo" "Value":"0">]
- #
- # ---
- #
- # Returns columns data as column Arrays.
- #
- # One header:
- # table = CSV.parse(source, headers: true)
- # deleted_values = table.delete('Name')
- # deleted_values # => ["foo", "bar", "baz"]
- #
- # Two headers:
- # table = CSV.parse(source, headers: true)
- # deleted_values = table.delete('Value', 'Name')
- # deleted_values # => [["0", "1", "2"], ["foo", "bar", "baz"]]
- 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 rows or columns for which the block returns a truthy value;
- # returns +self+.
- #
- # Removes rows when the access mode is <tt>:row</tt> or <tt>:col_or_row</tt>;
- # calls the block with each \CSV::Row object:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # table.by_row! # => #<CSV::Table mode:row row_count:4>
- # table.size # => 3
- # table.delete_if {|row| row['Name'].start_with?('b') }
- # table.size # => 1
- #
- # Removes columns when the access mode is <tt>:col</tt>;
- # calls the block with each column as a 2-element array
- # containing the header and an \Array of column fields:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # table.by_col! # => #<CSV::Table mode:col row_count:4>
- # table.headers.size # => 2
- # table.delete_if {|column_data| column_data[1].include?('2') }
- # table.headers.size # => 1
- #
- # Returns a new \Enumerator if no block is given:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # table.delete_if # => #<Enumerator: #<CSV::Table mode:col_or_row row_count:4>:delete_if>
- 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
-
- # Calls the block with each row or column; returns +self+.
- #
- # When the access mode is <tt>:row</tt> or <tt>:col_or_row</tt>,
- # calls the block with each \CSV::Row object:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # table.by_row! # => #<CSV::Table mode:row row_count:4>
- # table.each {|row| p row }
- # Output:
- # #<CSV::Row "Name":"foo" "Value":"0">
- # #<CSV::Row "Name":"bar" "Value":"1">
- # #<CSV::Row "Name":"baz" "Value":"2">
- #
- # When the access mode is <tt>:col</tt>,
- # calls the block with each column as a 2-element array
- # containing the header and an \Array of column fields:
- # table.by_col! # => #<CSV::Table mode:col row_count:4>
- # table.each {|column_data| p column_data }
- # Output:
- # ["Name", ["foo", "bar", "baz"]]
- # ["Value", ["0", "1", "2"]]
- #
- # Returns a new \Enumerator if no block is given:
- # table.each # => #<Enumerator: #<CSV::Table mode:col row_count:4>:each>
- 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 each row of +self+ <tt>==</tt>
- # the corresponding row of +other_table+, otherwise, +false+.
- #
- # The access mode does no affect the result.
- #
- # Equal tables:
- # source = "Name,Value\nfoo,0\nbar,1\nbaz,2\n"
- # table = CSV.parse(source, headers: true)
- # other_table = CSV.parse(source, headers: true)
- # table == other_table # => true
- #
- # Different row count:
- # other_table.delete(2)
- # table == other_table # => false
- #
- # Different last row:
- # other_table << ['bat', 3]
- # table == other_table # => false
- 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 d1d0dc0e02..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.2.2"
-end
diff --git a/lib/csv/writer.rb b/lib/csv/writer.rb
deleted file mode 100644
index 4a9a35c5af..0000000000
--- a/lib/csv/writer.rb
+++ /dev/null
@@ -1,210 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "input_record_separator"
-require_relative "match_p"
-require_relative "row"
-
-using CSV::MatchP if CSV.const_defined?(:MatchP)
-
-class CSV
- # Note: Don't use this class directly. This is an internal class.
- class Writer
- #
- # 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.
- #
- 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
-
- #
- # Adds a new row
- #
- 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
-
- i = -1
- converted_row = row.collect do |field|
- i += 1
- quote(field, i)
- end
- line = converted_row.join(@column_separator) + @row_separator
- if @output_encoding
- line = line.encode(@output_encoding)
- end
- @output << line
-
- self
- end
-
- #
- # Winds back to the beginning
- #
- 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_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
-
- def prepare_format
- @column_separator = @options[:column_separator].to_s.encode(@encoding)
- row_separator = @options[:row_separator]
- if row_separator == :auto
- @row_separator = InputRecordSeparator.value.encode(@encoding)
- else
- @row_separator = row_separator.to_s.encode(@encoding)
- end
- @quote_character = @options[:quote_character]
- 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
- 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, i)
- if @force_quotes
- quote_field(field)
- elsif @force_quotes_fields and @force_quotes_fields[i]
- 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 (field.valid_encoding? and @quotable_pattern.match?(field))
- quote_field(field)
- else
- field # unquoted field
- end
- end
- end
- end
- end
-end