On Jun 11, 2009, at 12:43 PM, Kendall G. wrote:
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 W.'s code.
(In Gary W.'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