use std::collections::{HashMap, HashSet}; use std::error::Error; use lazy_static::lazy_static; use crate::utils::GenericError; #[path = "../utils/mod.rs"] mod utils; fn main() -> Result<(), Box<dyn Error>> { let lines = utils::read_input_lines()?; let passports = load_passports(&lines)?; let passports = passports.into_iter().filter(|p| p.is_valid_or_from_north_pole()).collect::<Vec<Passport>>(); println!("Valid passports with no field validation: {}", passports.len()); println!("Valid passports with field validation: {}", passports.iter().filter(|p| p.are_field_values_valid()).count()); Ok(()) } fn load_passports(lines: &Vec<String>) -> Result<Vec<Passport>, GenericError> { let mut passports = Vec::new(); let mut passport = Passport::new(); for line in lines { if line.is_empty() { passports.push(passport); passport = Passport::new(); } else { passport.load_fields_from_line(line.as_str())?; } } passports.push(passport); Ok(passports) } #[derive(Eq, PartialEq, Hash, Debug, Copy, Clone)] enum PassportField { BirthYear, IssueYear, ExpirationYear, Height, HairColor, EyeColor, PassportId, CountryId, } impl PassportField { fn from(s: &str) -> Option<PassportField> { match s { "byr" => Some(PassportField::BirthYear), "iyr" => Some(PassportField::IssueYear), "eyr" => Some(PassportField::ExpirationYear), "hgt" => Some(PassportField::Height), "hcl" => Some(PassportField::HairColor), "ecl" => Some(PassportField::EyeColor), "pid" => Some(PassportField::PassportId), "cid" => Some(PassportField::CountryId), _ => None } } fn is_value_valid(&self, value: &str) -> bool { fn as_u32(value: &str) -> Option<u32> { value.parse().ok() } fn as_u32_with_unit(value: &str, unit: &str) -> Option<u32> { value.strip_suffix(unit).and_then(as_u32) } match self { PassportField::BirthYear => as_u32(value).filter(|year| *year >= 1920 && *year <= 2002).is_some(), PassportField::IssueYear => as_u32(value).filter(|year| *year >= 2010 && *year <= 2020).is_some(), PassportField::ExpirationYear => as_u32(value).filter(|year| *year >= 2020 && *year <= 2030).is_some(), PassportField::Height => { if let Some(height) = as_u32_with_unit(value, "cm") { height >= 150 && height <= 193 } else if let Some(height) = as_u32_with_unit(value, "in") { height >= 59 && height <= 76 } else { false } } PassportField::HairColor => value.strip_prefix('#').filter(|hex| hex.chars().all(|c| c.is_digit(16))).is_some(), PassportField::EyeColor => VALID_EYE_COLORS.contains(value), PassportField::PassportId => value.len() == 9 && value.chars().all(|c| c.is_ascii_digit()), PassportField::CountryId => true } } } struct Passport { fields: HashMap<PassportField, String>, } impl Passport { fn new() -> Passport { Passport { fields: HashMap::new() } } fn load_fields_from_line(&mut self, line: &str) -> Result<(), GenericError> { for field_entry in line.split(' ') { let (field_name, field_value) = field_entry.split_once(':').ok_or_else(|| GenericError::new("Passport entry is missing a colon."))?; let field = PassportField::from(field_name).ok_or_else(|| GenericError::new(format!("Passport field is invalid: {}", field_name)))?; self.fields.insert(field, field_value.to_string()); } Ok(()) } fn is_valid_or_from_north_pole(&self) -> bool { let fields = &self.fields.keys().map(|f| *f).collect::<HashSet<PassportField>>(); return fields.is_superset(&REQUIRED_FIELDS); } fn are_field_values_valid(&self) -> bool { return self.fields.iter().all(|(field, value)| field.is_value_valid(value.as_str())); } } lazy_static! { static ref REQUIRED_FIELDS: HashSet<PassportField> = HashSet::from([ PassportField::BirthYear, PassportField::IssueYear, PassportField::ExpirationYear, PassportField::Height, PassportField::HairColor, PassportField::EyeColor, PassportField::PassportId ]); static ref VALID_EYE_COLORS: HashSet<&'static str> = HashSet::from([ "amb", "blu", "brn", "gry", "grn", "hzl", "oth" ]); }