utils/grid.rs
1//! Grid helpers.
2
3use crate::input::InputError;
4use std::error::Error;
5
6/// A parsed grid: `(number of rows, number of columns, data)`.
7pub type Grid<T> = (usize, usize, Vec<T>);
8
9/// Parse a 2D grid.
10///
11/// This function assumes that each byte represents one item in the grid.
12/// Using 1 byte wide output types is recommended to enable more efficient vectorization.
13///
14/// Parsing is done in two passes per line:
15///
16/// 1. A "hot" pass, where each byte is mapped to an output value and checked for validity.
17/// This uses the `hot_map` and `hot_valid` functions, which should be pure and total mappings
18/// from [`u8`] to their respective outputs to enable vectorization.
19/// Additionally, `hot_valid` must return false for both `'\r'` and `'\n'`.
20///
21/// 2. A "slow" pass, where any invalid bytes from the first pass are re-processed with their index
22/// in the final grid.
23/// This uses the `slow_map` function which returns a [`Result`], containing either a mapped
24/// value or an error.
25/// `slow_map` can be used to perform more complex, non-pure, mappings such as storing positions
26/// and will never be called for newlines or bytes that were valid in the first pass.
27/// If all the bytes were valid in the first pass, this pass is skipped.
28///
29/// `default_value` is used to initialize the grid before parsing and for any padding.
30/// A value with an all-zero bit pattern will usually faster to initialize and is recommended when
31/// padding is not used.
32///
33/// Returns (number of rows, number of columns, data) on success.
34///
35/// # Examples
36///
37/// ```
38/// # use utils::grid;
39/// assert_eq!(
40/// grid::parse(
41/// /* input */ "##.#\n#..#\n#.##",
42/// /* padding */ 0,
43/// /* default_value */ false,
44/// /* hot_map */ |b| b == b'#',
45/// /* hot_valid */ |b| matches!(b, b'.' | b'#'),
46/// /* slow_map */ |_, _| Err("expected '.' or '#'"),
47/// ).unwrap(),
48/// (3, 4, vec![
49/// true, true, false, true,
50/// true, false, false, true,
51/// true, false, true, true,
52/// ]),
53/// );
54/// ```
55///
56///
57/// ```
58/// # use utils::grid;
59/// assert_eq!(
60/// grid::parse(
61/// /* input */"##.#\n#..#\n#.##",
62/// /* padding */ 2,
63/// /* default_value */ false,
64/// /* hot_map */ |b| b == b'#',
65/// /* hot_valid */ |b| matches!(b, b'.' | b'#'),
66/// /* slow_map */ |_, _| Err("expected '.' or '#'"),
67/// ).unwrap(),
68/// (7, 8, vec![
69/// false, false, false, false, false, false, false, false,
70/// false, false, false, false, false, false, false, false,
71/// false, false, true, true, false, true, false, false,
72/// false, false, true, false, false, true, false, false,
73/// false, false, true, false, true, true, false, false,
74/// false, false, false, false, false, false, false, false,
75/// false, false, false, false, false, false, false, false,
76/// ]),
77/// );
78/// ```
79///
80/// ```
81/// # use utils::grid;
82/// let mut start = None;
83/// assert_eq!(
84/// grid::parse(
85/// /* input */ ".0.#S\r\n..1..\r\n.###2\r\n.3...",
86/// /* padding */ 1,
87/// /* default_value */ b'#',
88/// /* hot_map */ |b| b,
89/// /* hot_valid */ |b| matches!(b, b'.' | b'#' | b'0'..=b'9'),
90/// /* slow_map */ |i, b| {
91/// match b {
92/// b'S' if start.is_none() => {
93/// start = Some(i);
94/// Ok(b'.')
95/// },
96/// b'S' => Err("expected only one 'S'"),
97/// _ => Err("expected '.', '#', 'S' or a digit")
98/// }
99/// },
100/// ).unwrap(),
101/// (6, 7, vec![
102/// b'#', b'#', b'#', b'#', b'#', b'#', b'#',
103/// b'#', b'.', b'0', b'.', b'#', b'.', b'#',
104/// b'#', b'.', b'.', b'1', b'.', b'.', b'#',
105/// b'#', b'.', b'#', b'#', b'#', b'2', b'#',
106/// b'#', b'.', b'3', b'.', b'.', b'.', b'#',
107/// b'#', b'#', b'#', b'#', b'#', b'#', b'#',
108/// ]),
109/// );
110/// assert_eq!(start, Some(12));
111/// ```
112#[inline]
113pub fn parse<T: Clone, E: Into<Box<dyn Error>>>(
114 input: &str,
115 padding: usize,
116 default_value: T,
117 hot_map: impl Fn(u8) -> T,
118 hot_valid: impl Fn(u8) -> bool,
119 mut slow_map: impl FnMut(usize, u8) -> Result<T, E>,
120) -> Result<Grid<T>, InputError> {
121 assert!(!hot_valid(b'\n'));
122 assert!(!hot_valid(b'\r'));
123
124 // Line length including newline
125 let Some(line_length) = input
126 .bytes()
127 .position(|b| b == b'\n')
128 .map(|x| x + 1)
129 .filter(|&x| x >= 2)
130 else {
131 return Err(InputError::new(input, 0, "expected grid"));
132 };
133
134 let clrf_endings = input.as_bytes()[line_length - 2] == b'\r';
135 let newline_length = 1 + usize::from(clrf_endings);
136
137 // Add line_ending_length to account for the final newline which should have already been
138 // stripped by utils::input::strip_final_newline
139 if !(input.len() + newline_length).is_multiple_of(line_length) {
140 return Err(InputError::new(
141 input,
142 input.len(),
143 "expected input length to be a multiple of the first line length",
144 ));
145 }
146
147 let input_cols = line_length - newline_length;
148 let input_rows = (input.len() + newline_length) / line_length;
149
150 let padded_cols = input_cols + 2 * padding;
151 let padded_rows = input_rows + 2 * padding;
152
153 let mut data = vec![default_value; padded_cols * padded_rows];
154 for ((r, input_line), data_line) in input
155 .as_bytes()
156 .chunks(line_length)
157 .enumerate()
158 .zip(data.chunks_exact_mut(padded_cols).skip(padding))
159 {
160 // Hot pass
161 let mut valid = true;
162 for (&b, d) in input_line[..input_cols]
163 .iter()
164 .zip(data_line[padding..].iter_mut())
165 {
166 *d = hot_map(b);
167 valid &= hot_valid(b);
168 }
169
170 // Slow pass
171 if !valid {
172 for ((c, &b), d) in input_line[..input_cols]
173 .iter()
174 .enumerate()
175 .zip(data_line[padding..].iter_mut())
176 {
177 if hot_valid(b) {
178 continue;
179 }
180 if b == b'\n' || b == b'\r' {
181 return Err(InputError::new(
182 input,
183 &input_line[c..],
184 format!("expected {input_cols} columns"),
185 ));
186 }
187
188 let index = (r + padding) * padded_cols + padding + c;
189 match slow_map(index, b) {
190 Ok(v) => *d = v,
191 Err(err) => return Err(InputError::new(input, &input_line[c..], err)),
192 }
193 }
194 }
195
196 // Check input_line ends in the expected newline.
197 // Do this after the slow pass, so any earlier newlines earlier in the chunk have already
198 // been caught.
199 if r != input_rows - 1
200 && if clrf_endings {
201 input_line.last_chunk() != Some(b"\r\n")
202 } else {
203 input_line.last_chunk() != Some(b"\n")
204 }
205 {
206 return Err(InputError::new(
207 input,
208 &input_line[input_line.len() - 2..],
209 "expected newline",
210 ));
211 }
212 }
213
214 Ok((padded_rows, padded_cols, data))
215}
216
217/// Parse a "standard" maze with open tiles `.`, walls `#` and one start `S` and one end `E`.
218///
219/// Returns ((number of rows, number of columns, data), start index, end index) on success.
220///
221/// # Examples
222/// ```
223/// # use utils::grid;
224/// assert_eq!(
225/// grid::parse_maze("...#S\n.#E#.\n.###.\n.....", 1).unwrap(),
226/// (
227/// (6, 7, vec![
228/// b'#', b'#', b'#', b'#', b'#', b'#', b'#',
229/// b'#', b'.', b'.', b'.', b'#', b'.', b'#',
230/// b'#', b'.', b'#', b'.', b'#', b'.', b'#',
231/// b'#', b'.', b'#', b'#', b'#', b'.', b'#',
232/// b'#', b'.', b'.', b'.', b'.', b'.', b'#',
233/// b'#', b'#', b'#', b'#', b'#', b'#', b'#',
234/// ]),
235/// 12,
236/// 17,
237/// )
238/// );
239/// ```
240#[inline]
241pub fn parse_maze(input: &str, padding: usize) -> Result<(Grid<u8>, usize, usize), InputError> {
242 let mut start = None;
243 let mut end = None;
244 let grid = parse(
245 input,
246 padding,
247 if padding > 0 { b'#' } else { 0 },
248 |b| b,
249 |b| matches!(b, b'.' | b'#'),
250 |i, b| {
251 match b {
252 b'S' if start.is_none() => start = Some(i),
253 b'S' => return Err("expected one 'S'"),
254 b'E' if end.is_none() => end = Some(i),
255 b'E' => return Err("expected one 'E'"),
256 _ => return Err("expected '.', '#', 'S' or 'E'"),
257 }
258 Ok(b'.')
259 },
260 )?;
261 let Some(start) = start else {
262 return Err(InputError::new(input, 0, "expected one 'S'"));
263 };
264 let Some(end) = end else {
265 return Err(InputError::new(input, 0, "expected one 'E'"));
266 };
267 Ok((grid, start, end))
268}
269
270/// Checks that the provided grid has walls on each edge.
271///
272/// # Examples
273/// ```
274/// # use utils::grid::is_enclosed;
275/// assert_eq!(
276/// is_enclosed(5, 6, &[
277/// b'#', b'#', b'#', b'#', b'#', b'#',
278/// b'#', b'.', b'.', b'.', b'.', b'#',
279/// b'#', b'.', b'.', b'.', b'.', b'#',
280/// b'#', b'.', b'.', b'.', b'.', b'#',
281/// b'#', b'#', b'#', b'#', b'#', b'#',
282/// ], |&b| b == b'#'),
283/// true,
284/// );
285/// assert_eq!(
286/// is_enclosed(5, 6, &[
287/// b'#', b'#', b'#', b'#', b'#', b'#',
288/// b'#', b'.', b'.', b'.', b'.', b'#',
289/// b'#', b'.', b'.', b'.', b'.', b'#',
290/// b'#', b'.', b'.', b'.', b'.', b'.',
291/// b'#', b'#', b'#', b'#', b'#', b'#',
292/// ], |&b| b == b'#'),
293/// false,
294/// );
295/// ```
296pub fn is_enclosed<T>(rows: usize, cols: usize, grid: &[T], is_wall: impl Fn(&T) -> bool) -> bool {
297 grid[..cols].iter().all(&is_wall)
298 && grid[(rows - 1) * cols..].iter().all(&is_wall)
299 && (1..rows).all(|r| is_wall(&grid[r * cols]) && is_wall(&grid[(r + 1) * cols - 1]))
300}