diff --git a/lib/csv.rb b/lib/csv.rb index ebb6d67968..dca2a45b6a 100644 --- a/lib/csv.rb +++ b/lib/csv.rb @@ -141,7 +141,7 @@ end # There are several specialized class methods for one-statement reading or writing, # described in the Specialized Methods section. # -# If a String passed into ::new, it is internally wrapped into a StringIO object. +# If a String is passed into ::new, it is internally wrapped into a StringIO object. # # +options+ can be used for specifying the particular CSV flavor (column # separators, row separators, value quoting and so on), and for data conversion, @@ -890,8 +890,12 @@ class CSV # attempt to parse input not conformant # with RFC 4180, such as double quotes # in unquoted fields. - # :nil_value:: TODO: WRITE ME. - # :empty_value:: TODO: WRITE ME. + # :nil_value:: When set an object, any values of an + # empty field are replaced by the set + # object, not nil. + # :empty_value:: When set an object, any values of a + # blank string field is replaced by + # the set object. # # See CSV::DEFAULT_OPTIONS for the default settings. # @@ -1232,7 +1236,7 @@ class CSV elsif @unconverted_fields return add_unconverted_fields(Array.new, Array.new) elsif @use_headers - return self.class::Row.new(Array.new, Array.new) + return self.class::Row.new(@headers, Array.new) else return Array.new end diff --git a/lib/csv/csv.gemspec b/lib/csv/csv.gemspec index 0169c173d6..9d0ccc15af 100644 --- a/lib/csv/csv.gemspec +++ b/lib/csv/csv.gemspec @@ -1,6 +1,10 @@ # frozen_string_literal: true -require_relative "version" +begin + require_relative "lib/csv/version" +rescue LoadError + require_relative "version" +end Gem::Specification.new do |spec| spec.name = "csv" @@ -13,7 +17,8 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/ruby/csv" spec.license = "BSD-2-Clause" - spec.files = ["lib/csv.rb", "lib/csv/table.rb", "lib/csv/core_ext/string.rb", "lib/csv/core_ext/array.rb", "lib/csv/row.rb", "lib/csv/version.rb", "README.md", "LICENSE.txt", "news.md"] + spec.files = Dir.glob("lib/**/*.rb") + spec.files += ["README.md", "LICENSE.txt", "news.md"] spec.require_paths = ["lib"] spec.required_ruby_version = ">= 2.3.0" diff --git a/lib/csv/row.rb b/lib/csv/row.rb index 8ff3480ae8..31eab2d0a4 100644 --- a/lib/csv/row.rb +++ b/lib/csv/row.rb @@ -48,6 +48,11 @@ class CSV 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 diff --git a/lib/csv/table.rb b/lib/csv/table.rb index 17a7c542e4..e9f3366a4a 100644 --- a/lib/csv/table.rb +++ b/lib/csv/table.rb @@ -375,4 +375,4 @@ class CSV "#<#{self.class} mode:#{@mode} row_count:#{to_a.size}>".encode("US-ASCII") end end -end +end \ No newline at end of file diff --git a/lib/csv/version.rb b/lib/csv/version.rb index 35adea3a92..d62a093418 100644 --- a/lib/csv/version.rb +++ b/lib/csv/version.rb @@ -2,5 +2,5 @@ class CSV # The version of the installed library. - VERSION = "1.0.2" + VERSION = "3.0.1" end diff --git a/test/csv/test_csv_parsing.rb b/test/csv/test_csv_parsing.rb index ab8e97f4bb..e65bbad92e 100755 --- a/test/csv/test_csv_parsing.rb +++ b/test/csv/test_csv_parsing.rb @@ -148,13 +148,13 @@ class TestCSV::Parsing < TestCSV CSV.parse_line("1,2\r,3", row_sep: "\n") end - bad_data = <<-END_DATA.gsub(/^ +/, "") - line,1,abc - line,2,"def\nghi" + bad_data = <<-CSV +line,1,abc +line,2,"def\nghi" - line,4,some\rjunk - line,5,jkl - END_DATA +line,4,some\rjunk +line,5,jkl + CSV lines = bad_data.lines.to_a assert_equal(6, lines.size) assert_match(/\Aline,4/, lines.find { |l| l =~ /some\rjunk/ }) @@ -172,13 +172,13 @@ class TestCSV::Parsing < TestCSV assert_raise(CSV::MalformedCSVError) { CSV.parse_line('1,2,"3...') } - bad_data = <<-END_DATA.gsub(/^ +/, "") - line,1,abc - line,2,"def\nghi" + bad_data = <<-CSV +line,1,abc +line,2,"def\nghi" - line,4,8'10" - line,5,jkl - END_DATA +line,4,8'10" +line,5,jkl + CSV lines = bad_data.lines.to_a assert_equal(6, lines.size) assert_match(/\Aline,4/, lines.find { |l| l =~ /8'10"/ }) diff --git a/test/csv/test_data_converters.rb b/test/csv/test_data_converters.rb index 04970cd461..114049d66f 100755 --- a/test/csv/test_data_converters.rb +++ b/test/csv/test_data_converters.rb @@ -260,10 +260,10 @@ class TestCSV::DataConverters < TestCSV assert_equal(unconverted, row.unconverted_fields) end - data = <<-END_CSV.gsub(/^\s+/, "") - first,second,third - 1,2,3 - END_CSV + data = <<-CSV +first,second,third +1,2,3 + CSV row = nil assert_nothing_raised(Exception) do row = CSV.parse_line( data, diff --git a/test/csv/test_features.rb b/test/csv/test_features.rb index a851c908f0..45d937e037 100755 --- a/test/csv/test_features.rb +++ b/test/csv/test_features.rb @@ -37,12 +37,12 @@ class TestCSV::Features < TestCSV def setup super - @sample_data = <<-END_DATA.gsub(/^ +/, "") - line,1,abc - line,2,"def\nghi" + @sample_data = <<-CSV +line,1,abc +line,2,"def\nghi" - line,4,jkl - END_DATA +line,4,jkl + CSV @csv = CSV.new(@sample_data) end @@ -225,12 +225,12 @@ class TestCSV::Features < TestCSV end # reported by Kev Jackson - def test_failing_to_escape_col_sep_bug_fix + def test_failing_to_escape_col_sep assert_nothing_raised(Exception) { CSV.new(String.new, col_sep: "|") } end # reported by Chris Roos - def test_failing_to_reset_headers_in_rewind_bug_fix + def test_failing_to_reset_headers_in_rewind csv = CSV.new("forename,surname", headers: true, return_headers: true) csv.each {|row| assert_predicate row, :header_row?} csv.rewind @@ -238,16 +238,16 @@ class TestCSV::Features < TestCSV end # reported by Dave Burt - def test_leading_empty_fields_with_multibyte_col_sep_bug_fix - data = <<-END_DATA.gsub(/^\s+/, "") - <=><=>A<=>B<=>C - 1<=>2<=>3 - END_DATA + def test_leading_empty_fields_with_multibyte_col_sep + data = <<-CSV +<=><=>A<=>B<=>C +1<=>2<=>3 + CSV parsed = CSV.parse(data, col_sep: "<=>") assert_equal([[nil, nil, "A", "B", "C"], ["1", "2", "3"]], parsed) end - def test_gzip_reader_bug_fix + def test_gzip_reader zipped = nil assert_nothing_raised(NoMethodError) do zipped = CSV.new( @@ -261,7 +261,7 @@ class TestCSV::Features < TestCSV zipped.close end if defined?(Zlib::GzipReader) - def test_gzip_writer_bug_fix + def test_gzip_writer Tempfile.create(%w"temp .gz") {|tempfile| tempfile.close file = tempfile.path diff --git a/test/csv/test_headers.rb b/test/csv/test_headers.rb index 94f4359671..3ebb5cfc85 100755 --- a/test/csv/test_headers.rb +++ b/test/csv/test_headers.rb @@ -13,11 +13,11 @@ class TestCSV::Headers < TestCSV def setup super - @data = <<-END_CSV.gsub(/^\s+/, "") - first,second,third - A,B,C - 1,2,3 - END_CSV + @data = <<-CSV +first,second,third +A,B,C +1,2,3 + CSV end def test_first_row @@ -183,10 +183,10 @@ class TestCSV::Headers < TestCSV def test_converters # create test data where headers and fields look alike - data = <<-END_MATCHING_CSV.gsub(/^\s+/, "") - 1,2,3 - 1,2,3 - END_MATCHING_CSV + data = <<-CSV +1,2,3 +1,2,3 + CSV # normal converters do not affect headers csv = CSV.parse( data, headers: true, @@ -256,16 +256,16 @@ class TestCSV::Headers < TestCSV end def test_skip_blanks - @data = <<-END_CSV.gsub(/^ +/, "") + @data = <<-CSV - A,B,C +A,B,C - 1,2,3 +1,2,3 - END_CSV + CSV expected = [%w[1 2 3]] CSV.parse(@data, headers: true, skip_blanks: true) do |row| @@ -292,7 +292,7 @@ class TestCSV::Headers < TestCSV assert_equal(%w[first second third], csv.headers) # after headers are read end - def test_blank_row_bug_fix + def test_blank_row @data += "\n#{@data}" # add a blank row # ensure that everything returned is a Row object @@ -300,4 +300,19 @@ class TestCSV::Headers < TestCSV assert_instance_of(CSV::Row, row) end end + + def test_nil_row_header + @data = <<-CSV +A + +1 + CSV + + csv = CSV.parse(@data, headers: true) + + # ensure nil row creates Row object with headers + row = csv[0] + assert_equal([["A"], [nil]], + [row.headers, row.fields]) + end end diff --git a/test/csv/test_row.rb b/test/csv/test_row.rb index 23df4d4fe6..67ed65c0db 100755 --- a/test/csv/test_row.rb +++ b/test/csv/test_row.rb @@ -419,4 +419,12 @@ class TestCSV::Row < TestCSV row.dig("A", 0) end end + + def test_dup + row = CSV::Row.new(["A"], ["foo"]) + dupped_row = row.dup + dupped_row.delete("A") + assert_equal(["foo", nil], + [row["A"], dupped_row["A"]]) + end end diff --git a/test/csv/test_table.rb b/test/csv/test_table.rb index 34ea2c5c6b..d99b7d2932 100755 --- a/test/csv/test_table.rb +++ b/test/csv/test_table.rb @@ -42,6 +42,11 @@ class TestCSV::Table < TestCSV assert_equal(:row, rows.mode) assert_equal(@table, rows) + col_or_row = rows.by_col_or_row + assert_equal(:row, rows.mode) + assert_equal(:col_or_row, col_or_row.mode) + assert_equal(@table, col_or_row) + # destructive mode changing calls assert_equal(@table, @table.by_row!) assert_equal(:row, @table.mode) @@ -148,13 +153,13 @@ class TestCSV::Table < TestCSV @table.to_a ) # verify resulting table - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - A,B,C,Type,Index - 1,100,3,data,1 - 4,200,6,data,2 - 10,,12,data,3 - 13,,15,data, - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +A,B,C,Type,Index +1,100,3,data,1 +4,200,6,data,2 +10,,12,data,3 +13,,15,data, + CSV # with headers @header_table["Type"] = "data" @@ -286,12 +291,12 @@ class TestCSV::Table < TestCSV end def test_to_csv - csv = <<-END_CSV.gsub(/^\s+/, "") - A,B,C - 1,2,3 - 4,5,6 - 7,8,9 - END_CSV + csv = <<-CSV +A,B,C +1,2,3 +4,5,6 +7,8,9 + CSV # normal conversion assert_equal(csv, @table.to_csv) @@ -330,11 +335,11 @@ class TestCSV::Table < TestCSV assert_equal(@rows.map { |row| row["A"] }, @table.delete("A")) # verify resulting table - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - B,C - 2,3 - 8,9 - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +B,C +2,3 +8,9 + CSV end def test_delete_mixed_multiple @@ -352,11 +357,11 @@ class TestCSV::Table < TestCSV @table.delete(1, "A")) # verify resulting table - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - B,C - 2,3 - 8,9 - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +B,C +2,3 +8,9 + CSV end def test_delete_column @@ -369,12 +374,12 @@ class TestCSV::Table < TestCSV assert_equal(@rows.map { |row| row["C"] }, @table.delete("C")) # verify resulting table - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - B - 2 - 5 - 8 - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +B +2 +5 +8 + CSV end def test_delete_row @@ -387,11 +392,11 @@ class TestCSV::Table < TestCSV assert_raise(TypeError) { @table.delete("C") } # verify resulting table - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - A,B,C - 1,2,3 - 7,8,9 - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +A,B,C +1,2,3 +7,8,9 + CSV end def test_delete_with_blank_rows @@ -408,10 +413,10 @@ class TestCSV::Table < TestCSV assert_equal(@table, @table.delete_if { |row| (row["B"] % 2).zero? }) # verify resulting table - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - A,B,C - 4,5,6 - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +A,B,C +4,5,6 + CSV end def test_delete_if_row_without_block @@ -426,10 +431,10 @@ class TestCSV::Table < TestCSV assert_equal(@table, enum.each { |row| (row["B"] % 2).zero? }) # verify resulting table - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - A,B,C - 4,5,6 - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +A,B,C +4,5,6 + CSV end def test_delete_if_column @@ -439,12 +444,12 @@ class TestCSV::Table < TestCSV @table.by_col! assert_equal(@table, @table.delete_if { |h, v| h > "A" }) - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - A - 1 - 4 - 7 - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +A +1 +4 +7 + CSV end def test_delete_if_column_without_block @@ -458,12 +463,12 @@ class TestCSV::Table < TestCSV assert_equal(@table.headers.size, enum.size) assert_equal(@table, enum.each { |h, v| h > "A" }) - assert_equal(<<-END_RESULT.gsub(/^\s+/, ""), @table.to_csv) - A - 1 - 4 - 7 - END_RESULT + assert_equal(<<-CSV, @table.to_csv) +A +1 +4 +7 + CSV end def test_values_at