104

If i have a loop such as

users.each do |u|
  #some code
end

Where users is a hash of multiple users. What's the easiest conditional logic to see if you are on the last user in the users hash and only want to execute specific code for that last user so something like

users.each do |u|
  #code for everyone
  #conditional code for last user
    #code for the last user
  end
end
Nakilon
  • 34,866
  • 14
  • 107
  • 142
Splashlin
  • 7,225
  • 12
  • 46
  • 50
  • Do you really mean a hash? Ordering on a hash is not always reliable (depending on how you add things to the hash and what ruby version you are using). With unreliable ordering the 'last' item will not be consistent. The code in the question and the answers you are getting are more appropriate to an array or enumerable. For example, hashes don't have `each_with_index` or `last` methods. – Shadwell Oct 22 '10 at 20:49
  • 1
    `Hash` mixes in Enumerable, so it does have `each_with_index`. Even if the hash keys aren't sorted, this sort of logic comes up all the time when rendering views, where the last item might be displayed differently regardless of whether it's actually "last" in any data-meaningful sense. – Raphomet Oct 22 '10 at 21:04
  • Of course, quite right, hashes do have `each_with_index`, apologies. Yep, I can see that it would come up; just trying to clarify the question. Personally the best answer for me is the use of `.last` but that doesn't apply for a hash only an array. – Shadwell Oct 22 '10 at 21:09
  • Duplicate of [Magic First and Last Indicator in a Loop in Ruby/Rails?](http://stackoverflow.com/questions/2241684/magic-first-and-last-indicator-in-a-loop-in-ruby-rails), apart from this being a hash rather than an array. – Andrew Grimm Oct 24 '10 at 22:57
  • 1
    @Andrew agree its totally related, however meagars awesome little answer shows how it is not exactly a dupe. – Sam Saffron Oct 25 '10 at 00:46
  • @Sam: Could Meagar's answer be appropriate for the other question as well? – Andrew Grimm Oct 25 '10 at 01:24
  • @Andrew , I'm not sure I don't think so, the other question has no common clause for all the items in the collection – Sam Saffron Oct 25 '10 at 01:28

9 Answers9

163
users.each_with_index do |u, index|
  # some code
  if index == users.size - 1
    # code for the last user
  end
end
Raphomet
  • 3,589
  • 1
  • 23
  • 12
  • 4
    Problem with this is a conditional is ran through each time. use `.last` outside the loop. – bcackerman Aug 18 '13 at 20:01
  • 4
    I'd just add that if you're iterating over a hash, you have to write it like: `users.each_with_index do |(key, value), index| #your code end` – HUB Aug 27 '14 at 13:05
  • I know it's not entirely the same question, but this code works better I think if you want to do something to everyone BUT the last user, so I'm upvoting since that was what I was looking for – WhiteTiger Dec 11 '15 at 23:19
39

If it's an either/or situation, where you're applying some code to all but the last user and then some unique code to only the last user, one of the other solutions might be more appropriate.

However, you seem to be running the same code for all users, and some additional code for the last user. If that's the case, this seems more correct, and more clearly states your intent:

users.each do |u|
  #code for everyone
end

users.last.do_stuff() # code for last user
user229044
  • 232,980
  • 40
  • 330
  • 338
  • 2
    +1 for not needing a conditional, if that's appropriate. (of course, I did here) – Jeremy Oct 22 '10 at 22:44
  • @meagar won't this loop trough users twice? – ant Sep 16 '15 at 16:00
  • @ant No, there is one loop and one call to `.last` which has nothing to do with looping. – user229044 Sep 16 '15 at 16:21
  • @meagar so in order to get to the last it doesn't internally loop until the last element? it has a way of accessing the element directly without looping? – ant Sep 16 '15 at 16:29
  • 1
    @ant No, there is no internal looping involved in `.last`. The collection is already instantiated, it's just a simple accessing of an array. Even if the collection had not already been loaded (as in, it was still an unhydrated ActiveRecord relation) `last` still never *loops* to get the last value, that would be wildly inefficient. It simply modifies the SQL query to return the last record. That said, this collection has already been loaded by the `.each`, so there is no more complexity involved than if you did `x = [1,2,3]; x.last`. – user229044 Sep 16 '15 at 16:32
  • @meagar gotcha, thanks for the explanation, well said! – ant Sep 18 '15 at 19:22
  • @ant I'm late to the party here but something that might be too obvious to @meagar to mention but wasn't always to me: Unlike an AR relation which will need to run an (efficient) database search to find the last item, a ruby array or hash already knows its own length. It has it pre-stored as an internal instance variable. This means it already knows exactly where to find the last item. In general Methods like `count`, `last` or `find` may do the same thing for different data structures / AR objects but work very differently underneath. – Adamantish Jan 19 '17 at 14:41
22

I think a best approach is:

users.each do |u|
  #code for everyone
  if u.equal?(users.last)
    #code for the last user
  end
end
Stiig
  • 1,265
  • 13
  • 19
Alter Lagos
  • 12,090
  • 1
  • 70
  • 92
  • 27
    The problem with this answer is that if the last user also occurs earlier in the list, then the conditional code will get called multiple times. – evanrmurphy Dec 11 '12 at 05:07
  • 1
    you have to use `u.equal?(users.last)`, the `equal?` method compares the object_id, not the value of the object. But this will not work with symboles and numbers. – Nafaa Boutefer Jan 30 '16 at 10:50
12

Have you tried each_with_index?

users.each_with_index do |u, i|
  if users.size-1 == i
     #code for last items
  end
end
pdobb
  • 17,688
  • 5
  • 59
  • 74
Teja Kantamneni
  • 17,402
  • 12
  • 56
  • 86
8
h = { :a => :aa, :b => :bb }
h.each_with_index do |(k,v), i|
  puts ' Put last element logic here' if i == h.size - 1
end
DigitalRoss
  • 143,651
  • 25
  • 248
  • 329
6

Another solution is to rescue from StopIteration:

user_list = users.each

begin
  while true do
    user = user_list.next
    user.do_something
  end
rescue StopIteration
  user.do_something
end
ricardokrieg
  • 733
  • 8
  • 11
  • 6
    This is not a good solution to this problem. Exceptions shouldn't be abused for simple flow control, they're for *exceptional* situations. – user229044 Jan 15 '14 at 17:24
  • @meagar This is actually nearly quite cool. I agree that **unrescued** exceptions or ones that need to be passed up to a higher method should only be for exceptional situations. This, however, is a neat (and apparently only) way of getting access to ruby's native knowledge of where an iterator ends. If there is another way that, is of course, preferred. The only real issue I'd take with this is that it appears to skip and do nothing for the first item. Fixing that would take it over the boundary of clunkiness. – Adamantish Jan 19 '17 at 14:53
  • 3
    @meagar Sorry for an "argument from authority," but Matz disagrees with you. In fact, `StopIteration` is designed for the precise reason of handling loop exits. From Matz's book: "This may seem unusual—an exception is raised for an expected termination condition rather than an unexpected and exceptional event. (`StopIteration` is a descendant of `StandardError` and `IndexError`; note that it is one of the only exception classes that does not have the word “error” in its name.) Ruby follows Python in this external iteration technique. (more...) – BobRodes Jan 18 '20 at 23:05
  • (...) By treating loop termination as an exception, it makes your looping logic extremely simple; there is no need to check the return value of `next` for a special end-of-iteration value, and there is no need to call some kind of `next?` predicate before calling `next`." – BobRodes Jan 18 '20 at 23:06
  • 1
    @Adamantish It should be noted that `loop do` has an implicit `rescue` when encountering a `StopIteration`; it's specifically used when externally iterating an `Enumerator` object. `loop do; my_enum.next; end` will iterate `my_enum` and exit at the end; no need to put a `rescue StopIteration` in there. (You do have to if you use `while` or `until`.) – BobRodes Jan 18 '20 at 23:29
  • @BobRodes That's extremely interesting. Which book is that? – user229044 Jan 18 '20 at 23:42
  • @meagar I thought so too when I read it. It's one interesting book, and both readable and informative. [Here's](https://www.amazon.com/Ruby-Programming-Language-Everything-Need/dp/0596516177) the Amazon link. – BobRodes Jan 19 '20 at 06:03
  • While better answers were given for the op's problem, this one is the only or one of the only options for generic `Enumerator` use cases. – akostadinov Jan 01 '23 at 22:45
5

You can use @meager's approach also for an either/or situation, where you're applying some code to all but the last user and then some unique code to only the last user.

users[0..-2].each do |u|
  #code for everyone except the last one, if the array size is 1 it gets never executed
end

users.last.do_stuff() # code for last user

This way you don't need a conditional!

coderuby
  • 1,188
  • 1
  • 11
  • 26
  • @BeniBela No, [-1] is the last element. There is no [-0]. So the second to last is [-2]. – coderuby May 21 '16 at 14:07
  • `users[0..-2]` is correct, as is `users[0...-1]`. Note the different range operators `..` vs `...`, see http://stackoverflow.com/a/9690992/67834 – Eliot Sykes Jan 12 '17 at 15:47
3

Sometimes I find it better to separate the logic to two parts, one for all users and one for the last one. So I would do something like this:

users[0...-1].each do |user|
  method_for_all_users user
end

method_for_all_users users.last
method_for_last_user users.last
xlembouras
  • 8,215
  • 4
  • 33
  • 42
0

There are no last method for hash for some versions of ruby

h = { :a => :aa, :b => :bb }
last_key = h.keys.last
h.each do |k,v|
    puts "Put last key #{k} and last value #{v}" if last_key == k
end
  • Is it an answer that improves someone else's answer? Please post which answer mentions 'last' method and what do you propose to overcome this issue. – Artemix Jul 23 '13 at 07:28
  • For the versions of Ruby which don't have a `last`, the set of keys will be unordered, so this answer will return wrong/random results anyways. – user229044 Jan 15 '14 at 17:32