In Progress
Unit 1, Lesson 1
In Progress

Inverted Grep

Video transcript & code

Back in episode #215, we learned about Ruby's Enumerable#grep method. We learned that while it takes its name from the UNIX grep utility, Ruby's version generalizes it to work on any kind of data, not just strings.

For instance, as we saw in that episode, we can take a list of integers and filter it with a range using #grep

[87, 23, 15, 74, 62, 42, 91].grep(0..50)
# => [23, 15, 42]

This works because #grep recruits the full power of Ruby's "case equality" or "threequals" pattern-matching.

We also learned that #grep passes matching elements to its block:

[87, 23, 15, 74, 62, 42, 91].grep(0..50) do |e|
  puts e
end
# >> 23
# >> 15
# >> 42

But what if we wanted to quickly switch this around and see the list of numbers that don't match the filter? Ruby doesn't have an "exclusive range" syntax that would let us create a range of all numbers except for zero through fifty.

We might switch this to use the #reject method instead.

Then we have to provide a block that tests each element against the range.

And now we have a dangling block, which we have to re-home in an #each message.

[87, 23, 15, 74, 62, 42, 91].reject {|n| (0..50) === n }.each do |e|
  puts e
end
# >> 87
# >> 74
# >> 62
# >> 91

That's a lot of effort just to invert the grep. A slightly more tricky way to do it is with a lambda. We make a lambda that does an inverted version of the membership test, and then pass that lambda to #grep.

inverted = ->(n) { not (0..50) === n}

[87, 23, 15, 74, 62, 42, 91].grep(inverted) do |e|
  puts e
end

# >> 87
# >> 74
# >> 62
# >> 91

This works because Ruby lambdas alias the threequals method to the #call method.

Thankfully, now that Ruby 2.3 is out, we don't have to resort to either of these shenanigans any more. Ruby 2.3 finally introduces a standard inverted grep. All we have to to is switch from #grep_v.

[87, 23, 15, 74, 62, 42, 91].grep_v(0..50) do |e|
  puts e
end

# >> 87
# >> 74
# >> 62
# >> 91

Now if you're a longtime UNIX-head, this method name probably makes perfect sense to you. Because -v is the flag we pass to the command-line version of grep to tell it to invert its logic.

For anyone else, this probably seems like a terribly opaque choice of method names. And, well… you're not wrong.

All I can tell you to help you remember this method name is that the "V" stands for the "V" in "inVerted". It's not great, but it's the best mnemonic we have.

One last thing. Supposing we want to optionally invert the results, depending on a user flag.

invert_results = true

Here's an easy way to switch between versions of grep based on a boolean value. We can use a ternary operator to switch between #grep and #grep_v

…and then use #public_send to send the message.

When the flag is false, we get a regular grep.

And when the flag is true, we get an inverted grep.

invert_results = true
message = invert_results ? :grep_v : :grep
[87, 23, 15, 74, 62, 42, 91].public_send(message, 0..50) do |e|
  puts e
end

# >> 87
# >> 74
# >> 62
# >> 91

That's all for today. Happy hacking!

Responses