Skip to main content

year2020/
day04.rs

1use utils::prelude::*;
2
3/// Parsing and validating records.
4#[derive(Clone, Debug)]
5pub struct Day04 {
6    part1: usize,
7    part2: usize,
8}
9
10const BIRTH_YEAR: u8 = 0;
11const ISSUE_YEAR: u8 = 1;
12const EXPIRATION_YEAR: u8 = 2;
13const HEIGHT: u8 = 3;
14const HAIR_COLOR: u8 = 4;
15const EYE_COLOR: u8 = 5;
16const PASSPORT_ID: u8 = 6;
17const COUNTRY_ID: u8 = 7;
18const REQUIRED_FIELDS: u8 = 0b0111_1111;
19
20impl Day04 {
21    pub fn new(input: &str, _: InputType) -> Result<Self, InputError> {
22        let key = parser::literal_map!(
23            "byr:" => BIRTH_YEAR,
24            "iyr:" => ISSUE_YEAR,
25            "eyr:" => EXPIRATION_YEAR,
26            "hgt:" => HEIGHT,
27            "hcl:" => HAIR_COLOR,
28            "ecl:" => EYE_COLOR,
29            "pid:" => PASSPORT_ID,
30            "cid:" => COUNTRY_ID,
31        );
32        let value = parser::take_while1(u8::is_ascii_graphic);
33        let kv = parser::parse_tree!(
34            (k @ key, v @ value) =>> {
35                (b' ') => (k, v, false),
36                (parser::eol()) =>> {
37                    (parser::eol()) => (k, v, true),
38                    (parser::noop()) => (k, v, false),
39                },
40            },
41        );
42
43        let (mut part1, mut part2) = (0, 0);
44        let (mut fields, mut valid) = (0, true);
45        for entry in kv.parse_iterator(input) {
46            let (field, value, end) = entry?;
47
48            let bit = 1 << field;
49            if fields & bit != 0 {
50                return Err(InputError::new(input, value, "duplicate passport field"));
51            }
52            fields |= bit;
53            valid &= Self::valid_value(field, value);
54
55            if end {
56                let complete = fields & REQUIRED_FIELDS == REQUIRED_FIELDS;
57                part1 += usize::from(complete);
58                part2 += usize::from(complete & valid);
59                (fields, valid) = (0, true);
60            }
61        }
62
63        if fields != 0 {
64            return Err(InputError::new(
65                input,
66                input.len(),
67                "expected passport field after space",
68            ));
69        }
70
71        Ok(Self { part1, part2 })
72    }
73
74    #[inline]
75    fn valid_value(index: u8, value: &[u8]) -> bool {
76        match index {
77            BIRTH_YEAR => Self::num_range(value, b"1920", b"2002"),
78            ISSUE_YEAR => Self::num_range(value, b"2010", b"2020"),
79            EXPIRATION_YEAR => Self::num_range(value, b"2020", b"2030"),
80            HEIGHT => match value {
81                [value @ .., b'c', b'm'] => Self::num_range(value, b"150", b"193"),
82                [value @ .., b'i', b'n'] => Self::num_range(value, b"59", b"76"),
83                _ => false,
84            },
85            HAIR_COLOR => {
86                value.len() == 7
87                    && value[0] == b'#'
88                    && value[1..]
89                        .iter()
90                        .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
91            }
92            EYE_COLOR => {
93                matches!(
94                    value,
95                    b"amb" | b"blu" | b"brn" | b"gry" | b"grn" | b"hzl" | b"oth"
96                )
97            }
98            PASSPORT_ID => value.len() == 9 && value.iter().all(u8::is_ascii_digit),
99            COUNTRY_ID => true,
100            _ => unreachable!(),
101        }
102    }
103
104    #[inline]
105    fn num_range<const N: usize>(value: &[u8], min: &[u8; N], max: &[u8; N]) -> bool {
106        value.len() == N && value.iter().all(u8::is_ascii_digit) && value >= min && value <= max
107    }
108
109    #[must_use]
110    pub fn part1(&self) -> usize {
111        self.part1
112    }
113
114    #[must_use]
115    pub fn part2(&self) -> usize {
116        self.part2
117    }
118}
119
120examples!(Day04 -> (usize, usize) [
121    {file: "day04_example0.txt", part1: 2},
122    {file: "day04_example1.txt", part2: 0},
123    {file: "day04_example2.txt", part2: 4},
124]);