--- /dev/null
+(asdf:load-system :adventofcode2020)
+(in-package #:adventofcode2020)
+
+(defun parse-passport (str-list)
+ (flet ((parser (str)
+ (->> (split-sequence #\Space str)
+ (mapcar (fn* (destructuring-bind (a b) (split-sequence #\: _)
+ (list (read-from-string a) b)))))))
+ (mapcan #'parser str-list)))
+
+(defun four-digits-test (pred)
+ (lambda (s)
+ (cl-ppcre:register-groups-bind
+ ((#'parse-integer value)) ("^([0-9]{4})$" s)
+ (funcall pred value))))
+
+(defparameter *required-field-tests*
+ (list 'byr (four-digits-test (fn* (<= 1920 _ 2002)))
+ 'iyr (four-digits-test (fn* (<= 2010 _ 2020)))
+ 'eyr (four-digits-test (fn* (<= 2020 _ 2030)))
+ 'hgt (fn* (cl-ppcre:register-groups-bind
+ ((#'parse-integer value) unit) ("^([0-9]+)(in|cm)$" _)
+ (cond
+ ((string= unit "in") (<= 59 value 76))
+ ((string= unit "cm") (<= 150 value 193))
+ (t nil))))
+ 'hcl (fn* (cl-ppcre:scan "^#[0-9a-f]{6}$" _))
+ 'ecl (fn* (cl-ppcre:scan "^(amb|blu|brn|gry|grn|hzl|oth)$" _))
+ 'pid (fn* (cl-ppcre:scan "^[0-9]{9}$" _))
+ 'cid (constantly t)))
+
+(defun simple-validate-passport (passport)
+ (let ((required-fields '(byr iyr eyr hgt hcl ecl pid)))
+ (every #'identity (mapcar (fn* (assoc _ passport)) required-fields))))
+
+(defun complex-validate-passport (passport)
+ (flet ((check (pair)
+ (destructuring-bind (field value) pair
+ (funcall (getf *required-field-tests* field) value))))
+ (and
+ (simple-validate-passport passport)
+ (every #'identity (mapcar #'check passport)))))
+
+(day 04 input
+ (let ((passports (-<>> (list-from input)
+ (split-sequence "" <> :test #'string=)
+ (mapcar #'parse-passport))))
+ (part1 (count t (mapcar #'simple-validate-passport passports)))
+ (part2 (count t (mapcar #'complex-validate-passport passports)))))
+
+(def-suite day04)
+(in-suite day04)
+
+(defvar *passports*
+ '(("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")))
+
+(test parse-passports
+ (is (equal
+ '(((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")))
+ (mapcar #'parse-passport *passports*))))
+
+(test simple-validate-passports
+ (is (equal
+ '(t nil t nil)
+ (mapcar (compose #'simple-validate-passport #'parse-passport) *passports*))))
+
+(test passport-field-validators
+ (flet ((check (pair)
+ (destructuring-bind (field value) pair
+ (funcall (getf *required-field-tests* field) value))))
+ (is-true (check '(byr "2002")))
+ (is-false (check '(byr "2003")))
+
+ (is-true (check '(hgt "60in")))
+ (is-true (check '(hgt "190cm")))
+ (is-false (check '(hgt "190in")))
+ (is-false (check '(hgt "190")))
+
+ (is-true (check '(hcl "#123abc")))
+ (is-false (check '(hcl "#123abz")))
+ (is-false (check '(hcl "123abc")))
+
+ (is-true (check '(ecl "brn")))
+ (is-false (check '(ecl "wat")))
+
+ (is-true (check '(pid "000000001")))
+ (is-false (check '(pid "0123456789")))))
+
+(defvar *valid-passports*
+ '(((pid "087499704") (hgt "74in") (ecl "grn") (iyr "2012") (eyr "2030")
+ (byr "1980") (hcl "#623a2f"))
+ ((eyr "2029") (ecl "blu") (cid "129") (byr "1989") (iyr "2014")
+ (pid "896056539") (hcl "#a97842") (hgt "165cm"))
+ ((hcl "#888785") (hgt "164cm") (byr "2001") (iyr "2015") (cid "88")
+ (pid "545766238") (ecl "hzl") (eyr "2022"))
+ ((iyr "2010") (hgt "158cm") (hcl "#b6652a") (ecl "blu") (byr "1944")
+ (eyr "2021") (pid "093154719"))))
+
+(defvar *invalid-passports*
+ '(((eyr "1972") (cid "100") (hcl "#18171d") (ecl "amb") (hgt "170")
+ (pid "186cm") (iyr "2018") (byr "1926"))
+ ((iyr "2019") (hcl "#602927") (eyr "1967") (hgt "170cm") (ecl "grn")
+ (pid "012533040") (byr "1946"))
+ ((hcl "dab227") (iyr "2012") (ecl "brn") (hgt "182cm") (pid "021572410")
+ (eyr "2020") (byr "1992") (cid "277"))
+ ((hgt "59cm") (ecl "zzz") (eyr "2038") (hcl "74454a") (iyr "2023")
+ (pid "3556412378") (byr "2007"))))
+
+(test complex-validate-passports
+ (loop for passport in *valid-passports*
+ do (is-true (complex-validate-passport passport)))
+ (loop for passport in *invalid-passports*
+ do (is-false (complex-validate-passport passport))))
+
+(run! 'day04)