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?
answer
- select driven by a pattern
- pattern === element
- grep_v keeps the non-matches
- block maps only the matches
- grep_v(nil) drops nils
basics
~20 sgrep(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 linesrefs = %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 equalitygo deeper
Recall that grep keeps elements matching a pattern such as a regexp and grep_v keeps the others.
Explain that grep tests pattern === element, so classes, ranges and values work, and that the block form returns mapped matches.
Choose grep for pattern tests and select for logic in review, and watch for String patterns used as if they were substring searches.
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