This repository has been archived on 2024-12-13. You can view files and clone it, but cannot push or open issues or pull requests.
2024-12-04 23:26:21 -05:00

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);
}
}