Day 4 - Daily routine

In today's assignment, we have to check passports to see if they are valid. There are different fields with information in it. For a password to be valid there should be 7 fields present and there is 1 specific field that may be present. In part 2 there were some extra rules for the 7 fields.

There are two things stopping me from getting a decent time in the competition. The first is that although I knew how to solve the problem, I had a hard time finding the right functions (and syntax for them) in Haskell. Finishing part 1 already took 44 minutes because of that. That gets me to the second problem. Today I, of course, got up a little before 6, but normally I get up at around 7, and I prepare breakfast and wake my wife and kids and we have breakfast together. So today I worked on the assignment till a little to 7 and switched over to my normal routine. Had breakfast, prepared the kids for school. And brought my youngest daughter to school. Usually, this is done by my wife, but my daughter wanted to ride on her own bike to school today. Since my wife is over 37 weeks pregnant, she is not able to have enough control over my daughter riding her bike herself if she makes a mistake. She is 5 so she doesn't always have full control over the bike. So today I offered to bring her to school. After getting home, my normal working day starts on which I have a meeting at 9, so I had to follow that. So at 9.30 I finally got some time to spend on Advent of Code (I can be flexible in my working hours, so there is no problem having a break from work.) So at three minutes to 10, I was done with part 2, just in time for my next meeting. But oh man, 43rd place of the day and dropped way down to 12th place in the full competition.

Although I spend less than one and a half hours on the assignment, which was still quite slow due to my inexperience in Haskell, the time on the clock was almost 4 hours. So I really needed to let it sink in that this might happen more often and that reaching a top 5 spot might pose a problem. After a few hours, I had accepted it and will aim at the top 5, but have full realization that I might not make it.

Let's go over the solution. First I've written a parse function. The different passwords are separated by an empty line, so that will be two newline characters. The fields in the passport are separated by either space or newline. I replaced the newlines with spaces and then split on spaces. I googled for a replace function, but could find anything, luckily in Haskell you can write such a function easily yourself. Lastly, the fields are split into a key and value on a colon. The init is to remove a last empty passport.

replace r b input = map repl input where repl c = if c == r then b else c

parse = init . 
        (map (map (splitOn ":"))) . 
        (map (splitOn " ")) . 
        (map (replace '\n' ' ')) . 
        splitOn "\n\n"

We now have a list of passports, which consist of a list of fields, which consist of a list with a key and value. Our solution is to filter over all the passports. For each passport check if there are 7 fields after filtering out our optional 'cid' field. The length of the resulting list is our answer.

checkCountryId (key:value:[]) = key /= "cid"

isValid = ((==7) . length ) . (filter checkCountryId)

solve1 checks = length . filter isValid . parse

This gave me the correct answer for part 1, thus so far quite simple, although I took way more time than I had hoped because I had to google all the syntax and had problems finding the replace function.

Part 2

For the second part, we have to check all the fields against certain conditions. I changed my current code into something more generic where I could provide a list of filters. With this, I could filter out the optional field, but also filter invalid fields. If one of the fields would be invalid this would lead to a count of fields less than 7.

checkCountryId (key:value:[]) = key /= "cid"

isValid checks xs = ((==7) . length ) $
           foldr filter xs checks

solve checks = length . filter (isValid checks) . parse

solve1 = solve [ checkCountryId ]

To check every filter I use the foldr function which loops over a list (in this case the list of filters), it starts with one input (in this case our fields) and applies the provided function on the input, the output of that function is the input for the next iteration

For solving part 2, I wrote filter functions for all the fields. Again this took way more time because I had to lookup everything. But in the end, we have a working solution with generic code for part 1 and 2, just as I like it.

between l h i = l <= i && i <= h

parsedNumberBetween l h value = go l h parsedMaybe where
  parsedMaybe = readMaybe value :: Maybe Int
  go _ _ Nothing    = False
  go l h (Just val) = between l h val

checkBirthYear (key:value:[]) = key /= "byr" || parsedNumberBetween 1920 2002 value
checkIssueYear (key:value:[]) = key /= "iyr" || parsedNumberBetween 2010 2020 value
checkExpirationYear (key:value:[]) = key /= "eyr" || parsedNumberBetween 2020 2030 value
checkHairColor (key:(ht:clr):[]) = key /= "hcl" || (ht == '#' && all (\x -> elem x "0123456789abcdef") clr)
checkEyeColor (key:value:[]) = key /= "ecl" || value `elem` ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"]
checkPassportId (key:value:[]) = key /= "pid" || (all isDigit value && length value == 9)
checkHeight (key:value:[]) = key /= "hgt" || (unit == "in" && between 59 76 val) || (unit == "cm" && between 150 193 val) where
  parts = splitAt ((length value) - 2) value
  val = fromMaybe 0 $ (readMaybe $ fst parts :: Maybe Int)
  unit = snd parts
checkEmpty = ((==2) . length)

solve2 = solve [ checkBirthYear, checkIssueYear, checkExpirationYear,
                 checkHeight, checkHairColor, checkEyeColor,
                 checkPassportId, checkCountryId ]

Although this code actually describes what this problem is about. I found out by looking at other solutions that a way more simple approach would have been to check the validness of a passport by using regular expressions and just checked it as text. In that case, you would only have to split on the double newline. And filtered over the resulting list with a regex which might match or not. This could be implemented in a simple onliner (with a quite long regex.)

Happy with my solution, although I think it can be improved. But not too happy about how much time it took. Hoping for a better result for tomorrow.