use std::collections::HashMap;
use std::fs;
#[derive(Eq, PartialEq, Debug)]
struct PartNumber {
number: u32,
x: usize,
y: usize,
}
impl PartNumber {
fn new(number: u32, x: usize, y: usize) -> PartNumber {
PartNumber { number, x, y }
}
}
type Point = (usize, usize);
type SymbolLookup = HashMap<Point, char>;
#[derive(Eq, Debug)]
struct Gear {
part_1: u32,
part_2: u32,
}
impl Gear {
fn new(part_1: u32, part_2: u32) -> Gear {
Gear { part_1, part_2 }
}
}
impl PartialEq for Gear {
fn eq(&self, other: &Self) -> bool {
(self.part_1 == other.part_1 && self.part_2 == other.part_2)
|| (self.part_1 == other.part_2 && self.part_2 == other.part_1)
}
fn ne(&self, other: &Self) -> bool {
!self.eq(other)
}
}
pub fn run() {
let contents = fs::read_to_string("res/day-3-input.txt").expect("Failed to read file");
let (part_numbers, symbol_lookup) = parse_grid(&contents);
println!(
"The sum of valid part numbers is {}",
sum_valid_part_numbers(&part_numbers, &symbol_lookup)
);
println!(
"The sum of gear ratios is {}",
sum_gear_ratios(&part_numbers, &symbol_lookup)
);
}
fn parse_grid(input: &String) -> (Vec<PartNumber>, SymbolLookup) {
let mut parts = Vec::new();
let mut symbols = HashMap::new();
let mut num: u32 = 0;
let mut num_origin: Option<Point> = None;
fn build_part_number_and_reset(
parts: &mut Vec<PartNumber>,
num: &mut u32,
num_origin: &mut Option<Point>,
) {
if let Some((x, y)) = num_origin {
parts.push(PartNumber::new(num.clone(), x.clone(), y.clone()));
*num = 0;
*num_origin = None;
}
}
for (y, line) in input.lines().enumerate() {
for (x, chr) in line.chars().enumerate() {
if !chr.is_digit(10) {
build_part_number_and_reset(&mut parts, &mut num, &mut num_origin);
}
match chr {
'.' => {}
c if c.is_digit(10) => {
num_origin = num_origin.or(Some((x, y)));
num = num * 10 + chr.to_digit(10).expect("Tested with is_digit");
}
_ => {
symbols.insert((x, y), chr);
}
}
}
build_part_number_and_reset(&mut parts, &mut num, &mut num_origin);
}
(parts, symbols)
}
fn sum_valid_part_numbers(part_numbers: &Vec<PartNumber>, symbol_lookup: &SymbolLookup) -> u32 {
part_numbers
.iter()
.filter(|&part_number| has_adjacent_symbol(part_number, symbol_lookup))
.map(|part_number| part_number.number)
.sum()
}
fn has_adjacent_symbol(part_number: &PartNumber, symbol_lookup: &SymbolLookup) -> bool {
return get_adjacent_points(part_number)
.iter()
.any(|point| symbol_lookup.contains_key(point));
}
fn get_adjacent_points(part_number: &PartNumber) -> Vec<Point> {
let mut points = Vec::new();
let length = part_number.number.ilog10() as usize + 1;
let start = part_number.x.checked_sub(1).unwrap_or(0);
let end = part_number.x + length;
for x in start..=end {
if part_number.y > 0 {
points.push((x, part_number.y - 1))
}
if x < part_number.x || x >= end {
points.push((x, part_number.y))
}
points.push((x, part_number.y + 1))
}
points
}
fn find_gears(part_numbers: &Vec<PartNumber>, symbol_lookup: &SymbolLookup) -> Vec<Gear> {
let part_nums_adjacent_to_gear_points = part_numbers
.iter()
.flat_map(explode_adjacent_points)
.filter(|(_, point)| is_point_a_gear_symbol(point, symbol_lookup));
let mut part_numbers_per_gear_point: HashMap<Point, Vec<u32>> = HashMap::new();
for (part_number, point) in part_nums_adjacent_to_gear_points {
part_numbers_per_gear_point
.entry(point)
.or_insert(Vec::new())
.push(part_number)
}
part_numbers_per_gear_point
.values()
.filter(|parts| parts.len() == 2)
.map(|parts| Gear::new(parts[0], parts[1]))
.collect()
}
fn explode_adjacent_points(part_number: &PartNumber) -> Vec<(u32, Point)> {
get_adjacent_points(part_number)
.into_iter()
.map(|point| (part_number.number, point))
.collect::<Vec<(u32, Point)>>()
}
fn is_point_a_gear_symbol(point: &Point, symbol_lookup: &SymbolLookup) -> bool {
symbol_lookup
.get(point)
.filter(|&symbol| *symbol == '*')
.is_some()
}
fn sum_gear_ratios(part_numbers: &Vec<PartNumber>, symbol_lookup: &SymbolLookup) -> u32 {
find_gears(part_numbers, symbol_lookup)
.iter()
.map(|Gear { part_1, part_2 }| part_1 * part_2)
.sum()
}
#[cfg(test)]
mod tests {
use crate::day_3::*;
use crate::helpers::test::assert_contains_in_any_order;
fn sample_input() -> String {
return "\
467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598.."
.to_string();
}
fn example_part_numbers() -> Vec<PartNumber> {
vec![
PartNumber::new(467, 0, 0),
PartNumber::new(114, 5, 0),
PartNumber::new(35, 2, 2),
PartNumber::new(633, 6, 2),
PartNumber::new(617, 0, 4),
PartNumber::new(58, 7, 5),
PartNumber::new(592, 2, 6),
PartNumber::new(755, 6, 7),
PartNumber::new(664, 1, 9),
PartNumber::new(598, 5, 9),
]
}
fn example_symbol_lookup() -> SymbolLookup {
vec![
((3, 1), '*'),
((6, 3), '#'),
((3, 4), '*'),
((5, 5), '+'),
((3, 8), '$'),
((5, 8), '*'),
]
.into_iter()
.collect()
}
#[test]
fn can_parse_grid() {
let expected_parts = example_part_numbers();
let expected_symbol_lookup = example_symbol_lookup();
let (parts, symbols) = parse_grid(&sample_input());
assert_contains_in_any_order(parts, expected_parts);
assert_contains_in_any_order(symbols, expected_symbol_lookup);
}
#[test]
fn can_find_adjacent_points() {
#[rustfmt::skip] let examples = vec![
(PartNumber::new(99, 0, 0), vec![
(2, 0),
(0, 1), (1, 1), (2, 1)
]),
(PartNumber::new(1, 1, 1), vec![
(0, 0), (1, 0), (2, 0),
(0, 1) , (2, 1),
(0, 2), (1, 2), (2, 2),
]),
];
for (part_number, expected_points) in examples {
assert_contains_in_any_order(get_adjacent_points(&part_number), expected_points);
}
}
#[test]
fn can_determine_if_part_is_adjacent_to_a_symbol() {
let symbol_lookup = example_symbol_lookup();
let examples = vec![
(PartNumber::new(467, 0, 0), true),
(PartNumber::new(114, 5, 0), false),
(PartNumber::new(35, 2, 2), true),
(PartNumber::new(633, 6, 2), true),
(PartNumber::new(617, 0, 4), true),
(PartNumber::new(58, 7, 5), false),
(PartNumber::new(592, 2, 6), true),
(PartNumber::new(755, 6, 7), true),
(PartNumber::new(664, 1, 9), true),
(PartNumber::new(598, 5, 9), true),
];
for (part_number, expected) in examples {
assert_eq!(
has_adjacent_symbol(&part_number, &symbol_lookup),
expected,
"{:?} should{} have an adjacent symbol",
part_number,
if expected { "" } else { " not" }
)
}
}
#[test]
fn can_sum_valid_part_numbers() {
assert_eq!(
sum_valid_part_numbers(&example_part_numbers(), &example_symbol_lookup(),),
4361
)
}
#[test]
fn can_find_gears() {
let expected_gears = vec![Gear::new(467, 35), Gear::new(755, 598)];
assert_contains_in_any_order(
find_gears(&example_part_numbers(), &example_symbol_lookup()),
expected_gears,
)
}
#[test]
fn can_find_gears_with_shared_part_number() {
let example_grid = "\
1...2
.*.*.
..3.."
.to_string();
let (part_numbers, symbol_lookup) = parse_grid(&example_grid);
let expected_gears = vec![Gear::new(1, 3), Gear::new(2, 3)];
assert_contains_in_any_order(find_gears(&part_numbers, &symbol_lookup), expected_gears)
}
#[test]
fn can_sum_gear_ratios() {
assert_eq!(
sum_gear_ratios(&example_part_numbers(), &example_symbol_lookup()),
467835
);
}
}