Sean McLemon | Advent of Code

Home | Czech | Blog | GitHub | Advent Of Code | Notes


2020-12-04 - Passport Processing

(original .ipynb)

Day 4 puzzle input is a set of data representing attributes of identification documents - possibly passports - with each document terminated by a blank line (mine is here). Part 1 involves parsing the input and returning the number of these documents which contain all of the required fields. Part 2 involves taking these parsed documents and applying some more detailed validation to each document's attributes and counting how many documents pass all validation.

test_input_str = """ecl:gry pid:860033327 eyr:2020 hcl:#fffffd
byr:1937 iyr:2017 cid:147 hgt:183cm

iyr:2013 ecl:amb cid:350 eyr:2023 pid:028048884
hcl:#cfa07d byr:1929

hcl:#ae17e1 iyr:2013
eyr:2024
ecl:brn pid:760753108 byr:1931
hgt:179cm

hcl:#cfa07d eyr:2025 pid:166559648
iyr:2011 ecl:brn hgt:59in"""

puzzle_input_str = open("puzzle_input/day4.txt").read()

required_fields = [
    "ecl", "pid", "eyr", "hcl", "byr", "iyr", "hgt"
]

def has_required_fields(passport):
    return all(field in passport for field in required_fields)

def parse_passport(input_line):
    passport_elements = input_line.split(" ")
    passport = {}
    
    for element in passport_elements:
        k,v = element.split(":")
        passport[k] = v
        
    return passport

def parse_passports(input_str):
    input_lines = input_str.split("\n")
    raw_passport_data = []
    current_passport = []
    
    for input_line in input_lines:
        input_line = input_line.strip()
        
        if len(input_line) == 0:
            raw_passport_data.append(" ".join(current_passport))
            current_passport = []
        else:
            current_passport.append(input_line)
            
    raw_passport_data.append(" ".join(current_passport))
    passports = [parse_passport(passport_data) for passport_data in raw_passport_data]
    
    return [p for p in passports if has_required_fields(p)]
    
assert 2 == len(parse_passports(test_input_str))
print(len(parse_passports(puzzle_input_str)))
200
import re

def valid_passport(passport):
    
    validation_funcs = [
        validate_byr,
        validate_iyr,
        validate_eyr,
        validate_hgt,
        validate_hcl,
        validate_ecl,
        validate_pid
    ]
    
    return all(validate(passport) for validate in validation_funcs)
    
#     byr (Birth Year) - four digits; at least 1920 and at most 2002.
def validate_byr(passport):
    byr_str = passport["byr"]
    
    if not re.match("^\d{4}$", byr_str):
        return False
    
    try:
        byr = int(byr_str)
        return byr >= 1920 and byr <= 2002
    except:
        return False

#     iyr (Issue Year) - four digits; at least 2010 and at most 2020.
def validate_iyr(passport):
    iyr_str = passport["iyr"]
    
    if not re.match("^\d{4}$", iyr_str):
        return False
    
    try:
        iyr = int(iyr_str)
        return iyr >= 2010 and iyr <= 2020 and len(passport["iyr"]) == 4
    except:
        return False

#     eyr (Expiration Year) - four digits; at least 2020 and at most 2030.
def validate_eyr(passport):
    eyr_str = passport["eyr"]
    if not re.match("^\d{4}$", eyr_str):
        return False
    
    eyr = int(eyr_str, 10)
    return eyr >= 2020 and eyr <= 2030
    
def parse_hgt(hgt_str):
    end_hgt = -1
    units = ""
    
    if not re.match("^\d+(cm|in)$", hgt_str):
        return None
    
    if "cm" in hgt_str:
        end_hgt = hgt_str.index("cm")
        units = "cm"
    if "in" in hgt_str:
        end_hgt = hgt_str.index("in")
        units = "in"
        
    if end_hgt > 0:
        return (int(hgt_str[:end_hgt]), units)
    
    return None

#     hgt (Height) - 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.
def validate_hgt(passport):
    hgt = parse_hgt(passport["hgt"])
    if not hgt:
        return False
    
    value, units = hgt
    if units == "cm":
        return value >= 150 and value <= 193
    elif units == "in":
        return value >= 59 and value <= 76
    
    return False

#     hcl (Hair Color) - a # followed by exactly six characters 0-9 or a-f.
def validate_hcl(passport):
    return parse_hcl(passport["hcl"]) != None

def parse_hcl(hcl_str):
    if not re.match("^#[0-9a-f]{6}$", hcl_str):
        return None
    
    return int(hcl_str[1:], 16)


valid_eye_colors = set("amb blu brn gry grn hzl oth".split(" "))

#     ecl (Eye Color) - exactly one of: amb blu brn gry grn hzl oth.
def validate_ecl(passport):
    return passport["ecl"] in valid_eye_colors
    
def validate_pid(passport):
    pid = parse_pid(passport["pid"])
    return pid != None
    
#     pid (Passport ID) - a nine-digit number, including leading zeroes.
def parse_pid(pid_str):
    if not re.match("^\d{9}$", pid_str):
        return None
    
    return int(pid_str)

def parse_and_validate_passports(input_str):
    passports = parse_passports(input_str)
    return [p for p in passports if valid_passport(p)]

# byr valid:   2002
# byr invalid: 2003
assert validate_byr({"byr": "2002"})
assert not validate_byr({"byr": "2003"})

# hgt valid:   60in
# hgt valid:   190cm
# hgt invalid: 190in
# hgt invalid: 190
assert validate_hgt({"hgt": "60in"})
assert validate_hgt({"hgt": "190cm"})
assert not validate_hgt({"hgt": "190in"})
assert not validate_hgt({"hgt": "190"})

# hcl valid:   #123abc
# hcl invalid: #123abz
# hcl invalid: 123abc
assert validate_hcl({"hcl":"#123abc"})
assert not validate_hcl({"hcl":"#123abz"})
assert not validate_hcl({"hcl":"123abc"})

# ecl valid:   brn
# ecl invalid: wat
assert validate_ecl({"ecl":"brn"})
assert not validate_ecl({"ecl":"wat"})

# pid valid:   000000001
# pid invalid: 0123456789
assert validate_pid({"pid":"000000001"})
assert not validate_pid({"pid":"0123456789"})

print(len(parse_and_validate_passports(puzzle_input_str)))
116