«

»

Aug
19

Enumerable grep method in Ruby

There is some methods that are not very known among ruby developpers.
One of them is Enumerable#grep method.
I would like to share one of the use case I have to face with, and how I implement grep.

Purpose, I have a Hash representing a simple network
I’d like to select all the people related to :patrick with network label as key.

# Say I have this hash
h = {
  :work => { :patrick => [:joana, :elvis] },
  :football => { :patrick => [:john, :fred, :marcus] , :john => [:paul, :patrick]},
  :friends => { :paul => [ :joana, :elisabeth]}
}
# I want this result :
{
  :work => [:joana,:elvis],
  :football => [:john, :fred, :marcus]
}

A solution is to iterate throug each values then select :patrick

  h.inject({}) do |memo,(k,v)|
   if v.has_key?(:patrick)
    memo.merge(k => v[:patrick])
   else 
    memo
   end
  end

Though it works, I think this piece code is not “rubyesque”.

Well, this is a basic case but imagine you have to select X number of variables or more ( :patrick,:paul … etc ) or worse select with a more complex criteria.

After digging into the rdoc documentation, it seems that Enumerable#grep matches our requirements :

  • It takes a parameter to compare with each elements of the Enumerable
  • It can take a block that returns the value of matching elements

Well, how can we implement that ?
This code below demonstrate why I love Ruby !

class NetworkPattern
  def initialize(*args)
    @matches = args
  end
 
  def ===(pair)
     key, value = pair
     value.values_at(*@matches).any?
  end
 
  def result(hash)
    hash.values_at(*self).compact.flatten
  end
 
  def exec(hash)
    Hash[ hash.to_a.grep(self){|k,v|  [k,self.result(v)] } ]
  end
 
  def to_a
     @matches
  end
end
 
pattern = NetworkPattern.new(:patrick)
 
Hash[ h.to_a.grep(pattern){|k,v|  [k,pattern.result(v)] } ] 
=> { :work => [:joana, :elvis], :football=>[:john, :fred, :marcus]}
# With somer refactoring, we implement NetworPattern#exec and use it 
pattern.exec(h)
=> { :work => [:joana, :elvis], :football=>[:john, :fred, :marcus]}

Well it may seem more complicated but if you want to select both :patrick and :paul relationships, the code does not change howmany arguments you pass in NetworkPattern.new

pattern = NetworkPattern.new(:patrick, :paul)
pattern.exec(h)
=> { :work => [:joana, :elvis], :football =>[:john, :fred, :marcus], :friends =>[:joana, :elisabeth]}

All the logic belongs to the class NetworkPattern and it uses Enumerable#grep with block return.
In your project you can use something like that since it can be easily tested and more readable.

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" line="" escaped="" highlight="">