Advent of Code 2020, day 4

Programming Advent of Code, programming, puzzle

It’s day 4… As before, all my solutions are available on https://git.sr.ht/~schnouki/advent-of-code.

Today we’re dealing with passport validation. Spoiler alert: as often with data validation issues, it’s pretty boring…

Okay, let’s parse those passports. Split input when there are 2 newlines, split each block on whitespace, parse key:value entries. Boring!

class Passport(dict):
    @classmethod
    def from_kv_str(cls, data: str) -> "Passport":
        res = cls()
        for field in data.split():
            k, v = field.split(":", 1)
            res[k] = v
        return res

data = [Passport.from_kv_str(block) for block in raw_data.split("\n\n")]

Part 1

In this part we just need to check if all required fields are present in the passport. Nothing about their values, even empty strings are OK. Meh.

class Passport(dict):
    @property
    def is_valid_p1(self) -> bool:
        return all(
            field in self for field in ("byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid")
        )

print(sum(1 for p in data if p.is_valid_p1))

Done. 0.18 ms. Boring.

Part 2

Now we need to validate data. A few years with min/max, a height in cm or in, a hex color, a value in a set, and a number of a specific length… Meh, I don’t even want to try to make that fun.

import re

RE_HEIGHT = re.compile(r"^(\d+)(cm|in)$")
RE_COLOR = re.compile(r"^#[0-9a-f]{6}$")


class Passport(dict):
    @property
    def is_valid_p2(self) -> bool:
        checks = [
            self._check_year("byr", 1920, 2002),
            self._check_year("iyr", 2010, 2020),
            self._check_year("eyr", 2020, 2030),
            self._check_height(),
            RE_COLOR.match(self.get("hcl", "")),
            self.get("ecl") in ("amb", "blu", "brn", "gry", "grn", "hzl", "oth"),
            len(self.get("pid", "")) == 9 and self.get("pid", "").isdigit(),
        ]
        return all(checks)

    def _check_year(self, field: str, min: int, max: int) -> bool:
        if field not in self:
            return False
        try:
            return min <= int(self[field]) <= max
        except ValueError:
            return False

    def _check_height(self) -> bool:
        if "hgt" not in self:
            return False
        if (m := RE_HEIGHT.match(self["hgt"])) :
            min, max = (150, 193) if m.group(2) == "cm" else (59, 76)
            return min <= int(m.group(1)) <= max
        return False

Validation is pretty minimal, but that’s enough for this case as we’re dealing with well-behaved input anyway. 0.53 ms. Only cool thing: I got to use Python’s new walrus operator. Yay?