tags:

views:

87

answers:

4

I have a Ruby array containing some string values. I need to:

  1. Find all elements that match some predicate
  2. Run the matching elements through a transformation
  3. Return the results as an array

Right now my solution looks like this:

def example
  matchingLines = @lines.select{ |line| ... }
  results = matchingLines.map{ |line| ... }
  return results.uniq.sort
end

Is there an Array or Enumerable method that combines select and map into a single logical statement?

+1  A: 

No, but you can do it like this:

lines.map { |line| do_some_action if check_some_property  }.reject(&:nil?)

Or even better:

lines.inject([]) { |all, line| all << line if check_some_property; all }
floatless
Jörg W Mittag
Yeah, so the inject method is even better.
floatless
+7  A: 

I usually use map and compact together along with my selection criteria as a postfix if. Compact gets rid of the nils.

jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}    
 => [3, 3, 3, nil, nil, nil] 


jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}.compact
 => [3, 3, 3] 
Jed Schneider
Ah-ha, I was trying to figure out how to ignore nils returned by my map block. Thanks!
Seth Petry-Johnson
No problem, I love compact. it unobtrusively sits out there and does its job. I also prefer this method to chaining enumerable functions for simple selection criteria because it is very declarative.
Jed Schneider
Lots of good answers here, but this one eliminates the need to chain select and map together which is excellent. Thanks!
Seth Petry-Johnson
+3  A: 

Not sure there is one (The Enumerable module which adds select and map - doesn't show one). - because that'd require you to pass in two blocks to the select_and_transform method, which would IMHO be a bit unintuitive.

Obviously, you could just chain them together, which is more readable.

transformed_list = lines.select{|line| ...}.map{|line| ... }

Gishu
+1  A: 
def example
  @lines.select {|line| ... }.map {|line| ... }.uniq.sort
end

In Ruby 1.9 and 1.8.7, you can also chain and wrap iterators by simply not passing a block to them:

enum.select.map {|bla| ... }

But it's not really possible in this case, since the types of the block return values of select and map don't match up. It makes more sense for something like this:

enum.inject.with_index {|(acc, el), idx| ... }

AFAICS, the best you can do is the first example.

Here's a small example:

%w[a b 1 2 c d].map.select {|e| if /[0-9]/ =~ e then false else e.upcase end }
# => ["a", "b", "c", "d"]

%w[a b 1 2 c d].select.map {|e| if /[0-9]/ =~ e then false else e.upcase end }
# => ["A", "B", false, false, "C", "D"]

But what you really want is ["A", "B", "C", "D"].

Jörg W Mittag
I did a very brief web search last night for "method chaining in Ruby" and it seemed like it wasn't supported well. Tho, I probably should have tried it... also, why do you say the types of the block arguments don't match up? In my example both blocks are taking a line of text from my array, right?
Seth Petry-Johnson
@Seth Petry-Johnson: Yeah, sorry, I meant the return values. `select` returns a Boolean-ish value that decides whether to keep the element or not, `map` returns the transformed value. The transformed value itself is probably going to be truthy, so all elements get selected.
Jörg W Mittag