Is there a reason that we cannot iterate on “reverse Range” in ruby?

0 votes
asked Jan 15, 2010 by fifigyuri

I tried to iterate backwards with using a Range and each:

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

It 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?

10 Answers

0 votes
answered Jan 15, 2010 by john-feminella

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.

0 votes
answered Jan 15, 2010 by marocchino

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

0 votes
answered Jan 15, 2010 by fifigyuri

I add one another possibility how to realise iteration over reverse Range. I do not use it, but it is a possibility. It is a bit risky to monkey patch 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
0 votes
answered Jan 15, 2010 by bta

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.

0 votes
answered Jan 16, 2010 by chuck

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.

0 votes
answered Jan 10, 2013 by bakerstreet221b

You can even use a for loop:

for n in 4.downto(0) do
  print n
end

which prints:

4
3
2
1
0
0 votes
answered Jan 9, 2014 by sergey-kishenin

Another way is (1..10).to_a.reverse

0 votes
answered Jan 13, 2014 by marko-taponen

How about (0..1).reverse_each which iterates the range backwards?

0 votes
answered Jan 22, 2016 by forforf

This worked for my lazy use case

(-999999..0).lazy.map{|x| -x}.first(3)
#=> [999999, 999998, 999997]
0 votes
answered Sep 15, 2017 by android-weasel

The OP wrote

It 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?

not 'Can it be done?' but to answer the question that wasn't asked before getting to the question that was actually asked:

$ irb
2.1.5 :001 > (0..4)
 => 0..4
2.1.5 :002 > (0..4).each { |i| puts i }
0
1
2
3
4
 => 0..4
2.1.5 :003 > (4..0).each { |i| puts i }
 => 4..0
2.1.5 :007 > (0..4).reverse_each { |i| puts i }
4
3
2
1
0
 => 0..4
2.1.5 :009 > 4.downto(0).each { |i| puts i }
4
3
2
1
0
 => 4

Since reverse_each is claimed to build an entire array, downto is clearly going to be more efficient. The fact that a language designer could even consider implementing things like that kinda ties into the answer to the actual question as asked.

To answer the question as actually asked...

The reason is because Ruby is an endlessly surprising language. Some surprises are pleasant, but there is a lot of behaviour which is downright broken. Even if some of these following examples are corrected by newer releases, there are plenty of others, and they remain as indictments on the mindset of the original design:

nil.to_s
   .to_s
   .inspect

results in "" but

nil.to_s
#  .to_s   # Don't want this one for now
   .inspect

results in

 syntax error, unexpected '.', expecting end-of-input
 .inspect
 ^

You would probably expect << and push to be the same for appending to arrays, but

a = []
a << *[:A, :B]    # is illegal but
a.push *[:A, :B]  # isn't.

You would probably expect 'grep' to behave like its Unix command-line equivalent, but it does === matching not =~, despite its name.

$ echo foo | grep .
foo
$ ruby -le 'p ["foo"].grep(".")'
[]

Various methods are unexpectedly aliases for each other, so you have to learn multiple names for the same thing - e.g. find and detect - even if you do like most developers and only ever use one or the other. Much the same goes for size, count, and length, except for classes which define each differently, or don't define one or two at all.

Unless someone has implemented something else - like the core method tap has been redefined in various automation libraries to press something on the screen. Good luck finding out what's going on, especially if some module required by some other module has monkeyed yet another module to do something undocumented.

The environment variable object, ENV does not support 'merge', so you have to write

 ENV.to_h.merge('a': '1')

As a bonus, you can even redefine your or someone else's constants if you change your mind about what they should be.

Welcome to Q&A, where you can ask questions and receive answers from other members of the community.
Website Online Counter

...