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

前端 未结 16 2267
眼角桃花
眼角桃花 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:31

    If you are willing to add some boilerplate, you can add something like this to the array class:

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

    and then anywhere you need to, you get the following syntax:

    [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
    

    for the following output:

    first: 1
    otherwise: 2
    otherwise: 3
    last: 4
    
    0 讨论(0)
  • 2020-12-08 00:34

    Or a tiny little Domain Specific Language:

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

    and the code that makes it go:

    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
    

    I started thinking, "if only you could pass multiple blocks to a Ruby function, then you could have a slick and easy solution to this question." Then I realized that DSL's play little tricks that are almost like passing multiple blocks.

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

    I see a lot of hacks here that are pretty close, but all heavily dependent on the given iterator having a fixed size and NOT being an iterator. I'd like to also propose saving the previous element as you iterate through to know the first/last element that was iterated over.

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

    Sometimes a for loop is just your best option

    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
    

    I know this question was answered, but this method has no side effects, and doesn't check if the 13th, 14th, 15th.. 10thousandth, 10,001th... record is the first record, or the last.

    Previous answers would have failed the assignment in any data structures class.

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

    I could not resist :) This is not tuned for performance although i guess it is should not be much slower than most of the other answers here. It's all about the sugar!

    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
    

    Example 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
    

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

    I needed this functionality from time to time, so I crafted a little class for that purpose.

    The latest version is at: https://gist.github.com/3823837

    Sample:

    ("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
    

    Actual class:

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