Map an array modifying only elements matching a certain condition

前端 未结 9 2211
梦如初夏
梦如初夏 2021-02-05 09:07

In Ruby, what is the most expressive way to map an array in such a way that certain elements are modified and the others left untouched?

This is a straight-forw

相关标签:
9条回答
  • 2021-02-05 09:08

    If you don't need the old array, I prefer map! in this case because you can use the ! method to represent you are changing the array in place.

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

    I prefer this over:

    new_map = self.old_map{ |x| (x=="b" ? x+"!" : x) }
    
    0 讨论(0)
  • 2021-02-05 09:15

    Ruby 2.7+

    As of 2.7 there's a definitive answer.

    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!

    0 讨论(0)
  • 2021-02-05 09:20

    Your map solution is the best one. I'm not sure why you think map_modifying_only_elements_where is somehow better. Using map is cleaner, more concise, and doesn't require multiple blocks.

    0 讨论(0)
  • 2021-02-05 09:20

    I've found that the best way to accomplish this is by using tap

    arr = [1,2,3,4,5,6]
    [].tap do |a|
      arr.each { |x| a << x if x%2==0 }
    end
    
    0 讨论(0)
  • 2021-02-05 09:22

    One liner:

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

    In the code above, you start with [] "cumulative". As you enumerate through an Enumerator (in our case the array, ["a", "b", "c"]), cumulative as well as "the current" item get passed to our block (|cumulative, i|) and the result of our block's execution is assigned to cumulative. What I do above is keep cumulative unchanged when the item isn't "b" and append "b!" to cumulative array and return it when it is a b.

    There is an answer above that uses select, which is the easiest way to do (and remember) it.

    You can combine select with map in order to achieve what you're looking for:

     arr = ["a", "b", "c"].select { |i| i == "b" }.map { |i| "#{i}!" }
     => ["b!"]
    

    Inside the select block, you specify the conditions for an element to be "selected". This will return an array. You can call "map" on the resulting array to append the exclamation mark to it.

    0 讨论(0)
  • 2021-02-05 09:23

    I agree that the map statement is good as it is. It's clear and simple,, and would easy for anyone to maintain.

    If you want something more complex, how about this?

    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"
    
    0 讨论(0)
提交回复
热议问题