【问题标题】:Magic First and Last Indicator in a Loop in Ruby/Rails?Ruby/Rails 循环中的魔术第一个和最后一个指标?
【发布时间】:2011-01-15 13:09:56
【问题描述】:

Ruby/Rails 在为基本事物添加糖时做了很多很酷的事情,我认为有一个非常常见的场景,我想知道是否有人做过帮助程序或类似的事情。

   a = Array.new(5, 1)

   a.each_with_index do |x, i|
     if i == 0
       print x+1
     elsif i == (a.length - 1)
       print x*10
     else
        print x
     end
   end

请原谅丑陋,但这得到了人们可能想要的......有没有一种红宝石方法可以对循环的第一个和最后一个做一些事情?

[EDIT] 我认为理想情况下这将是带有参数的 Array 的扩展(数组实例、所有元素函数、第一个元素函数、最后一个元素函数)......但我愿意其他想法。

【问题讨论】:

  • 只是对试图解决这个问题的人们的评论——真正的 Ruby 解决方案应该处理的不仅仅是each_with_index,还应该处理mapinject 等等。
  • 对于具有一个元素的数组,该元素是列表中的第一个、列表中的最后一个,还是两者兼而有之?
  • 这里有很多很棒的 cmets - 我将暂时搁置这个问题,希望我们能获得更多投票来选出最佳解决方案。
  • 我想如果是我,我会选择一个不到十行代码的解决方案。代码越短,阅读和调试就越容易。我想避免添加一两个优雅的类来做一些简单的事情,而这些简单的事情可以在几行中做得不太优雅。当然,如果你不想成为我,你也不必成为我。 (^_^)

标签: ruby-on-rails ruby syntactic-sugar


【解决方案1】:

我在这里看到很多非常接近的 hack,但都严重依赖于给定的具有固定大小的迭代器,而不是迭代器。我还想建议在迭代时保存前一个元素以了解迭代的第一个/最后一个元素。

previous = {}
elements.each do |element|
  unless previous.has_key?(:element)
    # will only execute the first time
  end

  # normal each block here

  previous[:element] = element
end

# the last element will be stored in previous[:element] 

【讨论】:

    【解决方案2】:

    正如许多人所指出的,each_with_index 似乎是解决这个问题的关键。我有这个我喜欢的代码块。

    array.each_with_index do |item,index|
      if index == 0
        # first item
      elsif index == array.length-1
        # last item
      else
        # middle items
      end
      # all items
    end
    

    或者

    array.each_with_index do |item,index|
      if index == 0
        # first item
      end
      # all items
      if index == array.length-1
        # last item
      end
    end
    

    或者通过数组扩展

    class Array
    
      def each_with_position
        array.each_with_index do |item,index|
          if index == 0
            yield item, :first
          elsif index == array.length-1
            yield item, :last
          else
            yield item, :middle
          end
        end
      end
    
      def each_with_index_and_position
        array.each_with_index do |item,index|
          if index == 0
            yield item, index, :first
          elsif index == array.length-1
            yield item, index, :last
          else
            yield item, index, :middle
          end
        end
      end
    
      def each_with_position_and_index
        array.each_with_index do |item,index|
          if index == 0
            yield item, :first, index
          elsif index == array.length-1
            yield item, :last, index
          else
            yield item, :middle, index
          end
        end
      end
    
    end
    

    【讨论】:

      【解决方案3】:

      我无法抗拒 :) 虽然我想它不应该比这里的大多数其他答案慢很多,但它并没有针对性能进行调整。一切都与糖有关!

      class Array
        class EachDSL
          attr_accessor :idx, :max
      
          def initialize arr
            self.max = arr.size
          end
      
          def pos
            idx + 1
          end
      
          def inside? range
            range.include? pos
          end
      
          def nth? i
            pos == i
          end
      
          def first?
            nth? 1
          end
      
          def middle?
            not first? and not last?
          end
      
          def last?
            nth? max
          end
      
          def inside range
            yield if inside? range
          end
      
          def nth i
            yield if nth? i
          end
      
          def first
            yield if first?
          end
      
          def middle
            yield if middle?
          end
      
          def last
            yield if last?
          end
        end
      
        def each2 &block
          dsl = EachDSL.new self
          each_with_index do |x,i|
            dsl.idx = i
            dsl.instance_exec x, &block
          end
        end
      end
      

      示例 1:

      [1,2,3,4,5].each2 do |x|
        puts "#{x} is first"  if first?
        puts "#{x} is third"  if nth? 3
        puts "#{x} is middle" if middle?
        puts "#{x} is last"   if last?
        puts
      end
      
      # 1 is first
      # 
      # 2 is middle
      # 
      # 3 is third
      # 3 is middle
      # 
      # 4 is middle
      # 
      # 5 is last
      

      示例 2:

      %w{some short simple words}.each2 do |x|
        first do
          puts "#{x} is first"
        end
      
        inside 2..3 do
          puts "#{x} is second or third"
        end
      
        middle do
          puts "#{x} is middle"
        end
      
        last do
          puts "#{x} is last"
        end
      end
      
      # some is first
      # short is second or third
      # short is middle
      # simple is second or third
      # simple is middle
      # words is last
      

      【讨论】:

        【解决方案4】:

        亲吻

        arr.each.with_index do |obj, index|
          p 'first' if index == 0        
          p 'last' if index == arr.count-1                  
        end
        

        【讨论】:

          【解决方案5】:

          我不时需要这个功能,所以我为此设计了一个小类。

          最新版本在:https://gist.github.com/3823837

          示例:

          ("a".."m").to_a.each_pos do |e|
            puts "Char\tfirst?\tlast?\tprev\tnext\twrapped?\tindex\tposition" if e.first?
            print "#{e.item}\t"
            print "#{e.first?}\t"
            print "#{e.last?}\t"
            print "#{e.prev}\t"
            print "#{e.next}\t"
            print "#{e.wrapped?}\t\t"
            print "#{e.index}\t"
            puts  "#{e.position}\t"
          end
          
          # Char  first?  last?  prev  next  wrapped?  index  position
          # a     true    false        b     false     0      1
          # b     false   false  a     c     true      1      2
          # c     false   false  b     d     true      2      3
          # d     false   false  c     e     true      3      4
          # e     false   false  d     f     true      4      5
          # f     false   false  e     g     true      5      6
          # g     false   false  f     h     true      6      7
          # h     false   false  g     i     true      7      8
          # i     false   false  h     j     true      8      9
          # j     false   false  i     k     true      9      10
          # k     false   false  j     l     true      10     11
          # l     false   false  k     m     true      11     12
          # m     false   true   l           false     12     13
          
          
          
          {
            a: "0",
            b: "1",
            c: "2",
            d: "3",
            e: "4",
            f: "5",
            g: "6",
            h: "7",
            i: "8",
            j: "9",
            k: "10",
            l: "11",
            m: "12",
          }.each_pos do |(k, v), e|
            puts "KV\tChar\t\tfirst?\tlast?\tprev\t\tnext\t\twrapped?\tindex\tposition" if e.first?
            print "#{k} => #{v}\t"
            print "#{e.item}\t"
            print "#{e.first?}\t"
            print "#{e.last?}\t"
            print "#{e.prev || "\t"}\t"
            print "#{e.next || "\t"}\t"
            print "#{e.wrapped?}\t\t"
            print "#{e.index}\t"
            puts  "#{e.position}\t"
          end
          
          # KV      Char        first?  last?   prev        next        wrapped?  index position
          # a => 0  [:a, "0"]   true    false               [:b, "1"]   false     0     1
          # b => 1  [:b, "1"]   false   false   [:a, "0"]   [:c, "2"]   true      1     2
          # c => 2  [:c, "2"]   false   false   [:b, "1"]   [:d, "3"]   true      2     3
          # d => 3  [:d, "3"]   false   false   [:c, "2"]   [:e, "4"]   true      3     4
          # e => 4  [:e, "4"]   false   false   [:d, "3"]   [:f, "5"]   true      4     5
          # f => 5  [:f, "5"]   false   false   [:e, "4"]   [:g, "6"]   true      5     6
          # g => 6  [:g, "6"]   false   false   [:f, "5"]   [:h, "7"]   true      6     7
          # h => 7  [:h, "7"]   false   false   [:g, "6"]   [:i, "8"]   true      7     8
          # i => 8  [:i, "8"]   false   false   [:h, "7"]   [:j, "9"]   true      8     9
          # j => 9  [:j, "9"]   false   false   [:i, "8"]   [:k, "10"]  true      9     10
          # k => 10 [:k, "10"]  false   false   [:j, "9"]   [:l, "11"]  true      10    11
          # l => 11 [:l, "11"]  false   false   [:k, "10"]  [:m, "12"]  true      11    12
          # m => 12 [:m, "12"]  false   true    [:l, "11"]              false     12    13
          

          实际类:

          module Enumerable
            # your each_with_position method
            def each_pos &block
              EachWithPosition.each(self, &block)
            end
          end
          
          class EachWithPosition
            attr_reader :index
          
            class << self
              def each *a, &b
                handler = self.new(*a, :each, &b)
              end
            end
          
            def initialize collection, method, &block
              @index = 0
              @item, @prev, @next = nil
              @collection = collection
              @callback = block
              self.send(method)
            end
          
            def count
              @collection.count
            end
            alias_method :length, :count
            alias_method :size, :count
          
            def rest
              count - position
            end
          
            def first?
              @index == 0
            end
          
            def last?
              @index == (count - 1)
            end
          
            def wrapped?
              !first? && !last?
            end
            alias_method :inner?, :wrapped?
          
            def position
              @index + 1
            end
          
            def prev
              @prev
            end
          
            def next
              @next
            end
          
            def current
              @item
            end
            alias_method :item, :current
            alias_method :value, :current
          
            def call
              if @callback.arity == 1
                @callback.call(self)
              else
                @callback.call(@item, self)
              end
            end
          
            def each
              @collection.each_cons(2) do |e, n|
                @prev = @item
                @item = e
                @next = n
          
                self.call
                @index += 1
          
                # fix cons slice behaviour
                if last?
                  @prev, @item, @next = @item, @next, nil
                  self.call
                  @index += 1
                end
              end
            end
          end
          

          【讨论】:

            【解决方案6】:

            有时 for 循环只是您的最佳选择

            if(array.count > 0)
               first= array[0]
               #... do something with the first
            
               cx = array.count -2 #so we skip the last record on a 0 based array
               for x in 1..cx
                 middle = array[x]
                 #... do something to the middle
               end
            
               last = array[array.count-1]
               #... do something with the last item. 
            end
            

            我知道这个问题已经回答了,但是这个方法没有副作用,并且不检查第 13、14、15 .. 10,001、10,001... 记录是第一条记录,还是最后一条记录。

            以前的答案会导致任何数据结构类中的分配失败。

            【讨论】:

              【解决方案7】:

              有趣的问题,也是我思考过的一个问题。

              我认为您必须创建三个不同的块/过程/无论它们被称为什么,然后创建一个方法来调用正确的块/过程/什么。 (抱歉含糊不清 - 我还不是黑带元程序员)[编辑:不过,我是从最底层的人那里抄来的)

              class FancyArray
                def initialize(array)
                  @boring_array = array
                  @first_code = nil
                  @main_code = nil
                  @last_code = nil
                end
              
                def set_first_code(&code)
                  @first_code = code
                end
              
                def set_main_code(&code)
                  @main_code = code
                end
              
                def set_last_code(&code)
                  @last_code = code
                end
              
                def run_fancy_loop
                  @boring_array.each_with_index do |item, i|
                    case i
                    when 0 then @first_code.call(item)
                    when @boring_array.size - 1 then @last_code.call(item)
                    else @main_code.call(item)
                    end
                  end
                end
              end
              
              fancy_array = FancyArray.new(["Matti Nykanen", "Erik Johnsen", "Michael Edwards"])
              fancy_array.set_first_code {|item| puts "#{item} came first in ski jumping at the 1988 Winter Olympics"}
              fancy_array.set_main_code {|item| puts "#{item} did not come first or last in ski jumping at the 1988 Winter Olympics"}
              fancy_array.set_last_code {|item| puts "#{item} came last in ski jumping at the 1988 Winter Olympics"}
              fancy_array.run_fancy_loop
              

              生产

              Matti Nykanen came first in ski jumping at the 1988 Winter Olympics
              Erik Johnsen did not come first or last in ski jumping at the 1988 Winter Olympics
              Michael Edwards came last in ski jumping at the 1988 Winter Olympics
              

              编辑:Svante 的answer(附有 molf 的建议)对相关问题的说明显示了如何将多个代码块传递给单个方法:

              class FancierArray < Array
                def each_with_first_last(first_code, main_code, last_code)
                  each_with_index do |item, i|
                    case i
                      when 0 then first_code.call(item)
                      when size - 1 then last_code.call(item)
                      else main_code.call(item)
                    end
                  end
                end
              end
              
              fancier_array = FancierArray.new(["Matti Nykanen", "Erik Johnsen", "Michael Edwards"])
              fancier_array.each_with_first_last(
                lambda {|person| puts "#{person} came first in ski jumping at the 1988 Winter Olympics"},
                lambda {|person| puts "#{person} did not come first or last in ski jumping at the 1988 Winter Olympics"},
                lambda {|person| puts "#{person} came last in ski jumping at the 1988 Winter Olympics"})
              

              【讨论】:

              • @baash05:你能修正你的 O(n) 符号吗?
              • 很抱歉投了反对票.. 一个有一百万的数组,你正在做 (O(n)-2)*2 额外的比较。现在,如果编译器很聪明,也许它只是 O(n)-2 额外比较,但这很丑。
              【解决方案8】:

              或者一个很小的领域特定语言:

              a = [1, 2, 3, 4]
              
              FirstMiddleLast.iterate(a) do
                first do |e|
                  p [e, 'first']
                end
                middle do |e|
                  p [e, 'middle']
                end
                last do |e|
                  p [e, 'last']
                end
              end
              
              # => [1, "first"]
              # => [2, "middle"]
              # => [3, "middle"]
              # => [4, "last"]
              

              以及实现它的代码:

              class FirstMiddleLast
              
                def self.iterate(array, &block)
                  fml = FirstMiddleLast.new(array)
                  fml.instance_eval(&block)
                  fml.iterate
                end
              
                attr_reader :first, :middle, :last
              
                def initialize(array)
                  @array = array
                end
              
                def first(&block)
                  @first = block
                end
              
                def middle(&block)
                  @middle = block
                end
              
                def last(&block)
                  @last = block
                end
              
                def iterate
                  @first.call(@array.first) unless @array.empty?
                  if @array.size > 1
                    @array[1..-2].each do |e|
                      @middle.call(e)
                    end
                    @last.call(@array.last)
                  end
                end
              
              end
              

              我开始想,“如果你能将多个块传递给一个 Ruby 函数,那么你就可以对这个问题有一个巧妙而简单的解决方案。”然后我意识到 DSL 玩的小技巧几乎就像传递多个块一样。

              【讨论】:

                【解决方案9】:

                如果你能做到这一点呢?

                %w(a b c d).each.with_position do |e, position|
                  p [e, position]    # => ["a", :first]
                                     # => ["b", :middle]
                                     # => ["c", :middle]
                                     # => ["d", :last]
                end
                

                还是这个?

                %w(a, b, c, d).each_with_index.with_position do |(e, index), position|
                  p [e, index, position]    # => ["a,", 0, :first]
                                            # => ["b,", 1, :middle]
                                            # => ["c,", 2, :middle]
                                            # => ["d", 3, :last]
                end
                

                在 MRI >= 1.8.7 中,只需要这个猴子补丁:

                class Enumerable::Enumerator
                
                  def with_position(&block)
                    state = :init
                    e = nil
                    begin
                      e_last = e
                      e = self.next
                      case state
                      when :init
                        state = :first
                      when :first
                        block.call(e_last, :first)
                        state = :middle
                      when :middle
                        block.call(e_last, :middle)
                      end
                    rescue StopIteration
                      case state
                      when :first
                        block.call(e_last, :first)
                      when :middle
                        block.call(e_last, :last)
                      end
                      return
                    end while true
                  end
                
                end
                

                它有一个小的状态引擎,因为它必须向前看一次迭代。

                诀窍是 each, each_with_index, &c.如果没有给定块,则返回一个枚举器。 Enumerators 做了 Enumerable 所做的一切,甚至更多。但对我们来说,重要的是我们可以对 Enumerator 进行猴子修补,以添加另一种迭代方式,“包装”现有的迭代,无论它是什么。

                【讨论】:

                • 对于 ruby​​ 2 及更高版本,将 class Enumerable::Enumerator 替换为 class Enumerator
                【解决方案10】:

                如果你不介意“最后”动作发生在中间的东西之前,那么这个猴子补丁:

                class Array
                
                  def for_first
                    return self if empty?
                    yield(first)
                    self[1..-1]
                  end
                
                  def for_last
                    return self if empty?
                    yield(last)
                    self[0...-1]
                  end
                
                end
                

                允许这样做:

                %w(a b c d).for_first do |e|
                  p ['first', e]
                end.for_last do |e|
                  p ['last', e]
                end.each do |e|
                  p ['middle', e]
                end
                
                # => ["first", "a"]
                # => ["last", "d"]
                # => ["middle", "b"]
                # => ["middle", "c"]
                

                【讨论】:

                  【解决方案11】:

                  将数组划分为多个范围,每个范围内的元素应该表现不同。将由此创建的每个范围映射到一个块。

                  class PartitionEnumerator
                      include RangeMaker
                  
                      def initialize(array)
                          @array = array
                          @handlers = {}
                      end
                  
                      def add(range, handler)
                          @handlers[range] = handler
                      end
                  
                      def iterate
                          @handlers.each_pair do |range, handler|
                            @array[range].each { |value| puts handler.call(value) }
                          end
                      end
                  end
                  

                  可以手动创建范围,但下面的这些助手更容易:

                  module RangeMaker
                    def create_range(s)
                      last_index = @array.size - 1
                      indexes = (0..last_index)
                      return (indexes.first..indexes.first) if s == :first
                      return (indexes.second..indexes.second_last) if s == :middle
                      return (indexes.last..indexes.last) if s == :last
                    end  
                  end
                  
                  class Range
                    def second
                      self.first + 1
                    end
                  
                    def second_last
                      self.last - 1
                    end
                  end
                  

                  用法:

                  a = [1, 2, 3, 4, 5, 6]
                  
                  e = PartitionEnumerator.new(a)
                  e.add(e.create_range(:first), Proc.new { |x| x + 1 } )
                  e.add(e.create_range(:middle), Proc.new { |x| x * 10 } )
                  e.add(e.create_range(:last), Proc.new { |x| x } )
                  
                  e.iterate
                  

                  【讨论】:

                    【解决方案12】:

                    如果你愿意添加一些样板,你可以在数组类中添加这样的东西:

                    class Array
                      def each_fl
                        each_with_index do |x,i|
                          yield [i==0 ? :first : (i==length-1 ? :last : :inner), x]
                        end
                      end
                    end
                    

                    然后在任何你需要的地方,你都会得到以下语法:

                    [1,2,3,4].each_fl do |t,x|
                      case t
                        when :first
                          puts "first: #{x}"
                        when :last
                          puts "last: #{x}"
                        else
                          puts "otherwise: #{x}"
                      end
                    end
                    

                    对于以下输出:

                    first: 1
                    otherwise: 2
                    otherwise: 3
                    last: 4
                    

                    【讨论】:

                    • 您正在对数组中的每个项目进行比较,以检查它是第一个元素还是最后一个元素。在 100 万个数组中,您会进行 1,999,998 次无用的比较。
                    • baash05,我只是建议一个界面,任何复制粘贴以上内容的人都应该受到痛苦。
                    • 当然,但这个建议是邪恶的。新程序员做复制粘贴,有技能的人应该注意,并把它清理干净。你绝不是唯一一个建议这种机智的人。这告诉我,错误的答案正在通过菜鸟激增......看看韦恩斯......和你的一样......
                    • 事实上..这里几乎所有的答案都有相同的方法..肯定会复制一个,我怀疑它会是一个每个项目有 2 个比较。我会在任何 ds 课程中不及格。
                    • @baash05,我的解决方案中只有一个涉及每个元素的比较,但这并不能使它(或任何其他这样做的)“菜鸟”。 Ruby 中整数和符号的比较非常快。无论如何,循环机制所花费的时间通常与循环体所花费的时间相形见绌。 Ruby 是关于传达其目的的清晰代码。 Ruby 的性能来自正确的数据结构和算法,而不是微优化。如果微优化似乎是必要的,那么部分或全部程序最好用更快的语言(C 等)编写。
                    【解决方案13】:

                    如果您知道数组中的项目是唯一的(与本例不同),您可以这样做:

                    a = [1,2,3,4,5]
                    
                    a.each_with_index do |x, i|
                      if x == a.first
                        print x+1
                      elsif x == a.last
                        print x*10
                      else
                        print x
                      end
                    end
                    

                    【讨论】:

                      【解决方案14】:

                      如果您愿意,您可以抓取第一个和最后一个元素并以不同方式处理它们。

                      first = array.shift
                      last = array.pop
                      process_first_one
                      array.each { |x| process_middle_bits }
                      process_last_one
                      

                      【讨论】:

                      • 我想我可能最喜欢这个,因为它可以很好地处理一个和两个项目数组,尽管它远不如 Andrew Grimm、John Feminella 或 clord 的一般化。
                      • 嗯。请注意您是否需要返回最终数组 - 您需要 push 和 unshift,但这没什么大不了的。
                      • 为了避免副作用,你可以这样做first, middle, last = array.first, array[1..-2], array.last
                      • 随着 Andrew Grimm 的加入,我想我会回答这个问题,尽管我喜欢所有的活动。
                      • 感谢@Matchu 让我度过了美好的一天
                      【解决方案15】:

                      Ruby 中没有“在(第一次|最后一次)时间执行此操作”语法。但如果你想要简洁,你可以这样做:

                      a.each_with_index do |x, i|
                        print (i > 0 ? (i == a.length - 1 ? x*10 : x) : x+1)
                      end
                      

                      结果如你所愿:

                      irb(main):001:0> a = Array.new(5,1)
                      => [1, 1, 1, 1, 1]
                      irb(main):002:0> a.each_with_index do |x,i|
                      irb(main):003:1*   puts (i > 0 ? (i == a.length - 1 ? x*10 : x) : x+1)
                      irb(main):004:1> end
                      2
                      1
                      1
                      1
                      10
                      

                      【讨论】:

                        【解决方案16】:

                        如果第一次和最后一次迭代的代码与其他迭代的代码没有共同之处,您也可以这样做:

                        do_something( a.first )
                        a[1..-2].each do |x|
                          do_something_else( x )
                        end
                        do_something_else_else( a.last )
                        

                        如果不同的情况有一些共同的代码,你的方法很好。

                        【讨论】:

                        • 很酷的想法...事实上,即使有共同点,您也可以在其他所有内容之前执行 a.each,然后在末尾执行 a.first 和 a.last追加/覆盖你所做的。
                        • a.first === a[0]; a.last === a[-1],顺便说一句。
                        • 应该是答案...同意即使它们有共同点,您也不会询问数组中的每个元素是第一个还是最后一个,这让我微笑。
                        猜你喜欢
                        • 2016-02-22
                        • 2019-04-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2022-01-03
                        • 2017-11-01
                        • 1970-01-01
                        • 2018-08-17
                        相关资源
                        最近更新 更多