43

code:

 c = 0  
 items.each { |i|  
   puts i.to_s    
   # if c > 9 escape the each iteration early - and do not repeat  
   c++  
 }  

I want to grab the first 10 items then leave the "each" loop.

What do I replace the commented line with? is there a better approach? something more Ruby idiomatic?

BuddyJoe
  • 69,735
  • 114
  • 291
  • 466
  • I suggest nimrodm's answer that uses take: http://stackoverflow.com/questions/1568288/escaping-the-each-iteration-early-in-ruby/1568445#1568445 – Robert K Oct 14 '09 at 19:36

8 Answers8

61

While the break solution works, I think a more functional approach really suits this problem. You want to take the first 10 elements and print them so try

items.take(10).each { |i| puts i.to_s }
nimrodm
  • 23,081
  • 7
  • 58
  • 59
  • 15
    Shorter: `puts items.take(10)` – Telemachus Oct 14 '09 at 19:45
  • I tried Googling for 'ruby take method' just now and couldn't find a reference to what module `take` is in. Where is it in the API? – Sarah Vessels Oct 14 '09 at 19:53
  • 1
    I suppose I need to upgrade my Ruby version: `Nothing known about Array#take` – Chris Lutz Oct 14 '09 at 20:02
  • Or ri Enumerable#take, I suppose. Note that it appears to be new in Ruby 1.9. I get a no method error in Ruby 1.8.6 trying it. – Telemachus Oct 14 '09 at 20:04
  • 1
    `Array#take` is present in Ruby 1.8.7 and up. – Chuck Oct 14 '09 at 20:07
  • Ruby 1.8.7 has it, otherwise you can monkey-patch it into Array: `class Array; def take(how_many); self[0..how_many]; end`. AFAIK this is simple and reliable. – Robert K Oct 15 '09 at 15:10
  • ahhh. I'm down with FP. "You know me!". +1 and answer. I like this better than previous answer. But good to know the break option is there. break lines are easy to comment out during testing. – BuddyJoe Oct 19 '09 at 21:08
55

There is no ++ operator in Ruby. It's also convention to use do and end for multi-line blocks. Modifying your solution yields:

c = 0  
items.each do |i|  
  puts i.to_s    
  break if c > 9
  c += 1 
end

Or also:

items.each_with_index do |i, c|  
  puts i.to_s    
  break if c > 9
end

See each_with_index and also Programming Ruby Break, Redo, and Next.

Update: Chuck's answer with ranges is more Ruby-like, and nimrodm's answer using take is even better.

Community
  • 1
  • 1
Sarah Vessels
  • 30,930
  • 33
  • 155
  • 222
  • Thanks. Answer and +1. Wow I was way off on the initial syntax. – BuddyJoe Oct 14 '09 at 19:07
  • You weren't far off, really: the only invalid part of your answer was the `++`. Curly braces for blocks will work, it's just not preferred for multi-line blocks; see http://stackoverflow.com/questions/533008/what-is-the-difference-or-value-of-these-block-coding-styles-in-ruby – Sarah Vessels Oct 14 '09 at 19:12
  • I like your first solution because if you want to loop over 100 items every time but only take out 10 conditionally you can increment the counter independently. – chrisallick Dec 11 '14 at 04:33
7

break works for escaping early from a loop, but it's more idiomatic just to do items[0..9].each {|i| puts i}. (And if all you're doing is literally printing the items with no changes at all, you can just do puts items[0..9].)

Chuck
  • 234,037
  • 30
  • 302
  • 389
4

Another variant:

puts items.first(10)

Note that this works fine with arrays of less than 10 items:

>> nums = (1..5).to_a
=> [1, 2, 3, 4, 5]
>> puts nums.first(10)
1
2
3
4
5

(One other note, a lot of people are offering some form of puts i.to_s, but in such a case, isn't .to_s redundant? puts will automatically call .to_s on a non-string to print it out, I thought. You would only need .to_s if you wanted to say puts 'A' + i.to_s or the like.)

Telemachus
  • 19,459
  • 7
  • 57
  • 79
4

Another option would be

items.first(10).each do |i|
  puts i.to_s
end

That reads a little more easily to me than breaking on an iterator, and first will return only as many items as available if there aren't enough.

Emily
  • 17,813
  • 3
  • 43
  • 47
1

Does this look like what you want?

10.times { |i|
  puts items[i].to_s
}
Chris Lutz
  • 73,191
  • 16
  • 130
  • 183
0
items.each_with_index { |i, c| puts i and break if c <= 9 }
khelll
  • 23,590
  • 15
  • 91
  • 109
  • That will break after the first item. – Chuck Oct 14 '09 at 19:25
  • Not really, how did u test it? – khelll Oct 14 '09 at 19:32
  • 1
    @Khelll: is this due to lazy evaluation of `and`? It works, but it's a bit too clever for me. My brain keeps wanting `>=` since I see "and break if" together. – Telemachus Oct 14 '09 at 20:05
  • Ok, now I see it: `puts` returns `nil` when it works. Thus, as long as the index is equal to or less than 9, the `puts` happens, `nil` is returned and the second half of the `and` is not evaluated. When the index hits 10, the `puts` doesn't happen and the second half of the `and` gets evaluated. At that point, boom: `break`. – Telemachus Oct 14 '09 at 20:24
  • 3
    OK, I had it backwards before, but it's still wrong: This *never* breaks. Look at it like this: `if c <= 9; puts i; break; end`. The `and break` is never executed because `puts i` is always nil and once c>9, the entire body of the if-statement is no longer executed. Replace the `break` with `(puts "YOU WON'T SEE THIS")` if you want to prove that that branch is never reached. – Chuck Oct 14 '09 at 21:57
  • 1
    @Chuck: thanks for one more round. @Khelll: I think we've proven that it doesn't read very naturally. – Telemachus Oct 14 '09 at 23:03
-2

It was asked:

I want to grab the first 10 items then leave the "each" loop.

Use throw and catch to accomplish this, with few changes to the example:

catch(:done) do
    c = 0
    collected = []
    items.each do |item|
        collected << item
        throw(:done, collected) if c == 9 # started at 0
        c += 1
    end
    collected # if the list is less than 10 long, return what was collected
end

Simply throw the label :done with collected and the catch which is waiting for :done will return collected.

And to "ruby" this up a bit:

catch(:done) do
    items.inject([]) do |collected, item|
        throw(:done, collected) if collected.size == 10
        collected << item # collected gets returned here and populates the first argument of this block
    end
end

I do not know why some people refuse to use inject and use reduce instead (they are equivalent) when clearly the empty array given to inject([]) is being injected with items! Anyhow, the inject will return collected if there are less than 10 items.

Most answers are trying to answer what might be the intent of the question instead of what was asked and items.take(10) does make perfect sense in that case. But I can imagine wanting to grab the first items that fit within my $100 budget. Then you can simply:

catch(:done) do
    items.inject({items: [], budget: 100}) do |ledger, item|
        remainder = ledger[:budget] - item.price
        if remainder < 0
            throw(:done, ledger)
        else
            ledger.tap do |this|
                this[:items] << item
                this[:budget] = remainder
            end # tap just returns what is being tapped into, in this case, ledger
        end
    end
end
Nothus
  • 1,127
  • 8
  • 7
  • You took a simple question and gave a really complicated answer. There's no need to use `throw` and `catch` here or turn this into 13 lines of deeply nested code. Keep it simple. – NeuroXc Oct 09 '17 at 22:57
  • My answer is 6 lines, it shows an alternative way to escape an each loop which was asked, it is nested one level deeper than most of the answers. My hope in leaving this answer was to show how to alternatively get out of a loop taking advantage of this simple context. If you had actually read my post, my 13 lines of code is a more complex answer to a more complex example I posed in my answer. I apologize in advance for having too many words in this response. – Nothus Oct 17 '17 at 06:14