From b0df9540f564b28cb53e97e2c020ef7a750df586 Mon Sep 17 00:00:00 2001
From: Brad <1676751+Sinkleberg@users.noreply.github.com>
Date: Fri, 6 Nov 2026 01:05:11 -0500
Subject: [PATCH] Document pattern matching on parsed CSV rows
---
doc/csv/recipes/filtering.rdoc | 33 +++++++++++++++++++++
lib/csv/row.rb | 2 ++
test/csv/test_patterns.rb | 52 ++++++++++++++++++++++++++++++++++
3 files changed, 87 insertions(+)
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