views:

256

answers:

6

I tried to iterate backwards with ruby using a Range and each. This way:

(4..0).each do |i|
  puts i
end
==> 4..0

Iteration through 0..4 writes the numbers. On the other Range r = 4..0 seems to be ok, r.first == 4, r.last == 0. Seems to be strange to me that the construct above does not produce the expected result. What is the a reason for that? What are the situations when this behaviour is reasonable?

+12  A: 

A range is just that: something defined by its start and end, not by its contents. "Iterating" over a range doesn't really make sense in a general case. Consider, for example, how you would "iterate" over the range produced by two dates. Would you iterate by day? by month? by year? by week? It's not well-defined. IMO, the fact that it's allowed for forward ranges should be viewed as a convenience method only.

If you want to iterate backwards over a range like that, you can always use downto:

$ r = 10..6
=> 10..6

$ (r.first).downto(r.last).each { |i| puts i }
10
9
8
7
6

Here are some more thoughts from others on why it's tough to both allow iteration and consistently deal with reverse-ranges.

John Feminella
I think iterating over a range from 1 to 100 or from 100 to 1 intuitively means using step 1. If someone wants a different step, changes the default. Similarly, for me (at least) iterating from 1st of January to 16th of August means stepping by days. I think there is often something we can commonly agree on, because we intuitively mean it that way.Thanks for your answer, also the link you gave was useful.
fifigyuri
I still think defining "intuitive" iterations for many ranges is challenging to do consistently, and I don't agree that iterating over dates that way intuitively implies a step equal to 1 day -- after all, a day itself is already a range of time (from midnight to midnight). For example, who's to say that "January 1 to August 18" (exactly 20 weeks) doesn't imply an iteration of weeks instead of days? Why not iterate by hour, minute, or second?
John Feminella
The `.each` there is redundant, `5.downto(1) { |n| puts n }` works fine. Also, instead of all that r.first r.last stuff, just do `(6..10).reverse_each`.
Mk12
+5  A: 

Iterating over a range in Ruby with each calls the succ method on the first object in the range.

$ 4.succ
=> 5

And 5 is outside the range.

You can simulate reverse iteration with this hack:

(-4..0).each { |n| puts n.abs }

John pointed out that this will not work if it spans 0. This would:

>> (-2..2).each { |n| puts n*-1 }
2
1
0
-1
-2
=> -2..2

Can't say I really like any of them because they kind of obscure the intent.

Jonas Elfström
Clever hack! It won't work for ranges that span 0, though.
John Feminella
No but by multiplying by -1 instead of using .abs you can.
Jonas Elfström
A: 

if list is not that big. i think [*0..4].reverse.each { |i| puts i } is simplest way.

marocchino
IMO it is generally good to assume it is big. I think it is the right belief and habit to follow generally. And as the devil never sleeps, I do not trust myself that I remember where I iterated over an array.But you are right, if we have 0 and 4 constant, iterating over array might not cause any problem.
fifigyuri
A: 

I add one another possibility how to realise iteration over reverse Range. I do not use it, but it is a possibility. It a bit risk monkey patching ruby core objects.

class Range

  def each(&block)
    direction = (first<=last ? 1 : -1)
    i = first
    not_reached_the_end = if first<=last
                            lambda {|i| i<=last}
                          else
                            lambda {|i| i>=last}
                          end
    while not_reached_the_end.call(i)
      yield i
      i += direction
    end
  end
end

Realisation of this iteration wasn't my main intention, I explained myself in a comment to the question.

fifigyuri
+2  A: 

According to the book "Programming Ruby", the Range object stores the two endpoints of the range and uses the .succ member to generate the intermediate values. Depending on what kind of data type you are using in your range, you can always create a subclass of Integer and re-define the .succ member so that it acts like a reverse iterator (you would probably also want to re-define .next as well).

You can also achieve the results you are looking for without using a Range. Try this:

4.step(0, -1) do |i|
    puts i
end

This will step from 4 to 0 in steps of -1. However, I don't know if this will work for anything except Integer arguments.

bta
A: 

As bta said, the reason is that Range#each sends succ to its beginning, then to the result of that succ call, and so on until the result is greater than the end value. You can't get from 4 to 0 by calling succ, and in fact you already start out greater than the end.

Chuck