Posts Tagged ‘select’

Ruby Enumerable Magic: Filters

December 6, 2010

  1. The Basics
  2. Unary Ampersand Operator
  3. Booleans
  4. Filters
  5. New Collections
  6. Aggregates
 

A powerful part of the Enumerable module is the set of methods that help you filter a larger collection down to a smaller one, containing just the items you need. There are several methods to help.

entries (or to_a)

This is the most permissive of the Enumerable filter methods – it doesn’t filter out anything. In fact, if you use this on an array you won’t see any difference at all. entries returns the list of items in the collection. For an array, the array itself will be returned. This makes sense, when you see that this method is aliased as to_a.

But more complex collections like our Team class will provide a better example. Here’s the class, in case you missed it from previous posts:

class Team
  include Enumerable
  
  attr_accessor :members
  
  def initialize
    @members = []
  end
  
  def each &block
    @members.each{|member| block.call(member)}
  end
end

Let’s use entries:

irb(main):001:0> require 'team.rb'
=> true
irb(main):002:0> team = Team.new
=> #<Team:0x100391088 @members=[]>
irb(main):003:0> team.members = ['joshua', 'gabriel', 'jacob']
=> ["joshua", "gabriel", "jacob"]
irb(main):004:0> team
=> #<Team:0x100391088 @members=["joshua", "gabriel", "jacob"]>
irb(main):005:0> team.entries
=> ["joshua", "gabriel", "jacob"]

You can see that team is a non-array collection, but calling entries returns the members list. To be honest, this isn’t immensely useful – but it’s good to know that it exists, and how it works.

select (or find_all)

In contrast to entries, select is arguably the most used enumerable filter. It returns a list of all the collection items that pass the test that you pass in as a block. For example:

irb(main):006:0> team.select{|member| member =~ /^j/}
=> ["joshua", "jacob"]

Here we want to filter the collection to just those items that start with the letter “j”. As with all Ruby methods that expect a block, we can use the unary ampersand operator to make our code shorter and sweeter:

irb(main):007:0> team.members[1].freeze
=> "gabriel"
irb(main):008:0> team.select(&:frozen?)
=> ["gabriel"]

In the example above, we “froze” the second member in the collection, “gabriel”. Then we called select on the collection, asking for just the frozen members.

reject

This is the opposite of select. It will only return items in the collection that *don’t* pass the test we give it. Let’s do the opposite of our last two tests:

irb(main):009:0> team.reject{|member| member =~ /^j/}
=> ["gabriel"]
irb(main):010:0> team.reject(&:frozen?)
=> ["joshua", "jacob"]

partition

This little method probably deserves a lot more use and recognition than it gets. You might say it’s the Bruce Campbell of enumerable methods. It’s like calling select AND reject on a collection. it gives you both lists back – the passers, and the failures:

irb(main):011:0> frozen, not_frozen = team.partition(&:frozen?)
=> [["gabriel"], ["joshua", "jacob"]]
irb(main):012:0> frozen
=> ["gabriel"]
irb(main):013:0> not_frozen
=> ["joshua", "jacob"]

grep

In our previous examples, we’ve used regular expressions in some of our filter methods. To be honest, I do this a lot. I didn’t realize that Enumerable provided a special method just for this use case until I researched this article. It works like select, except it only takes a regular expression pattern, no need to pass it a block. That means this snippet:

irb(main):014:0> team.select{|member| member =~ /^j/}
=> ["joshua", "jacob"]

Can be rewritten like this:

irb(main):015:0> team.grep(/^j/)
=> ["joshua", "jacob"]

If you like passing blocks to methods (and who doesn’t??) then you’re in luck, because you can still do that with grep. If you pass a block, each collection item that matches the regular expression will be run through the block, and the results will be gathered up into the return array for you. Like this:

irb(main):016:0> team.grep(/^j/){|member| member.capitalize}
=> ["Joshua", "Jacob"]

grep found our matches, passed each one through capitalize, and gave us a list of the results.

detect (or find)

detect works a lot like select, except it stops looking as soon as it finds the first match. Unlike the other methods, it doesn’t return an array. It returns just the single item:

irb(main):017:0> team.detect{|member| member =~ /^j/}
=> "joshua"

By default, detect will return nil if no match is found. But you can pass a block, which it will run and return, if no match is found:

irb(main):018:0> team.detect(lambda{'turtles'}){|member| member =~ /^t/}
=> "turtles"

An alias for this method is find, which you’ll obviously want to avoid if you’re in a Rails application, since ActiveRecord has its own find method, and you don’t want to make your code harder to read.

Summary

Don’t be a fool, like I was, and wait until you write your own article about enumerables to brush up on all the filter methods. I remember the first time I changed the brakes on my own car. One nut would not come loose, and I was even using vise grips on it. After a few hours, I called a friend who brought over a tool that actually tightens the more you crank it (unlike vise grips where you determine the pressure beforehand) and it was off in under a minute. The first step in knowing the right tool for the job is knowing what tools you have at your disposal.


Follow

Get every new post delivered to your Inbox.