use regex::Regex; use std::collections::HashMap; use std::fs; /// One remembered Aunt Sue: her number and whatever compounds were noted. #[derive(Debug)] struct Sue { number: u32, properties: HashMap, } /// Parses the puzzle input into a list of `Sue`s. /// /// Each line looks like `Sue 1: goldfish: 9, cars: 0, samoyeds: 9`. The /// number of properties varies per line, so we pull out the `name: count` /// pairs with a regex rather than assuming a fixed format. fn parse_sues(input: &str) -> Vec { let line_re = Regex::new(r"^Sue (\d+): (.+)$").unwrap(); let property_re = Regex::new(r"(\w+): (\d+)").unwrap(); input .lines() .filter_map(|line| { let caps = line_re.captures(line)?; let number = caps[1].parse().unwrap(); let properties = property_re .captures_iter(&caps[2]) .map(|prop| (prop[1].to_string(), prop[2].parse().unwrap())) .collect(); Some(Sue { number, properties }) }) .collect() } /// The exact readings from the MFCSAM. fn target_properties() -> HashMap<&'static str, u32> { HashMap::from([ ("children", 3), ("cats", 7), ("samoyeds", 2), ("pomeranians", 3), ("akitas", 0), ("vizslas", 0), ("goldfish", 5), ("trees", 3), ("cars", 2), ("perfumes", 1), ]) } /// A Sue matches if every compound she's remembered for equals the target /// reading exactly. Properties she isn't remembered for are simply skipped, /// per the puzzle's "missing things aren't zero" rule. fn matches_target(sue: &Sue, target: &HashMap<&str, u32>) -> bool { sue.properties .iter() .all(|(name, &count)| target.get(name.as_str()) == Some(&count)) } fn main() { let input = fs::read_to_string("input.txt").expect("failed to read input.txt"); let sues = parse_sues(&input); let target = target_properties(); let real_sue = sues .iter() .find(|sue| matches_target(sue, &target)) .expect("no Sue matched the MFCSAM readings"); println!("Sue {} gave the gift.", real_sue.number); }