191 lines
6.4 KiB
Rust
191 lines
6.4 KiB
Rust
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<Self, Self::Err> {
|
|
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::<i64>(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::<i64>(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::<i64>(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::<i64>(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::<i64>(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::<String>()))
|
|
}
|
|
(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::<i64>(value).is_ok()
|
|
}
|
|
(_, EntryType::Cid) => {
|
|
// ignored, missing or not
|
|
true
|
|
}
|
|
_ => false
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Passport<'a> {
|
|
fields: HashMap<EntryType, Option<&'a str>>
|
|
}
|
|
|
|
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::<Vec<&str>>());
|
|
let passports: Vec<Passport> = 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)
|
|
}
|
|
} |