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
]