This documentation explores a Rust solution for a word search puzzle, demonstrating several programming principles including functional programming, iterator chaining, and spatial algorithms. The solution effectively searches a 2D grid for specific patterns (“XMAS” and “MAS”) in various directions.
The core challenge is to scan a 2D grid of characters to find specific words in multiple directions. The solution utilizes:
The first step is to parse and represent the 2D character grid from the input file.
A 2D grid requires a data structure that supports efficient positional access and boundary checks. The solution uses a Field<char>
type, which wraps a vector of characters and provides methods for accessing elements by their 2D coordinates.
let input = std::fs::read_to_string("src/bin/day4/input.txt").expect("File not found");
let field = input.parse::<Field<char>>().expect("Doesn't error");
let (height, width) = (field.height(), field.width());
This code reads the input file and parses it into a Field<char>
representation, then extracts the grid dimensions for later use.
Next, we need a function to check if a word exists in a specific direction starting from a particular position.
Word matching needs to:
fn is_word_matched(field: &Field<char>, word: &str, start: Location, dir: DirVector) -> bool {
word.char_indices()
.all(|(i,c)| start
// calculate new location based on (a) current index (b) starting position & (c) direction
.move_relative((dir.0 * i as isize, dir.1 * i as isize))
.map(|p| field
// match the value in position with input's character
.get(p)
.map(|&val| val == c)
.unwrap_or(false)
).unwrap_or(false)
)
}
This function:
The use of map
and unwrap_or
ensures graceful handling of out-of-bounds coordinates.
To avoid code duplication, the solution creates a function that generates specialized word scanners.
Creating a higher-order function allows:
fn search_directions<'a>(
field: &'a Field<char>,
dirs: &'a [DirVector]
) -> impl Fn(&'a str, Location) -> Box<dyn Iterator<Item=(Location,DirVector)> + 'a>
{
// return a function that takes a world and location
// and performs a scan on field and set of directions that has be constructed with
move |word: &'a str, pos: Location| {
let ret = dirs.iter()
.copied()
.filter(move |&dir| is_word_matched(field, word, pos, dir))
.map(move |dir| (pos,dir));
// iterator must be boxed as it doesn't compile with "-> impl Iterator"
Box::new(ret)
}
}
This function:
The use of Rust’s lifetime annotations and boxed iterators ensures the returned function can be used flexibly.
The first part of the puzzle requires counting occurrences of “XMAS” and “SAMX” in the grid.
For efficiency, we can:
let xmas_scanner = search_directions(&field, &[(1,0),(0,1),(1,1),(1,-1)]);
let sum = (0..width)
.map(|x| (0..height)
.map(|y|
xmas_scanner("XMAS", Location(x,y)).count()
+ xmas_scanner("SAMX", Location(x,y)).count()
)
.sum::<usize>()
)
.sum::<usize>();
This code:
The second part looks for a specific pattern: “MAS” (or “SAM”) in two diagonal directions forming a cross.
To find this pattern:
let mas_leg1_scanner = search_directions(&field, &[(1,1)]);
let mas_leg2_scanner = search_directions(&field, &[(1,-1)]);
let sum = (0..height)
.map(|y| (0..width)
.filter(|&x|
(mas_leg1_scanner("MAS",Location(x,y)).count() == 1 ||
mas_leg1_scanner("SAM",Location(x,y)).count() == 1) &&
(mas_leg2_scanner("MAS",Location(x,y+2)).count() == 1 ||
mas_leg2_scanner("SAM",Location(x,y+2)).count() == 1)
)
.count()
)
.sum::<usize>();
This code:
The solution includes a test function to verify the word matching logic:
#[test]
fn test_scan_for_xmas() {
let input = std::fs::read_to_string("src/bin/day4/sample.txt").expect("File not found");
let field = input.parse::<Field<char>>().expect("Doesn't error");
assert!(is_word_matched(&field, "XMAS", Location(9, 9), (-1, -1)));
assert!(!is_word_matched(&field, "XMAS", Location(8, 9), (-1, -1)));
// Additional assertions...
}
This test ensures the is_word_matched
function correctly identifies words in the expected positions.
This solution demonstrates how to approach a 2D grid search problem using functional programming principles in Rust. By breaking down the problem into smaller, reusable pieces and leveraging Rust’s type system and iterator patterns, the solution achieves both clarity and efficiency.
The approach is also extensible - new patterns or directions could be easily added by modifying the direction vectors and pattern recognition logic without changing the core algorithm structure.