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

Ruby on-RailsRubySyntactic Sugar

Ruby on-Rails Problem Overview


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 something similar for.

   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

Pardon the ugliness, but this gets at what one might want... is there a ruby way to do something to the first and last of a loop?

[EDIT] I think ideally this would be an extension on Array with parameters (array instance, all elements function, first elements function, last elements function)... but I'm open to other thoughts.

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

You could grab the first and last elements and process them differently, if you like.

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

Solution 2 - Ruby on-Rails

If the code for the first and last iteration has nothing in common with the code for the other iterations, you could also do:

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

If the different cases have some code in common, your way is fine.

Solution 3 - Ruby on-Rails

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.

Solution 4 - Ruby on-Rails

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.

Solution 5 - Ruby on-Rails

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

Solution 6 - Ruby on-Rails

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

Solution 7 - Ruby on-Rails

There's no "do this the (first|last) time" syntax in Ruby. But if you're looking for succinctness, you could do this:

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

The result is what you'd expect:

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

Solution 8 - Ruby on-Rails

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"})

Solution 9 - Ruby on-Rails

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

Solution 10 - Ruby on-Rails

KISS

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

Solution 11 - Ruby on-Rails

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

Solution 12 - Ruby on-Rails

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

Solution 13 - Ruby on-Rails

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

Solution 14 - Ruby on-Rails

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] 

Solution 15 - Ruby on-Rails

If you know the items in the array are unique (unlike this case), you can do this:

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

Solution 16 - Ruby on-Rails

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionaronchickView Question on Stackoverflow
Solution 1 - Ruby on-RailsMatchuView Answer on Stackoverflow
Solution 2 - Ruby on-Railssepp2kView Answer on Stackoverflow
Solution 3 - Ruby on-RailsWayne ConradView Answer on Stackoverflow
Solution 4 - Ruby on-RailsWayne ConradView Answer on Stackoverflow
Solution 5 - Ruby on-RailsNateView Answer on Stackoverflow
Solution 6 - Ruby on-RailsclordView Answer on Stackoverflow
Solution 7 - Ruby on-RailsJohn FeminellaView Answer on Stackoverflow
Solution 8 - Ruby on-RailsAndrew GrimmView Answer on Stackoverflow
Solution 9 - Ruby on-Rails2called-chaosView Answer on Stackoverflow
Solution 10 - Ruby on-RailsSagiv OfekView Answer on Stackoverflow
Solution 11 - Ruby on-RailsWayne ConradView Answer on Stackoverflow
Solution 12 - Ruby on-RailsPatrick OscityView Answer on Stackoverflow
Solution 13 - Ruby on-RailsAnuragView Answer on Stackoverflow
Solution 14 - Ruby on-RailstedView Answer on Stackoverflow
Solution 15 - Ruby on-RailsPeterView Answer on Stackoverflow
Solution 16 - Ruby on-Railsbaash05View Answer on Stackoverflow