diff --git a/doc/csv/recipes/filtering.rdoc b/doc/csv/recipes/filtering.rdoc index d92afb72..af0a4bc0 100644 --- a/doc/csv/recipes/filtering.rdoc +++ b/doc/csv/recipes/filtering.rdoc @@ -9,6 +9,7 @@ All code snippets on this page assume that the following has been executed: === Contents +- {Filtering Rows with Pattern Matching}[#label-Filtering+Rows+with+Pattern+Matching] - {Source and Output Formats}[#label-Source+and+Output+Formats] - {Filtering String to String}[#label-Filtering+String+to+String] - {Recipe: Filter String to String parsing Headers}[#label-Recipe-3A+Filter+String+to+String+parsing+Headers] @@ -27,6 +28,38 @@ All code snippets on this page assume that the following has been executed: - {Recipe: Filter IO Stream to IO Stream parsing and writing Headers}[#label-Recipe-3A+Filter+IO+Stream+to+IO+Stream+parsing+and+writing+Headers] - {Recipe: Filter IO Stream to IO Stream Without Headers}[#label-Recipe-3A+Filter+IO+Stream+to+IO+Stream+Without+Headers] +=== Filtering Rows with Pattern Matching + +This example uses Ruby 3.0 or later, where +in+ returns +true+ or +false+. +Hash patterns use symbol keys. When parsing known headers for a hash pattern, +use header_converters: :symbol: + + source = <<~CSV + Name,Department,Salary + Bob,Engineering,1000 + Jane,Sales,2000 + John,Management,5000 + CSV + data = CSV.parse(source, headers: true, header_converters: :symbol) + data.headers # => [:name, :department, :salary] + selected = data.select { _1 in name: /^J/ } + selected.map(&:fields) + # => [["Jane", "Sales", "2000"], ["John", "Management", "5000"]] + +Header conversion is opt-in: CSV::Row preserves the header key types supplied +by the parser. Without conversion, these headers remain strings and a +following symbol-key pattern matches no rows: + + data = CSV.parse(source, headers: true) + data.headers # => ["Name", "Department", "Salary"] + data.select { _1 in Name: /^J/ } # => [] + +An array pattern matches the row's fields independently of header key types: + + selected = data.select { _1 in [/^J/, _, _] } + selected.map(&:fields) + # => [["Jane", "Sales", "2000"], ["John", "Management", "5000"]] + === Source and Output Formats You can use a Unix-style "filter" for \CSV data. diff --git a/lib/csv/row.rb b/lib/csv/row.rb index 40988001..45b75cb0 100644 --- a/lib/csv/row.rb +++ b/lib/csv/row.rb @@ -688,6 +688,8 @@ def to_h # # Returns the new \Hash suitable for pattern matching containing only the # keys specified as an argument. + # Hash pattern keys are symbols. To match parsed headers, explicitly use + # header_converters: :symbol when parsing the \CSV data. def deconstruct_keys(keys) if keys.nil? to_h diff --git a/test/csv/test_patterns.rb b/test/csv/test_patterns.rb index 881f03a3..08de81ec 100644 --- a/test/csv/test_patterns.rb +++ b/test/csv/test_patterns.rb @@ -18,10 +18,62 @@ def test_hash_rest end end + def test_hash_with_converted_headers + data = CSV.parse(csv_source, headers: true, header_converters: :symbol) + selected = data.select do |row| + case row + in name: /^J/ + true + else + false + end + end + + assert_equal([:name, :department, :salary], data.headers) + assert_equal([["Jane", "Sales", "2000"], ["John", "Management", "5000"]], + selected.map(&:fields)) + end + + def test_patterns_with_string_headers + data = CSV.parse(csv_source, headers: true) + selected = data.select do |row| + case row + in Name: /^J/ + true + else + false + end + end + assert_empty(selected) + assert_equal(["Name", "Department", "Salary"], data.headers) + + selected = data.select do |row| + case row + in [/^J/, _, _] + true + else + false + end + end + assert_equal([["Jane", "Sales", "2000"], ["John", "Management", "5000"]], + selected.map(&:fields)) + end + def test_array case CSV::Row.new(%i{A B C}, [1, 2, 3]) in *, matched assert_equal(3, matched) end end + + private + + def csv_source + <<~CSV + Name,Department,Salary + Bob,Engineering,1000 + Jane,Sales,2000 + John,Management,5000 + CSV + end end