Hi there, is there any recognized ruby idiomatic way for detecting
when you're on the first and/or last iteration over a collection? Or,
if not, anyone know of any tricky ways?
I don't see how you can do what you want without some sort of "index".
In October.2008 I was thinking about how to detect the last iteration
when you didn't know it was the last iteration, and devised something
which - unsurprisingly - was very similar to Gary Wright's code.
(In Gary Wright's code, is there a reason for using Object.new.
instead of, say, the following minor changes?
previous = sigil = Object.new ##-
previous = nil ##+
yield(previous, 'last') unless previous == sigil ##-
yield(previous, 'last') if previous ##+ )
Based on various possibilities for what you may want/need,
I've made some minor changes to my Enumerable#each_with_index!
If it is called with no argument, or with nil or false,
it acts (or should act!) exactly like each_with_index.
But if it is called with an argument which is not nil or false,
then it indicates the last iteration by returning index -1,
(by analogy with -1 meaning the last element of an array, etc),
unless the last iteration is also the first iteration.
If the iteration is in the middle, then it returns index > 0.
If the iteration is the first iteration then it returns index 0,
*unless* the first iteration will also be the last iteration,
in which case it returns whatever is the argument you supplied.
module Enumerable
# This is similar to each_with index, except that it also tells you
# whether you are at the end of the enumeration.
# The index (2nd) item in the yield is an integer >= 0 unless:
# each_with_index! is called with an argument which is not nil or false,
# *and* it is the last iteration.
# In this case the "index" item yielded is -1 *unless* the last iteration
# is also the first iteration, in which case the "index" value is whatever
# was the (not nil or false) argument to Enumerable#each_with_index!.
# This allows you to decide where there is only one iteration
# whether you want to treat this as being "last" (use argument -1),
# or "first" (use argument 0), or something else to specifically indicate
# this is both the first and last iteration.
#
# Because of the implementation of this, the "next" object is "available",
# so there is an option to have that as a 3rd item in the yield.
# (For example, you could use it as an each "pair" iteration,
# which you can "break" at index (2nd item in yield) == -1"
# or, maybe better,at index < 0, depending on the arguments to the method.)
#
# If as well as the possibly negative (or something else!) index,
# you also want an index which doesn't suddenly become negative
# (or something else!) you can do, for example:
# kk = -1
# object.each_with_index!( -1 ) do | obj, ii | kk += 1
# # code
# end
# or
# kk = nil
# object.each_with_index!( -1 ) do | obj, ii |
# if kk then kk += 1 else kk = 0 end
# # code
# end
#
# (?? Maybe use each_with_index!(*args) to
# allow more sophisticated arguments ??)
#
def each_with_index!( qtype = nil, next_obj = false )
obj = ii = nil
self.each do | obje |
if ii then
if next_obj then yield obj, ii, obje
else yield obj, ii
end
ii += 1
else ii = 0
end
obj = obje
end
if ii then # last, which is also first if ii == 0
if qtype then
if ii == 0 then yield obj, qtype
else yield obj, -1
end
else yield obj, ii
end
end
return nil
end
end
########################### examples ############
# Applied to your problem, here are some examples.
# For 3 or more iterations they all behave in the same way.
# If there are only two iterations, then the 2nd and 3rd examples
# behave diifferently.
# If there is only 1 iteration:
# * the first three display: "my only favorite"
# * the fourth displays: "my absolute favorite"
# * the fifth displays: "my least fav. of all my favorites"
genres = %w{ classical punk rock jazz }
5.times do | n |
genres[ -1, 1 ] = [] if n > 0
puts ; puts "*genres == " + genres.inspect
puts
genres.each_with_index!(true) do | genre, ii |
if ii == 0 then # proper first
desc = "my absolute favorite"
elsif ii == true then # first and last
desc = "my only favorite"
elsif ii > 0 then # proper middle
desc = "one of my favorites"
else # proper last
desc = "my least fav. of all my favorites"
end
puts "#{genre} is #{desc}"
end
# or:
puts
kk = -1
genres.each_with_index!(-1) do | genre, ii | kk += 1
if kk == 0 then # first
if ii == 0 then # proper first
desc = "my absolute favorite"
else # first and last
desc = "my only favorite"
end
elsif ii > 0 then # proper middle
desc = "one of my favorites"
else # proper last
if kk == 1 then
desc = "my second favorite"
else
desc = "my least fav. of all my favorites"
end
end
puts "#{genre} is #{desc}"
end
# or the less readable (but faster?):
puts
kk = -1
genres.each_with_index!(-1) do | genre, ii | kk += 1
if ii > 0 then # proper middle
desc = "one of my favorites"
elsif ii == 0 then # proper first
desc = "my absolute favorite"
elsif kk == 0 then # first and last
desc = "my only favorite"
else # proper last
if kk == 1 then
desc = "my second favorite"
else
desc = "my least fav. of all my favorites"
end
end
puts "#{genre} is #{desc}"
end
# or:
puts
genres.each_with_index!(0) do | genre, ii |
if ii == 0 then # first, and maybe also last
desc = "my absolute favorite"
elsif ii > 0 then # proper middle
desc = "one of my favorites"
else # proper last
desc = "my least fav. of all my favorites"
end
puts "#{genre} is #{desc}"
end
# or (and so on):
puts
genres.each_with_index!(-1) do | genre, ii |
if ii == 0 then # proper first
desc = "my absolute favorite"
elsif ii > 0 then # proper middle
desc = "one of my favorites"
else # last, and maybe also first
desc = "my least fav. of all my favorites"
end
puts "#{genre} is #{desc}"
end
# example of next object as well
puts
genres.each_with_index!(-1, true) do | genre, ii, next_obj |
p [ genre, ii, next_obj ]
end
# example of next object as well, but otherwise like each_with_index
puts
genres.each_with_index!(nil, true) do | genre, ii, next_obj |
p [ genre, ii, next_obj ]
end
end