Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions doc/csv/recipes/filtering.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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 <tt>header_converters: :symbol</tt>:

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.
Expand Down
2 changes: 2 additions & 0 deletions lib/csv/row.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
# <tt>header_converters: :symbol</tt> when parsing the \CSV data.
def deconstruct_keys(keys)
if keys.nil?
to_h
Expand Down
52 changes: 52 additions & 0 deletions test/csv/test_patterns.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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