Magic First and Last Indicator in a Loop in Ruby/Rails?

前端 未结 16 2269
眼角桃花
眼角桃花 2020-12-08 00:01

Ruby/Rails does lots of cool stuff when it comes to sugar for basic things, and I think there\'s a very common scenario that I was wondering if anyone has done a helper or s

相关标签:
16条回答
  • 2020-12-08 00:43

    Partition the array into ranges where elements within each range are supposed to behave different. Map each range thus created to a block.

    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
    

    Could create ranges by hand, but these helpers below make it easier:

    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
    

    Usage:

    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
    
    0 讨论(0)
  • 2020-12-08 00:45

    What if you could do this?

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

    Or this?

    %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
    

    In MRI >= 1.8.7, all it takes is this monkey-patch:

    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
    

    It's got a little state engine because it must look ahead one iteration.

    The trick is that each, each_with_index, &c. return an Enumerator if given no block. Enumerators do everything an Enumerable does and a bit more. But for us, the important thing is that we can monkey-patch Enumerator to add one more way to iterate, "wrapping" the existing iteration, whatever it is.

    0 讨论(0)
  • 2020-12-08 00:48

    Interesting question, and one I've thought a bit about as well.

    I think you'd have to create three different blocks/procs/whatever they're called, and then create a method that calls the correct block/proc/whatever. (Sorry for the vagueness - I'm not yet a black belt metaprogrammer) [Edit: however, I've copied from someone who is at the bottom)

    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
    

    produces

    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
    

    Edit: Svante's answer (with molf's suggestion) to a related question shows how to pass in multiple code blocks to a single method:

    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"})
    
    0 讨论(0)
  • 2020-12-08 00:49

    KISS

    arr.each.with_index do |obj, index|
      p 'first' if index == 0        
      p 'last' if index == arr.count-1                  
    end
    
    0 讨论(0)
  • 2020-12-08 00:50

    As many have pointed out, each_with_index seems to be the key to this. I have this code block that I liked.

    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
    

    Or

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

    Or by Array extensions

    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
    
    0 讨论(0)
  • 2020-12-08 00:51

    If you don't mind that the "last" action happens before the stuff in the middle, then this monkey-patch:

    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
    

    Allows this:

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