Add day 04
[adventofcode2020.git] / src / day04.lisp
diff --git a/src/day04.lisp b/src/day04.lisp
new file mode 100644 (file)
index 0000000..395dea3
--- /dev/null
@@ -0,0 +1,129 @@
+(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)