133 lines
4.4 KiB
Python
Executable file
133 lines
4.4 KiB
Python
Executable file
#! /usr/bin/env python3
|
|
|
|
import pathlib
|
|
import sys
|
|
import re
|
|
from dataclasses import dataclass
|
|
from pprint import pprint
|
|
|
|
@dataclass
|
|
class Passport:
|
|
birthYear: int | None
|
|
issueYear: int | None
|
|
expirationYear: int | None
|
|
height: str | None
|
|
hairColor: str | None
|
|
eyeColor: str | None
|
|
passportId: str | None
|
|
countryId: str | None
|
|
|
|
def regexUnwrap(match, group:int)->str | None:
|
|
return match[group] if match is not None else None
|
|
|
|
def parse(puzzle_input: str)->list[Passport]:
|
|
"""Parse input"""
|
|
rawRecords = [i.replace('\n', ' ') for i in puzzle_input.split('\n\n')]
|
|
records = []
|
|
for rawRecord in rawRecords:
|
|
birthYear=re.search(r'byr:(\S+)', rawRecord)
|
|
issueYear=re.search(r'iyr:(\S+)', rawRecord)
|
|
expirationYear=re.search(r'eyr:(\S+)', rawRecord)
|
|
height=re.search(r'hgt:(\S+)', rawRecord)
|
|
hairColor=re.search(r'hcl:(\S+)', rawRecord)
|
|
eyeColor=re.search(r'ecl:(\S+)', rawRecord)
|
|
passportId=re.search(r'pid:(\S+)', rawRecord)
|
|
countryId=re.search(r'cid:(\S+)', rawRecord)
|
|
records.append(Passport(
|
|
int(birthYear[1]) if birthYear is not None else None,
|
|
int(issueYear[1]) if issueYear is not None else None,
|
|
int(expirationYear[1]) if expirationYear is not None else None,
|
|
regexUnwrap(height,1),
|
|
regexUnwrap(hairColor,1),
|
|
regexUnwrap(eyeColor,1),
|
|
regexUnwrap(passportId,1),
|
|
regexUnwrap(countryId,1),
|
|
))
|
|
return records
|
|
|
|
def lazyCheckPassport(passport: Passport)->bool:
|
|
return (
|
|
passport.birthYear is not None
|
|
and passport.issueYear is not None
|
|
and passport.expirationYear is not None
|
|
and passport.height is not None
|
|
and passport.hairColor is not None
|
|
and passport.eyeColor is not None
|
|
and passport.passportId is not None
|
|
)
|
|
|
|
def part1(data):
|
|
"""Solve part 1"""
|
|
return sum(1 for record in data if lazyCheckPassport(record))
|
|
|
|
def checkBirthYear(passport: Passport)->bool:
|
|
if passport.birthYear is None: return False
|
|
return (1920<=passport.birthYear<=2002)
|
|
|
|
def checkIssueYear(passport: Passport)->bool:
|
|
if passport.issueYear is None: return False
|
|
return (2010<=passport.issueYear<=2020)
|
|
|
|
def checkExpirationYear(passport: Passport)->bool:
|
|
if passport.expirationYear is None: return False
|
|
return (2020<=passport.expirationYear<=2030)
|
|
|
|
def checkHeight(passport: Passport)->bool:
|
|
if passport.height is None: return False
|
|
rematch = re.match(r'(\d+)((?:in)|(?:cm))', passport.height)
|
|
if rematch is None: return False
|
|
number = int(rematch.group(1))
|
|
unit = rematch.group(2)
|
|
if unit == 'in':
|
|
return (59<=number<=76)
|
|
else:
|
|
return (150<=number<=193)
|
|
|
|
def checkHairColour(passport: Passport)->bool:
|
|
if passport.hairColor is None: return False
|
|
return (re.match(r'#[0123456789abcdef]{6}$', passport.hairColor) is not None)
|
|
|
|
def checkEyeColour(passport: Passport)->bool:
|
|
if passport.eyeColor is None: return False
|
|
return (passport.eyeColor == 'amb'
|
|
or passport.eyeColor == 'blu'
|
|
or passport.eyeColor == 'brn'
|
|
or passport.eyeColor == 'gry'
|
|
or passport.eyeColor == 'grn'
|
|
or passport.eyeColor == 'hzl'
|
|
or passport.eyeColor == 'oth'
|
|
)
|
|
|
|
def checkPassportId(passport: Passport)->bool:
|
|
if passport.passportId is None: return False
|
|
return (re.match(r'[0-9]{9}$', passport.passportId) is not None)
|
|
|
|
def checkPassport(passport: Passport)->bool:
|
|
return (checkBirthYear(passport)
|
|
and checkIssueYear(passport)
|
|
and checkExpirationYear(passport)
|
|
and checkHeight(passport)
|
|
and checkHairColour(passport)
|
|
and checkEyeColour(passport)
|
|
and checkPassportId(passport)
|
|
)
|
|
|
|
def part2(data):
|
|
"""Solve part 2"""
|
|
return sum(1 for record in data if checkPassport(record))
|
|
|
|
def solve(puzzle_input):
|
|
"""Solve the puzzle for the given input"""
|
|
data = parse(puzzle_input)
|
|
solution1 = part1(data)
|
|
solution2 = part2(data)
|
|
|
|
return solution1, solution2
|
|
|
|
if __name__ == "__main__":
|
|
for path in sys.argv[1:]:
|
|
print(f"{path}:")
|
|
puzzle_input = pathlib.Path(path).read_text().strip()
|
|
solutions = solve(puzzle_input)
|
|
print("\n".join(str(solution) for solution in solutions))
|