

A Teacher Looks at Advent of Code 2020 - Day 4
source link: https://cestlaz.github.io/post/advent-2020-day04/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

A Teacher Looks at Advent of Code 2020 - Day 4
One of the nice things about Advent of Code is that it gets me to explore language features I haven't used yet. Today's problem got me to explore Clojure Spec which is a very cool validation library. There's a complete run through of the solution in Clojure in the video but here I'll talk about the problem in Python (mostly).
Today's problem is about validating passports. You start with a text file consisting of passport information. Each passport is one or more lines with each line having a bunch of key value pairs. For example, these two lines represent a passport for someone who's eye color (ecl) is gray (gry) and who was born (byr) in 1937:
ecl:gry pid:860033327 eyr:2020 hcl:#fffffd byr:1937 iyr:2017 cid:147 hgt:183cm
The catch is that one passport can span multiple lines and that passports are separated by two consecutive newlines in the file.
A passport has 8 field types with one, Country of Origin (cid) being optional.
For part 1, a valid passport is one that contains all 7 required fields.
The video goes over a Clojure solution which, I think is cleaner but the idea is the same as the Python I'll talk about here.
Splitting the data into a list of potential passports is easy because you can split the string on two newlines:
data = open("../data/sample04.dat").read()
data=data.split("\n\n")
Now we have a list of string.
Next, we can split each string on whitespace so that each string in each sublist is a string in the form k:v:
data_list = []
for d in data:
data_list.append([item for item in d.split()])
So, for example, data_list[0] might look like this:
['ecl:gry', 'pid:860033327', 'eyr:2020', 'hcl:#fffffd', 'byr:1937', 'iyr:2017', 'cid:147', 'hgt:183cm']
Finally, we can convert each passport into a dictionary:
data_dicts=[]
for d in data_list:
temp = { item.split(":")[0]:item.split(":")[1] for item in d}
temp.pop('cid',None)
data_dicts.append(temp)
The easiest way I came up with to check if a passport was valid was to make a set out of a list of required field names, make a set out of each potential passports field names (they're dictionary keys) and see if they're equal:
fields = set(["byr","iyr","eyr","hgt","hcl","ecl","pid"])
valid_passports = [set(x.keys()) == fields for x in data_dicts]
I think the Clojure code is cleaner but it's much the same.
Part two added a twist - you now have to not only see if the required fields are there but you had to make sure they had valid data. For example, height had to start with a positive integer followed by either cm or in. If it was cm, the number had to be in a certain range and if it was in it had to be within a different range.
This didn't sound hard but could get tricky. For each field type you could write a function that took in the value and returned true or false depending on its validity - lots of ad hoc code. You could then loop over all the passports and test to see if all the conditions were met.
It turns out that Clojure has a really cool library - Clojure Spec that does just that. You set up validators for each field type and then one for an entire passport. Here's the code:
(s/def ::byr (s/and string? #(>= (u/parse-int %) 1920) #(<= (u/parse-int %) 2002)))
(s/def ::iyr (s/and string? #(>= (u/parse-int %) 2010) #(<= (u/parse-int %) 2020)))
(s/def ::eyr (s/and string? #(>= (u/parse-int %) 2020) #(<= (u/parse-int %) 2030)))
(s/def ::hgt (s/and string? hgt-test))
(s/def ::hcl (s/and string? #(re-find #"#[0-9a-f]{6}" %)))
(s/def ::ecl (s/and string? #(re-find #"amb|blu|brn|gry|grn|hzl|oth" %)))
(s/def ::pid (s/and string? #(re-find #"^[0-9]{9}$" % )))
(s/def ::cid string?)
(s/def ::passport (s/keys
:req [::byr ::iyr ::eyr ::hgt ::hcl ::ecl ::pid]
:opt [::cid]))
(s/valid? ::passport test-passport)
The last line would test to see if test-passport was valid. It's all covered in detail in the video.
Clojure spec wasn't required for this problem but I've been meaning to play with it for a while and it led to a clean and elegant way of testing passports.
Not sure if I'll get to more posts or even solve more problems - I'm trying to limit my own screen time over the weekends but we'll see.
If you want to check out all the Clojure goodness here it is: Enjoy!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK