skip to content

In Ruby, how do grep and grep_v filter an invoice list by class, range or regexp, and what does grep with a block return?

level: middleimportance: should knowfreq 32%

answer

  1. select driven by a pattern
  2. pattern === element
  3. grep_v keeps the non-matches
  4. block maps only the matches
  5. grep_v(nil) drops nils

basics

~20 s

grep(pattern) returns the elements for which pattern === element, and grep_v returns the rest. The pattern can be a class, range, regexp or plain value. With a block, grep returns the block's results for the matching elements, filtering and mapping in one pass.

solid answer

~30 s

`grep(pattern)` is a `select` whose test is `pattern === element`, so the pattern's class decides the meaning: `refs.grep(/\AINV-/)` matches text, `amounts.grep(10_000..)` checks a range, `rows.grep(Invoice)` keeps instances of a class, `statuses.grep(:overdue)` compares values. `grep_v` inverts it, like `reject`; `rows.grep_v(nil)` drops nils. Both always return a new `Array`. With a block, `grep` calls the block only on the matches and returns the block's results, so `refs.grep(/\AINV-(\d+)/) { $1.to_i }` filters and converts in one pass. `grep_v` accepts a block in the same way. Reach for `grep` when the test is a pattern; use `select` when it needs arbitrary logic.

code

ruby · 11 lines
ruby
refs    = %w[INV-7 CRN-2 INV-19 DRAFT-3]
amounts = [250, 12_400, 980, 31_000]
mixed   = [120, nil, "n/a", 80.5]

refs.grep(/\AINV-/)                  # => ["INV-7", "INV-19"]
refs.grep_v(/\AINV-/)                # => ["CRN-2", "DRAFT-3"]
refs.grep(/\AINV-(\d+)/) { $1.to_i } # => [7, 19]
amounts.grep(10_000..)               # => [12400, 31000]
mixed.grep(Numeric)                  # => [120, 80.5]
mixed.grep_v(nil)                    # => [120, "n/a", 80.5]
refs.grep("INV")                     # => [], String#=== is equality

go deeper

for a junior

Recall that grep keeps elements matching a pattern such as a regexp and grep_v keeps the others.

for a middle

Explain that grep tests pattern === element, so classes, ranges and values work, and that the block form returns mapped matches.

for a senior

Choose grep for pattern tests and select for logic in review, and watch for String patterns used as if they were substring searches.

for a principal

Favour idioms that state intent, such as grep(Invoice) for type filtering, and agree on when a pattern reads better than a block.

## What `grep` is The name comes from the Unix text-search tool, but Ruby's `Enumerable#grep` is more general than text search. It takes one **pattern** argument and returns a new **array** of every element for which `pattern === element` is truthy. In other words, it is a `select` whose block is fixed to a `===` test. `grep_v` is the inverse, named after `grep -v`: it returns the elements for which `pattern === element` is falsy, the way `reject` inverts `select`. ## What the pattern can be Because the test is `===`, the kind of object you pass decides what "matches" means. How each class implements `===` is a topic of its own; the practical menu for filtering is: | Pattern | Example | Keeps | |---|---|---| | Regexp | `refs.grep(/\AINV-/)` | strings matching the regexp | | Range | `amounts.grep(10_000..)` | values inside the range | | Class or module | `rows.grep(Invoice)` | instances of that class | | Plain value | `statuses.grep(:overdue)` | elements equal to the value | | `nil` with `grep_v` | `values.grep_v(nil)` | everything except `nil` | For an accounts-receivable list this covers most day-to-day filtering without writing a block: the invoice references for this year, the amounts over a threshold, or the rows that really are invoices in a mixed import. ## `grep` with a block When you give `grep` a block, it changes from a filter into a **filter-then-map**: 1. Each element is tested with `pattern === element`. 2. For each match, the block is called with that element. 3. The result array holds the **block's return values**, not the elements. So `%w[INV-7 CRN-2 INV-9].grep(/\AINV/) { |ref| ref.downcase }` returns `["inv-7", "inv-9"]`. Non-matching elements never reach the block. `grep_v` with a block behaves the same way on the non-matches. With a regexp pattern, the block can also use the match globals such as `$1` from that match, which makes extraction concise: `refs.grep(/\AINV-(\d+)/) { $1.to_i }` returns the invoice numbers as integers. ## `grep` versus `select` The two overlap, and choosing well is a readability decision: - Use **`grep`** when the test *is* a pattern: a class, a range, a regexp or a literal value. `amounts.grep(10_000..)` reads better than `amounts.select { |a| (10_000..).cover?(a) }`. - Use **`select`** when the test needs any other logic: several fields, method calls, or a comparison that is not a `===` match. - `select` takes **no argument**; passing a pattern to it raises `ArgumentError`. `grep` requires exactly one argument and takes an optional block. ## Things that surprise people - `grep` always returns an **array**, even on a `Hash`. Each hash element is a `[key, value]` pair, so the pattern is tested against the pair, and a regexp will not match it. - A **string** pattern uses `String#===`, which is plain equality, not substring search: `refs.grep("INV")` keeps only elements equal to `"INV"`. - `grep_v(nil)` removes `nil` elements, the same result `compact` gives on arrays, but it also works on any `Enumerable` and composes with other patterns, e.g. `grep_v(0)` to drop zeros. ## Symbols and other receivers The fast path for a regexp pattern also accepts **symbols**: a regexp tests a symbol's name, so `%i[overdue paid disputed].grep(/due/)` returns `[:overdue]`. Elements that are neither strings nor symbols simply do not match; `grep` does not raise on them. That makes `grep(/.../)` safe on a mixed array where `select { |e| e.match?(/.../) }` would raise `NoMethodError` on an Integer. `grep` works on any `Enumerable`, not just arrays: - `(1..100).grep(10..20)` returns the integers in the inner range. - `File.foreach(path).grep(/OVERDUE/)` reads a file line by line and returns the matching lines as an array. In each case the result is a plain array, built in a single pass. ## What to say in an interview Define `grep` as select-by-`===`, list the pattern kinds with one example each, and point out that the block form maps the matches. The string-pattern trap and the hash-pair trap are good extras that show real use.

  • Why does refs.grep("INV") return an empty array for references like INV-7?
    A String pattern is tested with `String#===`, which is equality, not substring search. No element equals exactly `"INV"`, so nothing matches. For a prefix or substring test, pass a regexp such as `/\AINV/`, or use `select` with `start_with?`.
  • What does grep return when called on a Hash?
    An array. `Hash` gets `grep` from `Enumerable`, whose elements are `[key, value]` pairs, so the pattern is tested against each pair and matching pairs are returned in an array. A regexp does not match an array, so filtering a hash by key or value usually needs `select` or a pattern built for pairs.

saying these in an interview costs you the question

  • grep only works on strings and regular expressions
  • grep with a block returns the matching elements unchanged
  • grep("INV") keeps every string containing INV
  • select(Integer) filters by class the same way grep(Integer) does
  • grep_v with a block ignores the block