use std::{collections::HashMap, str::FromStr}; use anyhow::Result; use regex::Regex; #[derive(Debug,thiserror::Error)] enum ParseError { #[error("Expected {0}")] Expected(String) } #[derive(Debug,PartialEq, Eq, Hash)] enum EntryType { Byr, Iyr, Eyr, Hgt, Hcl, Ecl, Pid, Cid, } impl FromStr for EntryType { type Err = ParseError; fn from_str(s: &str) -> Result { match s { "byr" => Ok(Self::Byr), "iyr" => Ok(Self::Iyr), "eyr" => Ok(Self::Eyr), "hgt" => Ok(Self::Hgt), "hcl" => Ok(Self::Hcl), "ecl" => Ok(Self::Ecl), "pid" => Ok(Self::Pid), "cid" => Ok(Self::Cid), _ => Err(ParseError::Expected(format!("byr, iyr, eyr, hgt, hcl, ecl, pid, cid, but got {}", s))) } } } impl EntryType { pub fn is_valid(&self, value: &Option<&str>) -> bool { match (*value, self) { (Some(value), EntryType::Byr) => { // four digits; at least 1920 and at most 2002. match str::parse::(value) { Ok(number) => { (1920..=2002).contains(&number) } Err(_) => { false } } } (Some(value), EntryType::Iyr) => { // four digits; at least 2010 and at most 2020. match str::parse::(value) { Ok(number) => { (2010..=2020).contains(&number) } Err(_) => { false } } } (Some(value), EntryType::Eyr) => { // four digits; at least 2020 and at most 2030. match str::parse::(value) { Ok(number) if (2020..=2030) .contains(&number) => true, _ => false } } (Some(value), EntryType::Hgt) => { // a number followed by either cm or in: // if cm, the number must be at least 150 and at most 193. // if in, the number must be at least 59 and at most 76. let rex_cm = Regex::new(r"(\d*)cm").unwrap(); let rex_in = Regex::new(r"(\d*)in").unwrap(); if rex_cm.is_match(value) { let m = rex_cm.captures(value); // At least "None" fails the test match str::parse::(m.unwrap().get(1).unwrap().as_str()) { Ok(height) if (150..=193).contains(&height) => true, _ => false } } else if rex_in.is_match(value) { let m = rex_in.captures(value); // At least "None" fails the test match str::parse::(m.unwrap().get(1).unwrap().as_str()) { Ok(height) if (59..=76).contains(&height) => true, _ => false } } else { false } } (Some(value), EntryType::Hcl) => { // a # followed by exactly six characters 0-9 or a-f. //probably ok as long as we deal with ascii right ? let rex = Regex::new(r"([a-z]|[0-9]){6}").unwrap(); value.starts_with('#') && rex.is_match(&(value.chars().skip(1).collect::())) } (Some(value), EntryType::Ecl) => { // exactly one of: amb blu brn gry grn hzl oth. let valid_values = vec!["amb", "blu", "brn", "gry", "grn", "hzl", "oth"]; valid_values.contains(&value) } (Some(value), EntryType::Pid) => { // a nine-digit number, including leading zeroes. // probably ok if we deal only with ascii right ? value.as_bytes().len() == 9 && str::parse::(value).is_ok() } (_, EntryType::Cid) => { // ignored, missing or not true } _ => false } } } #[derive(Debug)] struct Passport<'a> { fields: HashMap> } impl<'a> Passport<'a> { fn new() -> Self { let mut res = Self { fields: HashMap::new() }; res.fields.insert(EntryType::Byr, None); res.fields.insert(EntryType::Iyr, None); res.fields.insert(EntryType::Eyr, None); res.fields.insert(EntryType::Hgt, None); res.fields.insert(EntryType::Hcl, None); res.fields.insert(EntryType::Ecl, None); res.fields.insert(EntryType::Pid, None); res.fields.insert(EntryType::Cid, None); res } fn parse(&mut self, s: &'static str) { for entry_s in s.split(&[' ', '\n'][..]) { let mut tokens = entry_s.split(':'); if let Ok(etype) = EntryType::from_str(tokens.next().unwrap()) { self.fields.insert(etype, tokens.next()); } } } fn is_valid_legacy(&self, mandatory_cid: bool) -> bool { self.fields.iter().all(|(key, value)| { (!mandatory_cid && *key == EntryType::Cid) || *value != None }) } fn is_valid(&self) -> bool { self.fields.iter().all(|(key, value)| { key.is_valid(value) }) } } fn main() { let data = include_str!("../data/input.txt"); // eprintln!("data = {}", data); //eprintln!("data.split() = {:?}", data.split("\n\n").collect::>()); let passports: Vec = data.split("\n\n").map(|pass_str| { let mut res = Passport::new(); res.parse(pass_str); res }).collect(); // eprintln!("passports = {:#?}", passports); // eprintln!("passports.size() = {:#?}", passports.len()); let nb_valid = passports.iter().filter(|p| (*p).is_valid_legacy(false)).count(); eprintln!("nb_valid = {:#?}", nb_valid); let nb_valid_secondpass = passports.iter().filter(|p| p.is_valid()).count(); eprintln!("nb_valid_secondpass = {:#?}", nb_valid_secondpass); } #[cfg(test)] mod tests { use super::*; #[test] fn test_validation() { let mut pass = Passport::new(); pass.parse("cid:271 pid:009457684 hcl:#80fab0\nbyr:1999 ecl:blu\niyr:2015\neyr:2029 hgt:159cm"); eprintln!("pass = {:#?}", pass); assert_eq!(pass.is_valid(), true) } }