Idiomatic way to detect first/last iteration?

K

Kendall Gifford

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.
 
J

John W Higgins

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

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
 
K

Kendall Gifford

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).
 
M

Matthew K. Williams

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
 
B

Brad Ediger

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

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)
 
M

Matthew K. Williams

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
 
G

Gary Wright

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
 
C

Colin Bartlett

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
 
T

trans

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.
 
R

Robert Klemme

2009/6/11 Gary Wright said:
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 :eek:nly
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 : :eek:nly 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.rubybestpractices.com/
 
P

paron

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.

Is detection really what you want? Doesn't that imply that you test
each iteration? Since the array is ordered by definition, it seems
(needs tested!) that creating three entities might be faster.

in helper:
def first_also_ran_last(records)

first_record=records.shift
last_record=records.pop

return [first_record, records, last_record]
end

in view:
<% parsed_records = first_also_ran_last(@whatever)
<%="Really like #{parsed_records[0]}"%>
<% parsed_records[1].each do |also_ran|%> #(if any)
<%= "Sorta like #{also_ran}"%>
<%end -%>
<%unless parsed_records[2].nil?-%>
<%= "Don't much like #{parsed_records[2]}"%>
<%end->

Don't know if it's faster, or more idiomatic, but perhaps it's more
semantic?

Ron
 
H

Harry Kakueki

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.

This is similar to some other idea already posted but I did not
understand what you did not like about it.

If you just want to focus on the first and last, not the third or
something else, what is wrong with this?

af,lf = genres.shift, genres.pop
puts "#{af} is my absolute favorite"
genres.each {|genre| puts "#{genre} is one of my favorites"}
puts "#{lf} is my least fav. of all my favorites"


Harry
 
E

Eleanor McHugh

Is detection really what you want? Doesn't that imply that you test
each iteration? Since the array is ordered by definition, it seems
(needs tested!) that creating three entities might be faster.

in helper:
def first_also_ran_last(records)

first_record=records.shift
last_record=records.pop

return [first_record, records, last_record]
end

in view:
<% parsed_records = first_also_ran_last(@whatever)
<%="Really like #{parsed_records[0]}"%>
<% parsed_records[1].each do |also_ran|%> #(if any)
<%= "Sorta like #{also_ran}"%>
<%end -%>
<%unless parsed_records[2].nil?-%>
<%= "Don't much like #{parsed_records[2]}"%>
<%end->

Don't know if it's faster, or more idiomatic, but perhaps it's more
semantic?

With a bit of tidying you can reduce the view further:

def x records, message = "Nothing Relevant" # I can't think of a
_good_ name for the method off-hand
first = records.shift || message
last = records.pop || message
[first, records.empty? ? [message] || records, last]
end

<% track_listing = x records, "I don't have one!!!" %>
<%= "My favourite track: #{track_listing.shift} %>
<% track_listing.shift.each do |track| -%>
<%= "Another track I like: #{track} %>
<% end -%>
<%= "The track I like least: #{track_listing.shift} %>


Ellie

Eleanor McHugh
Games With Brains
http://slides.games-with-brains.net
 
G

Gary Wright

(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 ##+ )

Because you need an object that is guaranteed to not be
in the enumeration. You can't use nil because that could
be the last item in the collection, which in my code would
prevent the last nil from being yielded to the block.

Gary Wright
 
G

Gary Wright

first_record=records.shift
last_record=records.pop

You can't use 'shift' and 'pop' if you want the
solution to work with *any* enumerable object.

It just depends how general a solution you are
looking for.
 
H

Harry Kakueki

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.

Or how about something like this?

fav = Hash.new(" is one of my favorites")
fav[0] = " is my absolute favorite"
fav[genres.length-1] = " is my least fav. of all my favorites"

puts genres.zip(Array.new(genres.length){|i| fav}).map{|x| x.join}


Harry
 
R

Robert Dober

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|
=A0desc =3D "one of my favorites"
=A0desc =3D "my absolute favorite" if i.zero?
=A0desc =3D "my least fav. of all my favorites" if i + 1 =3D=3D genres.le= ngth
=A0puts "#{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.
Kendall

streams might give you the abstraction you need

--------------- 8< ------------------------
require 'lab419/functional/streams/core'
require 'lab419/functional/streams/as-enums'

x =3D %w{a b c a b}.to_stream

x.each do | s |
p [ s.head, s.tail =3D=3D EmptyStream ]
end
----------------- >8 ----------------

I might implement SimpleStream#first?, SimpleStream#last? and
SimpleStream#middle? if I find some spare time, and if anybody needs
it ;)

I admit however that there is still a little price to pay, you might
have noticed that s is a stream and not an element of the stream, but
that is just the way streams work.

HTH
Robert



--=20
Toutes les grandes personnes ont d=92abord =E9t=E9 des enfants, mais peu
d=92entre elles s=92en souviennent.

All adults have been children first, but not many remember.

[Antoine de Saint-Exup=E9ry]
 
P

paron

You can't use 'shift' and 'pop' if you want the
solution to work with *any* enumerable object.

It just depends how general a solution you are
looking for.

Good point; I'd forgotten that case.

There's really no inherently meaningful "first" or "last" with a Hash,
so I ignored it. In fact, couldn't you consider "first" and "last"
meaningless for any Enumerable that isn't indexed? Maybe the question
shouldn't be about Enumerable.

Anyway, good catch.

Ron
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,999
Messages
2,570,246
Members
46,839
Latest member
MartinaBur

Latest Threads

Top