Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I conveniently convert a 2-dimensional array into a 2-dimensional vector?

I'm following the Rust-wasm tutorial and I want to be able to easily add a ship (a shape really) to the Universe in the game of life.

As a first step, I'd like to convert a 2-dimensional array of 0 or 1 representing a shape into a vector of indices representing the coordinates of the shape in the Universe.

I have a working piece of code but I'd like to make it a bit more user-friendly:

const WIDTH: u32 = 64;
const HEIGHT: u32 = 64;

/// glider: [[0, 1, 0], [0, 0, 1], [1, 1, 1]]
fn make_ship(shape: Vec<Vec<u32>>) -> Vec<u32> {
    let mut ship: Vec<u32> = Vec::new();

    for row_idx in 0..shape.len() {
        for col_idx in 0..shape[row_idx].len() {
            let cell = shape[row_idx][col_idx];
            if cell == 1 {
                ship.push(col_idx as u32 + row_idx as u32 * WIDTH);
            }
        }
    }

    ship
}

#[test]
fn glider() {
    let glider  = vec![vec![0, 1, 0], vec![0, 0, 1], vec![1, 1, 1]];
    println!("{:?}", make_ship(glider));
}

The test shows my problem: the verbosity of vec!s. Ideally I'd like to be able to write it without all the vec!. The code of make_ship shouldn't care about the size of the shape arrays. Ideal example:

let glider = [[0, 1, 0],
              [0, 0, 1],
              [1, 1, 1],];

The question is: how to express a shape nicely with simple arrays and have the function make_ship take 2-dimensional vectors of arbitrary size?

like image 847
DjebbZ Avatar asked Oct 21 '25 12:10

DjebbZ


2 Answers

Reducing the number of vec!s is possible with a custom macro:

#[macro_export]
macro_rules! vec2d {
    ($($i:expr),+) => { // handle numbers
        {
            let mut ret = Vec::new();
            $(ret.push($i);)*
            ret
        }
    };

    ([$($arr:tt),+]) => { // handle sets
        {
            let mut ret = Vec::new();
            $(ret.push(vec!($arr));)*
            ret
        }
    };
}

fn main() {
    let glider = vec2d![[0, 1, 0],
                        [0, 0, 1],
                        [1, 1, 1]];

    let glider2 = vec2d![[0, 1, 0, 1],
                         [0, 0, 1, 0],
                         [1, 1, 1, 0],
                         [0, 1, 1, 0]];


    println!("{:?}", glider);  // [[0, 1, 0], [0, 0, 1], [1, 1, 1]]
    println!("{:?}", glider2); // [[0, 1, 0, 1], [0, 0, 1, 0], [1, 1, 1, 0], [0, 1, 1, 0]]
}

Your initial function could also use a bit of an improvement with the help of Rust's iterators:

fn make_ship(shape: Vec<Vec<u32>>) -> Vec<u32> {
    shape
        .iter()
        .enumerate()
        .flat_map(|(row, v)| {
            v.iter().enumerate().filter_map(move |(col, x)| {
                if *x == 1 {
                    Some(col as u32 + row as u32 * WIDTH)
                } else {
                    None
                }
            })
        })
        .collect()
}
like image 106
ljedrz Avatar answered Oct 23 '25 03:10

ljedrz


Vec<Vec<_>> is actually not a 2-dimensional vector but a "vector of vectors". This has major implications (assuming the outer vector is interpreted as rows, and the inner as columns):

  1. Rows can have different lengths. That is often not what you would want.
  2. Rows are individual objects, potentially scattered all over the heap memory.
  3. In order to access an element you have to follow two indirections.

I would implement a 2-dimensional vector rather as a 1-dimensional vector with additional information regarding its dimensions. Something like:

struct Vec2D<T> {
    n_rows: usize,  // number of rows
    n_cols: usize,  // number of columns (redundant, since we know the length of data)
    data: Vec<T>,   // data stored in a contiguous 1D array
}

This struct can be initialized with

let glider = Vec2D {
    n_rows: 3,
    n_cols: 3,
    data: vec![0, 1, 0, 
               0, 0, 1, 
               1, 1, 1],
};

Or more conveniently with functions or macros that take arrays-of-arrays. (See @ljedrz's answer for inspiration).

To access an element in the struct you'd have to use a little bit of math to convert a 2D index into a 1D index:

impl<T> Vec2D<T> {
    fn get(&self, row: usize, col: usize) -> &T {
         assert!(row < self.n_rows);
         assert!(col < self.n_cols);
         &self.data[row * self.n_cols + col]
    }
}

While implementing your own 2-dimensional array type is a fun exercise, for productive use it may be more efficient to use an existing solution such as the ndarray crate.

like image 29
MB-F Avatar answered Oct 23 '25 04:10

MB-F



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!