Range and reversed Range

I'm looking to have one variable that contains either a range, or a reversed range so I can use it in a for loop:

let r = if start < end { start..end } else { (start..end).rev() };

This doesn't work because the two legs of the if statement are different... how can I make this work?

2 Likes

Hiya! I'm not sure exactly what you mean by the two legs are different, do you want it to always go from "smallest number to biggest number", for any two numbers start and end?

Does this help (playpen):

fn print_range(start: i32, end: i32) {
    let items = if start < end {
        println!("start is smaller");
        (start..end).collect::<Vec<i32>>()
    } else {
        println!("end is smaller");
        (end..start).collect::<Vec<i32>>()
    };

    for i in items {
        println!("{}", i);
    }
}

fn main() {
    print_range(0, 3);
    print_range(3, 0);
}

The output is:

start is smaller
0
1
2
end is smaller
0
1
2
1 Like

.rev() is making the range into an iterator and reversing it. If you want the two arms to return the same type, you can probably use .iter() or .into_iter() there.

But if you want a std::ops::Range for each, you need to use the two values in the other order, as in the answer above (without collect). I'd rename them as a and b and use a..b or b..a in the if.

But also beware of off-by-one because the non-inclusive range, you might need to add or remove 1 (or use ..= inclusive range in nightly)

Alternate idea that might show intent more clearly

let mut v = [a, b];
v.sort();
let r = v[0] .. v[1];

The easiest approach is to put the ranges into a Box and thus form an Iterator trait object, and then iterate over that.

play that piggybacks on @azriel91’s version but uses type erasure.

By the way, should note that (start..end).rev() will be an empty iterator if start > end; the rev() doesn't change that. So in fact, you should probably just do:

let r = if start < end { start..end } else { end..start };

Then there's a single type involved, and no need for type erasure shenanigans.

1 Like

To elaborate on some previous answers:

This is not correct. A Range is, in fact, already an Iterator. The trouble here is that rev() (and virtually all other iterator methods) returns an adapter type, and so it is almost never possible to have two iterator chains with the same statically known type.

let r = if start < end {
    start..end          // this is a Range<usize>
} else {
    (start..end).rev()  // this is a Rev<Range<usize>>
}

This is why @azriel91's solution of collecting into a Vector, or @vitalyd's solution of type erasure are necessary.

There is also a third solution. (actually, it's just a small generalization of @azriel91's) You can take the part of the code after the construction of the range, and move it into a generic function. This will work as long as you can pull out all of the code up to the point where you consume the iterator.

For instance, you can transform this: (which doesn't compile)

let a = 2;
let b = 7;
let skip_evens = true;
let nums = if skip_evens {
    (a..b).filter(|x| x % 2 == 1)
} else {
    (a..b)
};
for n in nums {
    println!("{}", n);
} // at this point nums has been consumed

into this:

let a = 2;
let b = 7;
let skip_evens = true;
if skip_evens {
    rest_of_fn((a..b).filter(|x| x % 2 == 1))
} else {
    rest_of_fn((a..b))
}

fn rest_of_fn<I: Iterator<Item=usize>>(nums: I) {
    for n in nums {
        println!("{}", n);
    }
}
1 Like

Sorry, my ask wasn't clear enough. What I'm looking for is an easy way to construct a Range that can be used with a for loop inside a function, where I can pass in start and end with the following properties:

  • When start < end; produce the Range [start, start+1, start+2, ... end)
  • When start > end; produce the Range (start, start-1, start-2, ... end]

This is why I'm looking to call rev() so that I get decrementing numbers in the second case. However, even using type erasure I'm still getting errors about if and else having incompatible types: Rust Playground

Re: @azriel91's suggestion, I don't want to use collect because abs(start, end) = 100M, and there's no point allocating all that memory just for a loop iterator.

Re: @vitalyd's suggesstion, I cannot get Box to work.

Re: @ExpHP's suggestion, I'm not really seeing how specifying a second function helps.

Overall, it just seems like something is missing if I cannot do this somewhat gracefully.

let items = if start < end {
        println!("start is smaller");
        Box::new((start..end)) as Box<Iterator<Item = _>>
    } else {
        println!("end is smaller");
        Box::new((end..start).rev())
    };

You can also avoid a Box altogether if you want:

    let mut rf = start..end;
    let mut rb = (end..start).rev();
    let items = if start < end {
        println!("start is smaller");
        &mut rf as &mut Iterator<Item = _>
    } else {
        println!("end is smaller");
        &mut rb
    };

    for i in items {
        println!("{}", i);
    }
1 Like

It was the cast as Box<Iterator<Item=_>> that I was missing. Thanks!

As an FYI, you can also force the coercion by specifying the type on the let binding: let items: Box<Iterator<Item = _>> = .... You then don't need the as coercion.

Let's not forget about the either crate! Playground

    let iter = if start < end {
        either::Either::Left(start..end+1)
    } else {
        either::Either::Right( (end..start+1).rev() )
    };
3 Likes