[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.ruby

Idiomatic way to detect first/last iteration?

Kendall Gifford

6/11/2009 4:43:00 PM

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?

Specifically, I'm trying to avoid using each_with_index (as shown
below):

genres = %w{ classical punk rock jazz }
genres.each_with_index do |genre, i|
desc = "one of my favorites"
desc = "my absolute favorite" if i.zero?
desc = "my least fav. of all my favorites" if i + 1 == genres.length
puts "#{genre} is #{desc}"
end

I just don't like having to compare an index value (especially as when
detecting the final iteration) and hate even having it as a loop
parameter unless absolutely necessary. I know, I'm picky. Just wanted
to beat the bushes and see if any cool alternatives pop up.

18 Answers

John W Higgins

6/11/2009 5:19:00 PM

0

[Note: parts of this message were removed to make it a legal post.]

On Thu, Jun 11, 2009 at 9:43 AM, Kendall Gifford <zettabyte@gmail.com>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?
>
> Specifically, I'm trying to avoid using each_with_index (as shown
> below):
>
> genres = %w{ classical punk rock jazz }
> genres.each_with_index do |genre, i|
> desc = "one of my favorites"
> desc = "my absolute favorite" if i.zero?
> desc = "my least fav. of all my favorites" if i + 1 == genres.length
> puts "#{genre} is #{desc}"
> end
>
> I just don't like having to compare an index value (especially as when
> detecting the final iteration) and hate even having it as a loop
> parameter unless absolutely necessary. I know, I'm picky. Just wanted
> to beat the bushes and see if any cool alternatives pop up.
>
>
One option (not necessarily much prettier) would be to explicitly select out
the portions

genres = %w{ classical punk rock jazz }
puts "My absolute favorite #{genres[0]}"
genres[1..-2].each{ |g| puts "Another favorite #{g}" }
puts "My least favorite #{genres[-1]}" unless genres.length == 1

John

Kendall Gifford

6/11/2009 5:36:00 PM

0


On Jun 11, 11:18=A0am, John W Higgins <wish...@gmail.com> wrote:
>
> One option (not necessarily much prettier) would be to explicitly select =
out
> the portions
>
> genres =3D %w{ classical punk rock jazz }
> puts "My absolute favorite #{genres[0]}"
> genres[1..-2].each{ |g| puts "Another favorite #{g}" }
> puts "My least favorite #{genres[-1]}" unless genres.length =3D=3D 1
>

This is certainly an option in general. However, for my purposes, I
should provide more context of my actual situation (instead of my
contrived example).

I'm using ruby to build a rails application (I know, now this post may
become a bit rails specific, but hang with me). Specifically, I'm
writing code to generate/render the view of one of my pages (using
erb).

The gist of it is that I'm outputting a bunch or "rows", 1 for each
entry in a collection. However, I need the rendering to be slightly
different for the first and last row. I can totally use
each_with_index to do this, but I'm hoping for something else. The
rails stack provides a "helper" lib method called "cycle" that allows
me to elegantly render rows differently based on some repetition cycle
(every-other, or every-fifth, etc). The cycle helper allows me to
avoid using each_with_index and testing the "index % some_val" for row
coloring. I just want to know if there is a clean, idiomatic way,
specifically in ruby (not rails library code) to do something similar,
detecting the first/last iteration of a collection.

I may just end up writing a rails helper to do something to clean it
up (and keep my view code clean and low on logic).

Matthew Williams

6/11/2009 5:52:00 PM

0

I expect there's a better way to do this, but as a quick dash-off....

x=%w(the big fat cat)

x.each do |y|
print "first " if (y==x.first)
print "last " if (y==x.last)
puts y
end

--
"... if you do follow your bliss you put yourself on a kind of
track that has been there all the while, waiting for you, and the life
that you ought to be living is the one you are living. When you can
see that, you begin to meet people who are in your field of bliss, and
they open doors to you. I say, follow your bliss and don't be afraid,
and doors will open where you didn't know they were going to be." --
Joseph Campbell

Pieter V.

6/11/2009 5:54:00 PM

0

That won't actually work if your array contains duplicate elements...

On Thu, Jun 11, 2009 at 10:52 AM, Matthew K. Williams<matt@harpstar.com> wrote:
> I expect there's a better way to do this, but as a quick dash-off....
>
> x=%w(the big fat cat)
>
> x.each do |y|
> print "first " if (y==x.first)
> print "last " if (y==x.last)
> puts y
> end
>
> --
> "... if you do follow your bliss you put yourself on a kind of
> track that has been there all the while, waiting for you, and the life
> that you ought to be living is the one you are living. When you can
> see that, you begin to meet people who are in your field of bliss, and
> they open doors to you. I say, follow your bliss and don't be afraid,
> and doors will open where you didn't know they were going to be." --
> Joseph Campbell
>
>

Brad Ediger

6/11/2009 5:54:00 PM

0

[Note: parts of this message were removed to make it a legal post.]

On Thu, Jun 11, 2009 at 12:52 PM, Matthew K. Williams <matt@harpstar.com>wrote:

> I expect there's a better way to do this, but as a quick dash-off....
>
> x=%w(the big fat cat)
>
> x.each do |y|
> print "first " if (y==x.first)
> print "last " if (y==x.last)
> puts y
> end
>

Only if all of your elements are unique...

x=%w(1 2 1 3 4 3)

Matthew Williams

6/11/2009 6:02:00 PM

0


On Fri, 12 Jun 2009, Pieter V. wrote:

> That won't actually work if your array contains duplicate elements...
>

True -- however, given the scenario that the querent provided, it should
work.....

Hmmm... could also set a flag, I guess, if you're that opposed to using
index.

Matt

Gary Wright

6/11/2009 6:33:00 PM

0


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?


The following solution uses each_with_index internally but doesn't
depend on #first, #last, or #length. On a single item collection
it will report 'last' but not 'first'. You'll have to decide if
that works in your case. Obviously you could change it to report
'both' for a single item.

Gary Wright


module Enumerable
def each_with_position
previous = sigil = Object.new
each_with_index { |next_item, index|
case index
when 0
# do nothing
when 1
yield(previous, 'first')
else
yield(previous, 'middle')
end
previous = next_item
}
yield(previous, 'last') unless previous == sigil
end
end

>> [1,2,3,4].each_with_position { |*x| p x }
[1, "first"]
[2, "middle"]
[3, "middle"]
[4, "last"]
=> nil
>> [].each_with_position { |*x| p x }
=> nil
>> [1].each_with_position { |*x| p x }
[1, "last"]
=> nil





Colin Bartlett

6/12/2009 5:38:00 AM

0

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

Trans

6/12/2009 10:30:00 AM

0



On Jun 11, 12:43=A0pm, Kendall Gifford <zettab...@gmail.com> 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?
>
> Specifically, I'm trying to avoid using each_with_index (as shown
> below):
>
> genres =3D %w{ classical punk rock jazz }
> genres.each_with_index do |genre, i|
> =A0 desc =3D "one of my favorites"
> =A0 desc =3D "my absolute favorite" if i.zero?
> =A0 desc =3D "my least fav. of all my favorites" if i + 1 =3D=3D genres.l=
ength
> =A0 puts "#{genre} is #{desc}"
> end
>
> I just don't like having to compare an index value (especially as when
> detecting the final iteration) and hate even having it as a loop
> parameter unless absolutely necessary. I know, I'm picky. Just wanted
> to beat the bushes and see if any cool alternatives pop up.

I've experimented with something along these lines.

# =3D Iteration
#
class It
attr_reader :index, :value, :prior, :after
def initialize(array)
@array =3D array
@index =3D 0
@value =3D array[0]
@prior =3D []
@after =3D array[1..-1]
end
def first? ; @index =3D=3D 0 ; end
def last?
if Enumerable =3D=3D=3D self
nil
else
@index =3D=3D @array.length
end
end
private
def next_iteration
@index +=3D 1
@prior << @value
@value =3D @after.shift
end
end

class Array
# Iterate over each element of array using an iteration object.
#
# [1,2,3].each_iteration do |it|
# p it.index
# p it.value
# p it.first?
# p it.last?
# p it.prior
# p it.after
# end
#
# on each successive iteration produces:
#
# 0 1 2
# 1 2 3
# true false false
# false false true
# [] [1] [1,2]
# [2,3] [3] []
#
# CREDIT: Trans

def each_iteration
if block_given?
it =3D It.new(self)
each do |e|
yield(it)
it.send(:next_iteration)
end
else
return Enumerable::Enumerator.new(self, :each_iteration)
end
end
end

T.

Robert Klemme

6/12/2009 11:18:00 AM

0

2009/6/11 Gary Wright <gwtmp01@mac.com>:
>
> 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?
>
>
> The following solution uses each_with_index internally but doesn't
> depend on #first, #last, or #length. On a single item collection
> it will report 'last' but not 'first'. =A0You'll have to decide if
> that works in your case. Obviously you could change it to report
> 'both' for a single item.
>
> Gary Wright
>
>
> module Enumerable
> =A0def each_with_position
> =A0 =A0previous =3D sigil =3D Object.new
> =A0 =A0each_with_index { |next_item, index|
> =A0 =A0 =A0case index
> =A0 =A0 =A0when 0
> =A0 =A0 =A0 =A0# do nothing
> =A0 =A0 =A0when 1
> =A0 =A0 =A0 =A0yield(previous, 'first')
> =A0 =A0 =A0else
> =A0 =A0 =A0 =A0yield(previous, 'middle')
> =A0 =A0 =A0end
> =A0 =A0 =A0previous =3D next_item
> =A0 =A0}
> =A0 =A0yield(previous, 'last') unless previous =3D=3D sigil
> =A0end
> end
>
>>> [1,2,3,4].each_with_position { |*x| p x }
> [1, "first"]
> [2, "middle"]
> [3, "middle"]
> [4, "last"]
> =3D> nil
>>> [].each_with_position { |*x| p x }
> =3D> nil
>>> [1].each_with_position { |*x| p x }
> [1, "last"]
> =3D> nil

Turns out we think along similar lines, although my solution is a bit
more wasteful:

module Enumerable
# will yield elements along with
# a symbol :first, :last, :middle or :only
def each_special
last =3D nil

each do |x|
if last
yield *last
last =3D [x, :middle]
else
# first iter
last =3D [x, :first]
end
end

yield last.first, last.last =3D=3D :middle ? :last : :only if last
self
end
end

[
[],
[1],
[1,2],
[1,2,3],
[1,2,3,4,5],
].each do |en|
p en

en.each_special do |x,t|
printf "%-10p %p\n", t, x
end

puts "end"
end

Kind regards

robert
--=20
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestprac...