120

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
  • 4
    Ruby 2.7 is introducing filter_map for this exact purpose. More info here. Commented Jun 12, 2019 at 15:00

14 Answers 14

125

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] 
Sign up to request clarification or add additional context in comments.

6 Comments

Ah-ha, I was trying to figure out how to ignore nils returned by my map block. Thanks!
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.
I was unsure if map + compact would really perform better than inject and posted my benchmark results to a related thread: stackoverflow.com/questions/310426/list-comprehension-in-ruby/…
this will remove all nils, both the original nils and those that fail your criteria. So watch out
It doesn't entirely eliminate chaining map and select, it's just that compact is a special case reject that works on nils and performs somewhat better due to having been implemented directly in C.
|
117

Ruby 2.7+

There is now!

Ruby 2.7 is introducing filter_map for this exact purpose. It's idiomatic and performant, and I'd expect it to become the norm very soon.

For example:

numbers = [1, 2, 5, 8, 10, 13]
enum.filter_map { |i| i * 2 if i.even? }
# => [4, 16, 20]

Here's a good read on the subject.

Hope that's useful to someone!

3 Comments

No matter how often I upgrade, a cool feature is always in the next version.
Nice. One problem could be that since filter, select and find_all are synonymous, just as map and collect are, it might be hard to remember the name of this method. Is it filter_map, select_collect, find_all_map or filter_collect ?
This definitely should be the accepted answer in 2021 with Ruby 2.7! Works flawlessly.
58

You can use reduce for this, which requires only one pass:

[1,1,1,2,3,4].reduce([]) { |a, n| a.push(n*3) if n==1; a }
=> [3, 3, 3] 

In other words, initialize the state to be what you want (in our case, an empty list to fill: []), then always make sure to return this value with modifications for each element in the original list (in our case, the modified element pushed to the list).

This is the most efficient since it only loops over the list with one pass (map + select or compact requires two passes).

In your case:

def example
  results = @lines.reduce([]) do |lines, line|
    lines.push( ...(line) ) if ...
    lines
  end
  return results.uniq.sort
end

3 Comments

Doesn't each_with_object make a little more sense? You don't have to return the array at the end of each iteration of the block. You can simply do my_array.each_with_object([]) { |i, a| a << i if i.condition }.
@henrebotha Perhaps it does. I'm coming from a functional background, that's why I found reduce first 😊
Never forget reduce (left fold) is what makes all these maps and filters tick. Syntax sugars have their value too though.
22

Another different way of approaching this is using the new (relative to this question) Enumerator::Lazy:

def example
  @lines.lazy
        .select { |line| line.property == requirement }
        .map    { |line| transforming_method(line) }
        .uniq
        .sort
end

The .lazy method returns a lazy enumerator. Calling .select or .map on a lazy enumerator returns another lazy enumerator. Only once you call .uniq does it actually force the enumerator and return an array. So what effectively happens is your .select and .map calls are combined into one - you only iterate over @lines once to do both .select and .map.

My instinct is that Adam's reduce method will be a little faster, but I think this is far more readable.


The primary consequence of this is that no intermediate array objects are created for each subsequent method call. In a normal @lines.select.map situation, select returns an array which is then modified by map, again returning an array. By comparison, the lazy evaluation only creates an array once. This is useful when your initial collection object is large. It also empowers you to work with infinite enumerators - e.g. random_number_generator.lazy.select(&:odd?).take(10).

10 Comments

To each their own. With my kind of solution I can glance at the method names and immediately know that I am going to transform a subset of the input data, make it unique, and sort it. reduce as a "do everything" transformation always feels fairly messy to me.
@henrebotha: Forgive me if I've misunderstood what you meant, but this is a very important point: it's not correct to say that "you only iterate over @lines once to do both .select and .map". Using .lazy does not mean chained operations operations on a lazy enumerator will get "collapsed" into one single iteration. This is a common misunderstanding of lazy evaluation w.r.t chaining operations over a collection. (You can test this by adding a puts statement to the beginning of the select and map blocks in the first example. You'll find that they print the same number of lines)
@pje: [1, 2, 3, 4, 5].lazy.select { |i| puts "select"; i.odd? }.map { |i| puts "map"; i.to_s }.uniq.sort prints "select" five times and "map" three times, so I'm not sure what you mean.
The time complexity is the same in that the lazy/eager versions both make the same number of operations, but the space complexity is less since only one additional array is ever initialized with the lazy version.
I agree that the wording you only iterate over @lines once to do both .select and .map is incorrect. #lazy, #select and #map are all creating enumerators here, so there is actually more enumerations being performed with this solution because of the extra lazy enumerator. The answer shows that reduce is expected to be faster, but I expect this is also slower than most/all the other answers. I do love the rest of this answer though AND it introduced me to (allowed me to stumble upon) lazy enumeration in ruby. Thanks!
|
13

If you have a select that can use the case operator (===), grep is a good alternative:

p [1,2,'not_a_number',3].grep(Integer){|x| -x } #=> [-1, -2, -3]

p ['1','2','not_a_number','3'].grep(/\D/, &:upcase) #=> ["NOT_A_NUMBER"]

If we need more complex logic we can create lambdas:

my_favourite_numbers = [1,4,6]

is_a_favourite_number = -> x { my_favourite_numbers.include? x }

make_awesome = -> x { "***#{x}***" }

my_data = [1,2,3,4]

p my_data.grep(is_a_favourite_number, &make_awesome) #=> ["***1***", "***4***"]

2 Comments

It's not an alternative - it's the only correct answer to the question.
@inopinatus: Not anymore. This is still a good answer, though. I don't remember seeing grep with a block otherwise.
9

I'm not sure there is one. The Enumerable module, which adds select and map, doesn't show one.

You'd be required to pass in two blocks to the select_and_transform method, which would be a bit unintuitive IMHO.

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

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

Comments

3

Simple Answer:

If you have n records, and you want to select and map based on condition then

records.map { |record| record.attribute if condition }.compact

Here, attribute is whatever you want from the record and condition you can put any check.

compact is to flush the unnecessary nil's which came out of that if condition

1 Comment

You can use the same with unless condition too. As my friend asked.
2

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 }

2 Comments

reject(&:nil?) is basically the same as compact.
Yeah, so the inject method is even better.
2

I think that this way is more readable, because splits the filter conditions and mapped value while remaining clear that the actions are connected:

results = @lines.select { |line|
  line.should_include?
}.map do |line|
  line.value_to_map
end

And, in your specific case, eliminate the result variable all together:

def example
  @lines.select { |line|
    line.should_include?
  }.map { |line|
    line.value_to_map
  }.uniq.sort
end

Comments

1
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"].

2 Comments

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: 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.
1

You should try using my library Rearmed Ruby in which I have added the method Enumerable#select_map. Heres an example:

items = [{version: "1.1"}, {version: nil}, {version: false}]

items.select_map{|x| x[:version]} #=> [{version: "1.1"}]
# or without enumerable monkey patch
Rearmed.select_map(items){|x| x[:version]}

1 Comment

select_map in this library just implements the same select { |i| ... }.map { |i| ... } strategy from many answers above.
1

If you want to not create two different arrays, you can use compact! but be careful about it.

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}
new_array.compact!

Interestingly, compact! does an in place removal of nil. The return value of compact! is the same array if there were changes but nil if there were no nils.

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}.tap { |array| array.compact! }

Would be a one liner.

Comments

0

Your version:

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

My version:

def example
  results = {}
  @lines.each{ |line| results[line] = true if ... }
  return results.keys.sort
end

This will do 1 iteration (except the sort), and has the added bonus of keeping uniqueness (if you don't care about uniq, then just make results an array and results.push(line) if ...

Comments

-1

Here is a example. It is not the same as your problem, but may be what you want, or can give a clue to your solution:

def example
  lines.each do |x|
    new_value = do_transform(x)
    if new_value == some_thing
      return new_value    # here jump out example method directly.
    else
      next                # continue next iterate.
    end
  end
end

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.