仅修改符合特定条件的元素并映射数组

19
在Ruby中,最富表现力的方法是什么,可以将一个数组映射为某些元素被修改而其他元素不变?
这是一种直接的做法:
old_a = ["a", "b", "c"]                         # ["a", "b", "c"]
new_a = old_a.map { |x| (x=="b" ? x+"!" : x) }  # ["a", "b!", "c"]
当然,如果不足够的话,忽略“leave-alone”情况:
new_a = old_a.map { |x| x+"!" if x=="b" }       # [nil, "b!", nil]

我想要的是像这样的东西:

new_a = old_a.map_modifying_only_elements_where (Proc.new {|x| x == "b"}) 
        do |y|
          y + "!"
        end
# ["a", "b!", "c"]

有没有一种好的方式可以在Ruby中实现这个功能(或者Rails有某种方便的方法我还没有发现)?


感谢大家的回复。虽然你们共同说服我最好只是使用带有三元运算符的map,但你们中的一些人发布了非常有趣的答案!


我认为 #map 这个东西现在很好。;-) - Mike Woodhouse
是的,我同意。如果这样更让你喜欢,你可以去掉括号! - glenn mcdonald
关闭了?太快了。看看我的帖子 =P - August Lilleaas
我来这里是为了寻找一种只保留特定元素的方法,然后我发现这个链接非常有帮助:https://dev59.com/cm435IYBdhLWcg3w4Uas - Jason Swett
Ruby 2.7 引入了一个内置的解决方案:filter_map更多信息请参见此处 - SRack
9个回答

25

因为数组是指针,所以这也可行:

a = ["hello", "to", "you", "dude"]
a.select {|i| i.length <= 3 }.each {|i| i << "!" }

puts a.inspect
# => ["hello", "to!", "you!", "dude"]

在循环中,确保使用改变对象而不是创建新对象的方法。例如,使用upcase!而不是upcase

具体的步骤取决于您要实现什么。用虚构的例子很难得出确定的答案。


9
old_a.map! { |a| a == "b" ? a + "!" : a }

提供

=> ["a", "b!", "c"]

map! 直接修改了调用者,所以 old_a 现在是返回的数组。


6

我认为这个地图声明已经很好了。它清晰简洁,任何人都可以轻松维护。

如果你想要更复杂的内容,可以考虑以下建议:

module Enumerable
  def enum_filter(&filter)
    FilteredEnumerator.new(self, &filter)
  end
  alias :on :enum_filter
  class FilteredEnumerator
    include Enumerable
    def initialize(enum, &filter)
      @enum, @filter = enum, filter
      if enum.respond_to?(:map!)
        def self.map!
          @enum.map! { |elt| @filter[elt] ? yield(elt) : elt }
        end
      end
    end
    def each
      @enum.each { |elt| yield(elt) if @filter[elt] }
    end
    def each_with_index
      @enum.each_with_index { |elt,index| yield(elt, index) if @filter[elt] } 
    end
    def map
      @enum.map { |elt| @filter[elt] ? yield(elt) : elt }
    end
    alias :and :enum_filter
    def or
      FilteredEnumerator.new(@enum) { |elt| @filter[elt] || yield(elt) }
    end
  end
end

%w{ a b c }.on { |x| x == 'b' }.map { |x| x + "!" } #=> [ 'a', 'b!', 'c' ]

require 'set'
Set.new(%w{ He likes dogs}).on { |x| x.length % 2 == 0 }.map! { |x| x.reverse } #=> #<Set: {"likes", "eH", "sgod"}>

('a'..'z').on { |x| x[0] % 6 == 0 }.or { |x| 'aeiouy'[x] }.to_a.join #=> "aefiloruxy"

19
你把一句话拆成了三十多个句子。我喜欢你的风格。 - Pesto
我确实说过将条件语句放在映射内部是最好的 :) - rampion

4
你的map解决方案是最好的。我不确定为什么你认为map_modifying_only_elements_where更好。使用map更清晰,更简洁,而且不需要多个块。

3

简述:

["a", "b", "c"].inject([]) { |cumulative, i| i == "b" ? (cumulative << "#{i}!") : cumulative }

在上面的代码中,您以[] "cumulative" 开始。当您枚举枚举器(在我们的例子中为数组["a", "b", "c"])时,累计和“当前”项都会传递给我们的块(|cumulative, i|),并且块执行的结果分配给累计。我上面所做的是当项目不是“b”时保持累积不变,并将“b!”附加到累积数组中,当它是b时返回它。
上面有一个使用select的答案,这是最简单的方法(并且记住)。
您可以结合selectmap以实现您想要的结果:
 arr = ["a", "b", "c"].select { |i| i == "b" }.map { |i| "#{i}!" }
 => ["b!"]

select块内,您可以指定元素被“选中”的条件。这将返回一个数组。您可以在结果数组上调用“map”函数,以将感叹号附加到其末尾。

3

Ruby 2.7+

从2.7开始,这个问题有了一个明确的答案。

Ruby 2.7引入了filter_map来实现这个目的。它是惯用的且高效的,我预计它很快就会成为常态。

例如:

[1, 2, 3].filter_map { |n| n if n.even? }

将返回[2]

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

这是一个关于该主题的好文章。希望对某些人有用!
最初的回答:

这是一个关于该主题的好文章。希望对某些人有用!


1

如果您不需要旧数组,我建议在这种情况下使用map!,因为您可以使用!方法表示您正在原地更改数组。

self.answers.map!{ |x| (x=="b" ? x+"!" : x) }

我更喜欢这个:

new_map = self.old_map{ |x| (x=="b" ? x+"!" : x) }

1

虽然只有几行代码,但这是另一种选择:

oa = %w| a b c |
na = oa.partition { |a| a == 'b' }
na.first.collect! { |a| a+'!' }
na.flatten! #Add .sort! here if you wish
p na
# >> ["b!", "a", "c"]

在我看来,使用三元运算符的集合似乎是最好的选择。


1

我发现实现这个最好的方法是使用tap

arr = [1,2,3,4,5,6]
[].tap do |a|
  arr.each { |x| a << x if x%2==0 }
end

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接