Advent of Code: 2020 Day 04 
Tags: adventofcode, python
Published: 2020-12-04 07:56:02

**SPOILER ALERT**
This is a post with my solutions and learnings from the puzzle. Don't continue
reading if you haven't tried the puzzle on your own yet.

If you want to do the puzzle, visit [adventofcode.com/2020/day/4](https://adventofcode.com/2020/day/4).

My programming language of choice is `python` and all examples below are in python.

## Key learnings

* Debugging and testing

Todays puzzle has a number of specific requirements to follow. The key learning I took away from it too handle edge cases, follow specification and having a systematic way of debugging. A good practice would to create tests for the edge cases to ensure everything works as expected.

## Puzzle

The challenge is to validate data for a thousand passport.

Example input for two passports:

```
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
```

They fields are defined as:
- byr (Birth Year)
- iyr (Issue Year)
- eyr (Expiration Year)
- hgt (Height)
- hcl (Hair Color)
- ecl (Eye Color)
- pid (Passport ID)
- cid (Country ID)

## Part 1

All fields except for Country ID (cid) are mandatory.
Validate that all mandatory fields are present.

### Parse input

First step is to save the input in a local file and parse it in python:
```python
# Open the input file
inputfile = open('04.input', 'r')

# Parse lines
data = [x.strip() for x in inputfile.readlines()]
```

### Solution

```python
def part1(data):
    valid_passport_count = 0
    required_fields = ['byr' ,'iyr' ,'eyr' ,'hgt' ,'hcl' ,'ecl' ,'pid']

    # Variable to track number of required fields for current passport
    current = 0
    for line in data:
        if line == '':                              # Empty line indicates new passport
            if current == len(required_fields):
                valid_passport_count += 1
            current = 0
            continue

        for field in line.split():
            key, val = field.split(':')
            if key in required_fields:
                current += 1

    return valid_passport_count

print "Solution part 1: %d" % part1(data)
```

## Part 2

The second part adds validation for each field. The requirements are:

- byr (Birth Year) - four digits; at least 1920 and at most 2002.
- iyr (Issue Year) - four digits; at least 2010 and at most 2020.
- eyr (Expiration Year) - four digits; at least 2020 and at most 2030.
- 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.
- hcl (Hair Color) - a # followed by exactly six characters 0-9 or a-f.
- ecl (Eye Color) - exactly one of: amb blu brn gry grn hzl oth.
- pid (Passport ID) - a nine-digit number, including leading zeroes.
- cid (Country ID) - ignored, missing or not.


### My Solution

This part will have as many variations of solutions as there are developers. The important part is to read carufully the requirements and test against edge-cases. I did my testing continuously by printing relevant variables while coding. The solution below is therefore not the whole "solution" as I see the process as important as the final code.

```python
def valid(passport):
    # Validate mandatory fields
    fields = ['byr' ,'iyr' ,'eyr' ,'hgt' ,'hcl' ,'ecl' ,'pid']
    for f in fields:
        if(f not in passport):
            return False

    # Validate numerical
    if not ( 1920 <= int(passport['byr']) <= 2002):
        return False
    if not ( 2010 <= int(passport['iyr']) <= 2020):
        return False
    if not ( 2020 <= int(passport['eyr']) <= 2030):
        return False

    # Validate Height
    if 'cm' in passport['hgt'] and not (150 <= int(passport['hgt'][:-2]) <=193):
        return False
    elif 'in' in passport['hgt'] and not (59 <= int(passport['hgt'][:-2]) <= 76):
        return False
    if 'cm' not in passport['hgt'] and 'in' not in passport['hgt']:
        return False

    # Validate strings/enums
    if  passport['ecl'] not in ['amb', 'blu', 'brn','gry','grn','hzl','oth']:
        return False
    if re.match(r'^\#[0-9a-f]{6}$', passport['hcl']) is None:
        return False
    if re.match(r'^\d{9}$', passport['pid']) is None:
        return False

    return True

def part2(data):
    valid_passport_count = 0
    current = {}
    for line in data:
        if line == '':
            if valid(current):
                valid_passport_count += 1
            current = {}
            continue

        for field in line.split():
            field,val = field.split(':')
            current[field] = val
    return valid_passport_count
```

## Comments

One edge-case I missed was the passport-id with 9 digits. I forgot to add start and end notation on the regex. A 10-digit passport-id was valid as it did in fact contain a 9-digit number. This off by one error was hard for me to find. The lesson learned is to test edge-cases.

Though it is not practical in a "competition" environment. Therefore I tested by using `print` continuously to track what got passed and what didn't.

## Thanks for reading!

I hope these solutions were helpful for you. Just ask if anything was hard to grasp.

Complete code can be found at: [github.com/cNille/AdventOfCode/blob/master/2020/04.py](https://github.com/cNille/AdventOfCode/blob/master/2020/04.py)