Day 6

This is a fantastic use case for lens. We define a passport to be a list of fields and a field to be a key value pair. The parser is also rather simple.

data Field = Field { _fname :: String, _fvalue :: String }
  deriving (Show, Eq)
makeLenses ''Field

type Passport = [Field]

instance Read Field where
  readPrec = lift $ do
    skipSpaces
    f <- manyTill get (char ':')
    v <- many get
    pure $ Field f v

With some glue logic we can take a string and convert it into a list of passports.

passports :: String -> [Passport]
passports = fmap (fmap read . words . unwords) . splitOn [""] . lines

We want to split the lines by empty lines and then for each group of lines we get the relevant field information out of it.

For part A, we just want to check that the desired fields are there. With lenses, this is as simple as

hasField :: String -> Passport -> Bool
hasField f = anyOf (traverse . fname) (==f)

isValidA :: Passport -> Bool
isValidA p = and $ hasField <$> ["byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid"] <*> pure p

For part B, we need to perform extra validation. The skeleton would look something like this.

validateField :: String -> (String -> Bool) -> Passport -> Bool
validateField f g = allOf (traverse . filtered ((==f) . view fname) . fvalue) g

We then need the validation logic and the final validation.

byr :: String -> Bool
byr y' = case readMaybe @Int y' of
           Just y  -> length y' == 4 && y >= 1920 && y <= 2002
           Nothing -> False

iyr :: String -> Bool
iyr y' = case readMaybe @Int y' of
           Just y  -> length y' == 4 && y >= 2010 && y <= 2020
           Nothing -> False

eyr :: String -> Bool
eyr y' = case readMaybe @Int y' of
           Just y  -> length y' == 4 && y >= 2020 && y <= 2030
           Nothing -> False

hgt :: Int -> String -> Bool
hgt n "cm" = n >= 150 && n <= 193
hgt n "in" = n >= 59 && n <= 76
hgt n (x:xs) | isDigit x = hgt (10 * n + ord x - ord '0') xs
             | otherwise = False
hgt _ _ = False

hcl :: String -> Bool
hcl ('#':hc) = length hc == 6 && all isHex hc
  where isHex c = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')
hcl _ = False

ecl :: String -> Bool
ecl ec = ec `elem` ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"]

pid :: String -> Bool
pid p = length p == 9 && all isDigit p

isValidB :: Passport -> Bool
isValidB p = isValidA p && (and $ validations <*> pure p)
  where validations = [ validateField "byr" byr
                      , validateField "iyr" iyr
                      , validateField "eyr" eyr
                      , validateField "hgt" (hgt 0)
                      , validateField "hcl" hcl
                      , validateField "ecl" ecl
                      , validateField "pid" pid
                      ]