Search code examples
rustiteratorrange

Dynamically create a range in either direction in Rust


I am learning Rust and recently went through an exercise where I had to iterate through numbers that could go in either direction. I tried the below with unexpected results.

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Point {
    x: i32,
    y: i32
}

fn test() {
    let p1 = Point { x: 1, y: 8 };
    let p2 = Point { x: 3, y: 6 };

    let all_x = p1.x..=p2.x;
    println!("all_x: {:?}", all_x.clone().collect::<Vec<i32>>());
    let all_y = p1.y..=p2.y;
    println!("all_y: {:?}", all_y.clone().collect::<Vec<i32>>());
    
    let points: Vec<Point> = all_x.zip(all_y).map(|(x, y)| Point { x, y }).collect();

    println!("points: {:?}", points);
}

The output was

all_x: [1, 2, 3]
all_y: []
points: []

After some googling I found an explanation and some old answers which basically amount to use (a..b).rev() as needed.

My question is, how do I do this in a dynamic way? If I use an if...else like so

let all_x = if p1.x < p2.x { (p1.x..=p2.x) } else { (p2.x..=p1.x).rev() };

I get a type error because the else is different than the if

   |
58 |       let all_x = if p1.x < p2.x { (p1.x..=p2.x) }
   |                   -                ------------- expected because of this
   |  _________________|
   | |
59 | |     else { (p2.x..=p1.x).rev() };
   | |____________^^^^^^^^^^^^^^^^^^^_- `if` and `else` have incompatible types
   |              |
   |              expected struct `RangeInclusive`, found struct `Rev`
   |
   = note: expected type `RangeInclusive<_>`
            found struct `Rev<RangeInclusive<_>>`

After trying a bunch of different variations on let all_x: dyn Range<Item = i32>, let all_x: dyn Iterator<Item = i32>, etc, the only way I have managed to do this is by turning them into collections and then back to iterators.

let all_x: Vec<i32>;
if p1.x < p2.x { all_x = (p1.x..=p2.x).collect(); }
else { all_x = (p2.x..=p1.x).rev().collect(); }
let all_x = all_x.into_iter();
println!("all_x: {:?}", all_x.clone().collect::<Vec<i32>>());

let all_y: Vec<i32>;
if p1.y < p2.y { all_y = (p1.y..=p2.y).collect(); }
else { all_y = (p2.y..=p1.y).rev().collect(); }
let all_y = all_y.into_iter();
println!("all_y: {:?}", all_y.clone().collect::<Vec<i32>>());

which provides the desired outcome

all_x: [1, 2, 3]
all_y: [8, 7, 6]
points: [Point { x: 1, y: 8 }, Point { x: 2, y: 7 }, Point { x: 3, y: 6 }]

but is a bit repetitive, inelegant and I'm assuming not very efficient at large numbers. Is there a better way to handle this situation?

NOTE: Sorry for including the Point struct. I could not get my example to work with x1, x2, etc. Probably a different question for a different post lol.


Solution

  • You can dynamically dispatch it. Wrapping them into a Box and returning a dynamic object, an Iterator in this case. For example:

    fn maybe_reverse_range(init: usize, end: usize, reverse: bool) -> Box<dyn Iterator<Item=usize>> {
        if reverse {
            Box::new((init..end).rev())
        } else {
            Box::new((init..end))
        }
    }
    

    Playground