diff --git a/doc/csv/recipes/parsing.rdoc b/doc/csv/recipes/parsing.rdoc
index 63673072..f9571161 100644
--- a/doc/csv/recipes/parsing.rdoc
+++ b/doc/csv/recipes/parsing.rdoc
@@ -61,6 +61,8 @@ All code snippets on this page assume that the following has been executed:
- {Using Multiple Header Converters}[#label-Using+Multiple+Header+Converters]
- {Recipe: Specify Multiple Header Converters in Option :header_converters}[#label-Recipe-3A+Specify+Multiple+Header+Converters+in+Option+-3Aheader_converters]
- {Recipe: Specify Multiple Header Converters in a Custom Header Converter List}[#label-Recipe-3A+Specify+Multiple+Header+Converters+in+a+Custom+Header+Converter+List]
+- {Pattern Matching}[#label-Pattern+Matching]
+ - {Recipe: Match Parsed Rows by Header or Position}[#label-Recipe-3A+Match+Parsed+Rows+by+Header+or+Position]
- {Diagnostics}[#label-Diagnostics]
- {Recipe: Capture Unconverted Fields}[#label-Recipe-3A+Capture+Unconverted+Fields]
- {Recipe: Capture Field Info}[#label-Recipe-3A+Capture+Field+Info]
@@ -522,6 +524,45 @@ Apply multiple header converters by defining and registering a custom header con
parsed = CSV.parse(source, headers: true, header_converters: :my_header_converters)
parsed.headers # => [:name, :value]
+=== Pattern Matching
+
+==== Recipe: Match Parsed Rows by Header or Position
+
+This example requires Ruby 3.0 or later.
+Hash patterns use symbol keys. The built-in +:symbol+ header converter
+also downcases headers and replaces spaces with underscores:
+
+ source = <<~CSV
+ First Name,Department,Salary
+ Bob,Engineering,1000
+ Jane,Sales,2000
+ John,Management,5000
+ CSV
+ data = CSV.parse(source, headers: true, header_converters: :symbol)
+ data.headers # => [:first_name, :department, :salary]
+ selected = data.select { _1 in first_name: /^J/ }
+ selected.map(&:fields)
+ # => [["Jane", "Sales", "2000"], ["John", "Management", "5000"]]
+
+Headers are strings by default. So a symbol key doesn't match:
+
+ data = CSV.parse(source, headers: true)
+ data.headers # => ["First Name", "Department", "Salary"]
+ data.select { _1 in first_name: /^J/ } # => []
+
+To keep the original case, use a custom converter that calls +to_sym+:
+
+ case_sensitive = CSV.parse("Name\nJane\n", headers: true,
+ header_converters: ->(header) { header.to_sym })
+ case_sensitive.headers # => [:Name]
+ case_sensitive.first in Name: /^J/ # => true
+
+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"]]
+
=== Diagnostics
==== Recipe: Capture Unconverted Fields
diff --git a/lib/csv/row.rb b/lib/csv/row.rb
index 40988001..414f59f5 100644
--- a/lib/csv/row.rb
+++ b/lib/csv/row.rb
@@ -688,6 +688,10 @@ def to_h
#
# Returns the new \Hash suitable for pattern matching containing only the
# keys specified as an argument.
+ # Hash pattern keys are symbols. The +:symbol+ header converter downcases
+ # headers and replaces spaces with underscores: "First Name"
+ # becomes +:first_name+. To keep the original case, use
+ # header_converters: ->(header) { header.to_sym } instead.
def deconstruct_keys(keys)
if keys.nil?
to_h