From: Colin Bartlett Date: 2009-06-12T14:37:41+09:00 Subject: Re: Idiomatic way to detect first/last iteration? On Jun 11, 2009, at 12:43 PM, Kendall Gifford 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 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