266 lines
8.2 KiB
Rust
266 lines
8.2 KiB
Rust
use std::{
|
|
collections::HashSet,
|
|
error::Error,
|
|
fmt::Display,
|
|
fs::File,
|
|
io::{BufRead, BufReader},
|
|
};
|
|
|
|
use itertools::Itertools;
|
|
|
|
/// Basic error type
|
|
#[derive(Debug)]
|
|
enum ParsingError {
|
|
ParserFailure,
|
|
IndexOutOfBounds,
|
|
InconsistentGrid,
|
|
}
|
|
|
|
impl Display for ParsingError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
ParsingError::ParserFailure => write!(f, "Failed to parse segment"),
|
|
ParsingError::IndexOutOfBounds => write!(f, "Index out of bounds for CharGrid"),
|
|
ParsingError::InconsistentGrid => write!(f, "File is not a consistent char grid"),
|
|
}
|
|
}
|
|
}
|
|
|
|
type MyResult<T> = Result<T, Box<dyn Error>>;
|
|
|
|
impl Error for ParsingError {}
|
|
|
|
/// Struct that flattens out the input grid
|
|
pub struct CharGrid {
|
|
pub chars: Vec<char>,
|
|
pub m: usize,
|
|
pub n: usize,
|
|
}
|
|
|
|
impl CharGrid {
|
|
/// Take a file and split it into a CharGrid. Since input has only ASCII characters this should
|
|
/// be fine
|
|
pub fn new(input_file: &str) -> MyResult<CharGrid> {
|
|
let file = File::open(input_file)?;
|
|
let reader = BufReader::new(file);
|
|
|
|
let mut m = 0;
|
|
let mut n: Option<usize> = None;
|
|
let chars: Vec<char> = reader
|
|
.lines()
|
|
.map(|line| -> MyResult<Vec<char>> {
|
|
let parsed_line = Vec::from_iter(line?.chars());
|
|
m += 1;
|
|
if n.is_none() {
|
|
n = Some(parsed_line.len());
|
|
} else if n.unwrap() != parsed_line.len() {
|
|
return Err(Box::new(ParsingError::InconsistentGrid));
|
|
}
|
|
Ok(parsed_line)
|
|
})
|
|
.collect::<MyResult<Vec<Vec<char>>>>()?
|
|
.into_iter()
|
|
.flatten()
|
|
.collect();
|
|
|
|
// Safe to unwrap n because it is validated if we get to this point
|
|
if chars.len() != m * n.unwrap() {
|
|
Err(Box::new(ParsingError::ParserFailure))
|
|
} else {
|
|
Ok(CharGrid {
|
|
chars,
|
|
m,
|
|
n: n.unwrap(),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Get an index within the grid
|
|
pub fn get(&self, i: i32, j: i32) -> MyResult<char> {
|
|
let idx = self.two2one_index(i, j)?;
|
|
Ok(self.chars[idx])
|
|
}
|
|
|
|
// Convert the one dimensional index to two dimensional index
|
|
pub fn one2two_index(&self, idx: usize) -> (usize, usize) {
|
|
let i = idx / self.n;
|
|
let j = idx % self.n;
|
|
(i, j)
|
|
}
|
|
|
|
// Convert the two dimensional index to one dimensional index
|
|
pub fn two2one_index(&self, i: i32, j: i32) -> MyResult<usize> {
|
|
if i < 0 || j < 0 {
|
|
Err(Box::new(ParsingError::IndexOutOfBounds))
|
|
} else if i as usize >= self.m || j as usize >= self.n {
|
|
Err(Box::new(ParsingError::IndexOutOfBounds))
|
|
} else {
|
|
Ok((i as usize) * self.n + (j as usize))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<CharGrid> for CharGraph {
|
|
fn from(value: CharGrid) -> Self {
|
|
// Build up the CharGraph
|
|
let mut edges: Vec<Edge> = Vec::new();
|
|
let mut node_idx: Vec<usize> = vec![0];
|
|
let mut node_char: Vec<char> = Vec::new();
|
|
|
|
for (idx, c) in value.chars.iter().enumerate() {
|
|
let (i, j) = value.one2two_index(idx);
|
|
for (edge_type, (offset_i, offset_j)) in [-1, 0, 1]
|
|
.into_iter()
|
|
.cartesian_product([-1, 0, 1])
|
|
.enumerate()
|
|
{
|
|
let nei_i: i32 = i as i32 + offset_i;
|
|
let nei_j: i32 = j as i32 + offset_j;
|
|
if let Ok(nei_char) = value.get(nei_i, nei_j) {
|
|
if (*c == 'X' && nei_char == 'M')
|
|
|| (*c == 'M' && nei_char == 'A')
|
|
|| (*c == 'A' && nei_char == 'S')
|
|
{
|
|
edges.push(Edge {
|
|
start_idx: idx,
|
|
// Safe to unwrap here since this index has been validated already
|
|
end_idx: value.two2one_index(nei_i, nei_j).unwrap(),
|
|
start_char: *c,
|
|
edge_type,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
node_idx.push(edges.len());
|
|
node_char.push(*c);
|
|
}
|
|
CharGraph {
|
|
edges,
|
|
node_idx,
|
|
node_char,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Simple graph-like data structure to represent continuous XMAS's
|
|
pub struct CharGraph {
|
|
edges: Vec<Edge>,
|
|
node_idx: Vec<usize>,
|
|
node_char: Vec<char>,
|
|
}
|
|
|
|
impl CharGraph {
|
|
/// Iterate through all edges. If it's an X count all the full XMAS segments that can be made
|
|
pub fn count_xmas(&self) -> usize {
|
|
let mut segments = 0;
|
|
for (idx, edge) in self.edges.iter().enumerate() {
|
|
if edge.start_char == 'X' {
|
|
segments = self.get_full_segments(idx, ' ', segments);
|
|
}
|
|
}
|
|
segments
|
|
}
|
|
|
|
/// recursive call to get XMAS segments
|
|
fn get_full_segments(&self, edge_idx: usize, prev_char: char, mut segments: usize) -> usize {
|
|
let Edge {
|
|
end_idx,
|
|
start_char,
|
|
edge_type,
|
|
..
|
|
} = &self.edges[edge_idx];
|
|
|
|
// If this is an AS segment increment segment count and return
|
|
if *start_char == 'A' && prev_char == 'M' {
|
|
segments + 1
|
|
} else {
|
|
if *end_idx < self.node_idx.len() {
|
|
for next_edge_idx in self.node_idx[*end_idx]..self.node_idx[*end_idx + 1] {
|
|
if self.edges[next_edge_idx].edge_type == *edge_type {
|
|
segments = self.get_full_segments(next_edge_idx, *start_char, segments)
|
|
}
|
|
}
|
|
}
|
|
segments
|
|
}
|
|
}
|
|
|
|
/// Count all X-MAS shapes for part 2. Trying to reuse code from part 1 as much as possible so
|
|
/// this might be a little hacky. Running out of time here.
|
|
pub fn count_part2_xmas(&self) -> usize {
|
|
let mut a_we_met: HashSet<usize> = HashSet::new();
|
|
let mut xmas_count = 0;
|
|
for (idx, edge) in self.edges.iter().enumerate() {
|
|
if edge.start_char == 'M' && [0, 8, 2, 6].contains(&edge.edge_type) {
|
|
let segments = self.get_full_segments(idx, ' ', 0);
|
|
if segments == 1 {
|
|
if a_we_met.contains(&edge.end_idx) {
|
|
xmas_count += 1;
|
|
} else {
|
|
a_we_met.insert(edge.end_idx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
xmas_count
|
|
}
|
|
}
|
|
/// Representation of a directed edge where the edge only exists for:
|
|
/// start = X end = M
|
|
/// start = M end = A
|
|
/// start = A end = S
|
|
/// Edge type just represents the direction of the edge
|
|
/// | 0 1 2
|
|
/// | \ | /
|
|
/// | 3 -start- 5
|
|
/// | / | \
|
|
/// | 6 7 8
|
|
///
|
|
/// Should've used an enum but trying to rush here
|
|
pub struct Edge {
|
|
start_idx: usize,
|
|
start_char: char,
|
|
end_idx: usize,
|
|
edge_type: usize,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_char_grid() {
|
|
let grid = CharGrid::new("test_inputs/test_input.txt").unwrap();
|
|
assert_eq!(grid.m, 2);
|
|
assert_eq!(grid.n, 8);
|
|
|
|
if CharGrid::new("test_inputs/bad_input.txt").is_ok() {
|
|
panic!("Should've failed to parse bad input")
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn test_char_graph() {
|
|
let grid = CharGrid::new("test_inputs/test_input.txt").unwrap();
|
|
let graph = CharGraph::from(grid);
|
|
assert_eq!(graph.edges.len(), 20);
|
|
}
|
|
|
|
#[test]
|
|
fn test_count_xmas() {
|
|
let grid = CharGrid::new("test_inputs/test_input.txt").unwrap();
|
|
let graph = CharGraph::from(grid);
|
|
assert_eq!(graph.count_xmas(), 2);
|
|
let grid = CharGrid::new("test_inputs/test_advent_sample.txt").unwrap();
|
|
let graph = CharGraph::from(grid);
|
|
assert_eq!(graph.count_xmas(), 18);
|
|
}
|
|
|
|
#[test]
|
|
fn test_count_xmas_part2() {
|
|
let grid = CharGrid::new("test_inputs/test_advent_sample.txt").unwrap();
|
|
let graph = CharGraph::from(grid);
|
|
assert_eq!(graph.count_part2_xmas(), 9);
|
|
}
|
|
}
|