I'm writing a for loop in Ruby that iterates through the elements of an array until there are not more elements to read or a condition is met. This is my implementation:
# 'all' is an array
exists = false
for i in all && !exists
exists = all[i].has_card
end
As the title says, I'm getting this runtime error:
undefined method `each' for true:TrueClass (NoMethodError)
I'm fairly new to Ruby, but I'm guessing the problem is that the for
loop is trying to iterate over both all
and !exists
which is true
. How can I write a for that works the way I want?
EDIT: If it serves as clarification, this is how I would implement it in C++:
for (int i=0; i<all.size() && !exists; i++) { /*...*/ }
First of all, eww. for
is never used; it's the "moist" of Ruby. :)
for item in items ... end
is executed by Ruby as if you wrote items.each do |item| ... end
. (That's actually how Rubyists would write it in the first place.) This is relevant; so hang on.
Your line parses as:
for i in (all && (!exists))
When exists
is false
, !exists
is true
. Now, the &&
operator will check if the first argument is truthy; if it is not, it returns it; if it is, it returns the second argument. Since all
is presumably truthy, all && true
returns the second argument — true.
Now remember that your line actually, in the background, calls the each
method on the collection you are iterating. And your "collection" is true
— not a collection at all. You can't iterate on true
.
If you don't want to use break
(which is somewhat unreasonable, and you can quote me), you can use other methods from the Enumerable
module. For example:
all.take_while { |item| !item.has_card }.each do |item|
# ...
end
This is a bit slower than the break
method, since it will construct an intermediary array containing only the first N items that don't have cards that it will later iterate on, but worrying about it is a premature optimisation.
EDIT: Unfortunately Sebastian deleted their answer, which had the correct idiom (the one I would probably write sooner than the one above, though it definitely depends on the case), so I repeat it here:
all.each do |item|
break if item.has_card
# ...
end