在 Ruby 中,是否存在一个结合了“ select”和“ map”的 Array 方法?

我有一个包含一些字符串值的 Ruby 数组。我需要:

  1. 查找与某个谓词匹配的所有元素
  2. 通过转换运行匹配的元素
  3. 以数组形式返回结果

现在我的解决方案是这样的:

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

是否有一个 Array 或 Enumable 方法将 select 和 map 组合到一个逻辑语句中?

75119 次浏览

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 }

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]

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

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

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

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***"]

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

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).

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

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]}

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.

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

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!