<jupyter_start><jupyter_text>Day 4: Passport Processinghttps://adventofcode.com/2020/day/4<jupyter_code>inputLines = lines <$> readFile "input/day04.txt"
testInput = [ "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"
            , ""
            , "hcl:#ae17e1 iyr:2013"
            , "eyr:2024"
            , "ecl:brn pid:760753108 byr:1931"
            , "hgt:179cm"
            , ""
            , "hcl:#cfa07d eyr:2025 pid:166559648"
            , "iyr:2011 ecl:brn hgt:59in"]
import Data.List.Split (splitOn)  -- install with 'stack install split'
import Data.List (intercalate)<jupyter_output><empty_output><jupyter_text>Split input on blank lines to get the individual passports:<jupyter_code>passports = splitOn "\n\n" . intercalate "\n"
passports testInput<jupyter_output><empty_output><jupyter_text>Represent a passport as a map:<jupyter_code>import qualified Data.Map as Map
parsePassport :: String -> Map.Map String String
parsePassport = Map.fromList
              . map ((\ [k, v] -> (k, v)) . splitOn ":")
              . words<jupyter_output><empty_output><jupyter_text>Part 1All fields except `cid` are required:<jupyter_code>required = ["byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid"]
checkPassport passport = all ((/= Nothing). (`Map.lookup` passport)) required
countValidPassports1 = length . filter id . map (checkPassport . parsePassport)<jupyter_output><empty_output><jupyter_text>Verify given result for test input:<jupyter_code>countValidPassports1 . passports $ testInput<jupyter_output><empty_output><jupyter_text>Solution, part 1<jupyter_code>countValidPassports1 . passports <$> inputLines<jupyter_output><empty_output><jupyter_text>Part 2For all fields that contain years, we have to compare the value with the minimal and maximal accepted year.<jupyter_code>intValidator :: Int -> Int -> String -> Bool
intValidator minValue maxValue value = minValue <= number && number <= maxValue
    where
        number = read value<jupyter_output><empty_output><jupyter_text>Hair color, eye color, and passport ID are best validated with regular expressions.<jupyter_code>import Text.Regex.PCRE  -- install with 'stack install regex-pcre'
regexValidator :: String -> String -> Bool
regexValidator pattern value = (value =~ pattern)<jupyter_output><empty_output><jupyter_text>To validate the height, we parse the value and the unit wit a regular expression. If this is successful, we compare the height with the min and max value for the respective unit.<jupyter_code>heightValidator :: String -> Bool
heightValidator value = 
    case heightAndUnit of 
        Nothing             -> False
        Just (height, unit) -> minHeight unit <= height && height <= maxHeight unit
    where
        heightAndUnit = do
            [[_, heightValue, unit]] <- value =~~ "^([0-9]+)(cm|in)$"
            return (read heightValue, unit) 

        minHeight "cm" = 150
        minHeight "in" = 59

        maxHeight "cm" = 193
        maxHeight "in" = 76<jupyter_output><empty_output><jupyter_text>Assign the correct validator to each passport field.<jupyter_code>validators :: [(String, String -> Bool)]
validators = [
    ("byr", intValidator 1920 2002),
    ("iyr", intValidator 2010 2020),
    ("eyr", intValidator 2020 2030),
    ("hgt", heightValidator),
    ("hcl", regexValidator "^#[0-9a-f]{6}$"),
    ("ecl", regexValidator "^(amb|blu|brn|gry|grn|hzl|oth)$"),
    ("pid", regexValidator "^[0-9]{9}$")]<jupyter_output><empty_output><jupyter_text>A passport is valid if all fields are valid.<jupyter_code>validatePassport passport = all 
    ((== Just True) . 
     (\ (field, validator) -> validator <$> field `Map.lookup` passport)) validators
countValidPassports2 = length . filter id . map (validatePassport . parsePassport)<jupyter_output><empty_output><jupyter_text>Solution, part 2<jupyter_code>countValidPassports2 . passports <$> inputLines<jupyter_output><empty_output><jupyter_text>Appendix `validatePassport` can be written without a lambda.<jupyter_code>validatePassport' passport = all 
    ((== Just True) . 
     uncurry (flip (<$>) . (`Map.lookup` passport))) validators
countValidPassports2' = length . filter id . map (validatePassport . parsePassport)
countValidPassports2' . passports <$> inputLines<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.12525389302640488, 'fraction_numerical': 0.03656059580230196, 'mean_word_length': 4.12962962962963, 'pattern_counts': {'":': 1, '<': 64, '<?xml version=': 0, '>': 71, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '13', 'dir': 'jupyter-structured-clean-dedup', 'id': '553725', 'license': 'MIT', 'n_tokens_mistral': 1572, 'n_tokens_neox': 1415, 'n_words': 348, 'path': '2020/day04-haskell.ipynb', 'repo_name': 'freininghaus/adventofcode', 'size': '4431'}