traversing a grid up or down without having to write the same loop twice

Viewed 106

I'm working on moving in a vector that acts as a representation of a 2d grid, and I want to be able to move straight up or down, but don't want to rewrite the for loop twice. I came up with this solution that fails due to the borrow checker when indexing into the grid.

// fill the vec later
let grid: Vec<u8> = Vec::with_capacity(WIDTH*HEIGHT);
let mut current_position = Point::new(somewhere);
let end = Point::new(somewhere_else);
let moving: &mut usize;
let to: u32;

if moving_horizontal {
    moving = &mut current_position.x;
    to = end.x;
}
else {
    moving = &mut current_position.y;
    to = end.y;
}

for _ in *moving..=to {
    // do stuff with this
    grid[current_position.x+current_position.y*HEIGHT];
    *moving += 1;
}

Is there any neat solution to this or do I have to just write the same for loop twice in each block of my conditional statements?

3 Answers

Please don't too harsh on me. I've just started a few days ago to learn Rust. This is just an idea I had and I'm posting it to learn if it works in this situation or not.

let mut current_position = Point::new(somewhere);
let end = Point::new(somewhere_else);
let moving: &mut usize;
let not_moving: &usize;
let to: u32;

if moving_horizontal {
    moving = &mut current_position.x;
    not_moving = &current_position.y;
    to = end.x
} else {
    moving = &mut current_position.y;
    not_moving = &current_position.x;
    to = end.y
}

for _ in *moving..=to {
    // do stuff with this

    if moving_horizontal {
        grid[*moving + *not_moving * HEIGHT];
    } else {
        grid[*not_moving + *moving * HEGHT];
    }
    *moving += 1;
}

If we didn't have to use moving in the for declaration, it could be written in a much better way, but I'm not sure about the details of the algorithm.

Anyway, even if this passes the borrow checker, it is very difficult to read and a double for loop would be much more maintainable.

If I understood correctly, you want to mutate the current_position so that it moves to the end Point horizontally or vertically.

fn main() {
    moving_position(true);
    moving_position(false);
}

#[derive(Debug)]
struct Point {
    x: usize,
    y: usize,
}
impl Point {
    fn new(x: usize, y: usize) -> Self {
        Self { x, y }
    }
}

const WIDTH: usize = 30;
const HEIGHT: usize = 30;
fn moving_position(moving_horizontal: bool) {
    let grid: Vec<u8> = Vec::with_capacity(WIDTH * HEIGHT);
    let mut current_position = Point::new(8, 5);
    let end = Point::new(0, 1);
    println!("Starting at: {:?}", current_position);
    match moving_horizontal {
        true => move_horizontal(&mut current_position, end.x),
        false => move_vertical(&mut current_position, end.y),
    };
    println!("Final position: {:?}",current_position);
}

fn move_horizontal(current_position: &mut Point, to: usize) {
    match current_position.x <= to {
        true => for i in current_position.x+1..=to {
            current_position.x = i;
            println!("moving right: {:?}", current_position);
        },
        false => for i in (to..current_position.x).rev() {
            current_position.x = i;
            println!("moving left: {:?}", current_position);
        },
    };
}

fn move_vertical(mut current_position: &mut Point, to: usize) {    
    match current_position.y <= to {
        true => for i in current_position.y+1..=to {
            current_position.y = i;
            println!("moving up: {:?}", current_position);
        },
        false => for i in (to..current_position.y).rev() {
            current_position.y = i;
            println!("moving down: {:?}", current_position);
        },
    };
}

We check if its moving_horizontal and depending on that we call functions move_horizontal or move_vertical.
These two functions take the current_position as a mutable reference (meaning we will mutate current_position in the function but will return ownership).

Playground

I think you can generate all the move steps first and then change the current point with the steps one by one in one single loop.

There are four kinds of steps -- (-1, 0), (1, 0), (0, 1) and (0, -1), corresponding to moving left, right, up and down. You can calculate how many steps are needed and which direction needs to be taken first based on the relative positions of the curr and end points and the moving_horizontal flag.

use std::iter::repeat;

struct Point {
    x: usize,
    y: usize,
}

fn main() {
    const HEIGHT: usize = 25;
    const WIDTH: usize = 80;
    
    const LEFT: (i8, i8) = (-1, 0);
    const RIGHT: (i8, i8) = (1, 0);
    const UP: (i8, i8) = (0, 1);
    const DOWN: (i8, i8) = (0, -1);
    
    let grid: Vec<u8> = vec![0; WIDTH * HEIGHT];
    let mut curr = Point {x: 6, y: 2 };
    let end = Point {x: 3, y: 4 };

    let (x_step, x_num) = if end.x > curr.x { (RIGHT, end.x - curr.x) }
                          else { (LEFT, curr.x - end.x) };

    let (y_step, y_num) = if end.y > curr.y { (UP, end.y - curr.y) }
                          else { (DOWN, curr.y - end.y) };

    let moving_horizontal = true;
    let all_steps = if moving_horizontal {
        repeat(x_step).take(x_num).chain(repeat(y_step).take(y_num))
    } else {
        repeat(y_step).take(y_num).chain(repeat(x_step).take(x_num))
    };

    println!("Start: (x:{}, y:{})", curr.x, curr.y);
    println!("End: (x:{}, y:{})", end.x, end.y);
    println!("Steps (Horizontal: {}):", moving_horizontal);
    for (i, step) in all_steps.enumerate() {
        // do stuff with this
        grid[curr.x + curr.y * HEIGHT];

        curr.x = match step.0 {
            1 => curr.x + 1,
            -1 => curr.x - 1,
            _ => curr.x
        };

        curr.y = match step.1 {
            1 => curr.y + 1,
            -1 => curr.y -1,
            _ => curr.y
        };

        println!("{} => (x:{}, y:{})", i, curr.x, curr.y);

    }
}

Playground

The output when moving_horizontal is true:

Start: (x:6, y:2)
End: (x:3, y:4)
Steps (Horizontal: true):
0 => (x:5, y:2)
1 => (x:4, y:2)
2 => (x:3, y:2)
3 => (x:3, y:3)
4 => (x:3, y:4)

The output when moving_horizontal is false:

Start: (x:6, y:2)
End: (x:3, y:4)
Steps (Horizontal: false):
0 => (x:6, y:3)
1 => (x:6, y:4)
2 => (x:5, y:4)
3 => (x:4, y:4)
4 => (x:3, y:4)
Related