type field = string * string let fields0 str = let drop k str = String.(sub str k (length str - k)) in let get str = if str = "" then None else Scanf.sscanf str "%3s : %s %n" (fun k v len -> Some ((k, v), drop len str)) in Seq.unfold get str let%test_module _ = (module struct let fields' str = List.of_seq (fields0 str) let%test "ε" = fields' "" = [] let%test "xxx:b" = fields' "xxx:b" = [("xxx", "b")] let%test "abc:x def:y" = fields' "abc:x def:y" = [("abc", "x"); ("def", "y")] end) module StrSet = Set.Make(String) let optional_fields = StrSet.singleton "cid" let needed_fields = StrSet.of_list ["byr"; "iyr"; "eyr"; "hgt"; "hcl"; "ecl"; "pid"] let all_fields = StrSet.add "cid" needed_fields let field_keys str = fields0 str |> Seq.map fst |> StrSet.of_seq let valid_set set = StrSet.subset needed_fields set && StrSet.subset set all_fields let%test_module _ = (module struct let valid_list lst = List.to_seq lst |> StrSet.of_seq |> valid_set let%test "ex1" = valid_list ["ecl"; "pid"; "eyr"; "hcl"; "byr"; "iyr"; "cid"; "hgt"] let%test "ex2" = not @@ valid_list ["iyr"; "ecl"; "cid"; "eyr"; "pid"; "hcl"; "byr"] let%test "ex3" = valid_list ["hcl"; "iyr"; "eyr"; "ecl"; "pid"; "byr"; "hgt"] let%test "ex4" = not @@ valid_list ["hcl"; "eyr"; "pid"; "iyr"; "ecl"; "hgt"] end) let main' make valid file = let items = Bracket.infile_chunks file ~chunk:make ~of_seq:List.of_seq in let len_all = List.length items in let len_valid = List.(length (filter valid items)) in Printf.printf "%d valid out of %d\n" len_valid len_all let main1 = main' field_keys valid_set let%expect_test "part 1" = main1 "../../data/day4"; [%expect{| 233 valid out of 288 |}] module StrMap = Map.Make(String) module Validators = struct let (=~?) str rx = Re.Pcre.(pmatch ~rex:(regexp rx) str) let (=~) str rx = try Some (Re.Pcre.(extract ~rex:(regexp rx) str)) with Not_found -> None let year_range lo hi x = x =~? {|^\d{4}$|} && let x = int_of_string x in x >= lo && x <= hi let%test _ = year_range 1900 2000 "1950" let%test _ = year_range 1900 2000 "1900" let%test _ = year_range 1900 2000 "2000" let%test _ = not (year_range 1900 2000 "0950") let%test _ = not (year_range 1900 2000 "01950") let%test _ = not (year_range 1900 2000 "") let%test _ = not (year_range 1900 2000 "lmao") let%test _ = not (year_range 1900 2000 "1950aaaa") let height str = match str =~ {|^(\d+)(cm|in)$|} with | Some res -> let num = int_of_string res.(1) in if res.(2) = "cm" then num >= 150 && num <= 193 else num >= 59 && num <= 76 | None -> false let%test _ = height "150cm" let%test _ = height "175cm" let%test _ = height "193cm" let%test _ = not (height "200cm") (* hmmmmmmmmmm *) let%test _ = not (height "50cm") let%test _ = not (height "1500mm") let%test _ = height "59in" let%test _ = height "69in" let%test _ = height "76in" let%test _ = not (height "6in") let%test _ = not (height "600in") let%test _ = not (height "5ft") let%test _ = not (height "") let hex_col str = str =~? {|^#[0-9a-f]{6}$|} let%test _ = hex_col "#000000" let%test _ = not (hex_col "a#000000") let%test _ = not (hex_col "#000000a") let%test _ = hex_col "#abcabc" let%test _ = not (hex_col "#ABCABC") (* according to the spec anyway *) let eye_col str = str =~? {|^(amb|blu|brn|gry|grn|hzl|oth)$|} let%test _ = eye_col "blu" let%test _ = not (eye_col "blue") let%test _ = not (eye_col "") let pid str = str =~? {|^[0-9]{9}$|} let%test _ = pid "012345678" let%test _ = pid "123456789" let%test _ = not (pid "12345678") let%test _ = not (pid "1234567890") let map = StrMap.of_seq @@ List.to_seq ["byr", year_range 1920 2002; "iyr", year_range 2010 2020; "eyr", year_range 2020 2030; "hgt", height; "hcl", hex_col; "ecl", eye_col; "pid", pid; "cid", fun _ -> true] let validate k v = StrMap.find_opt k map |> Option.fold ~none:false ~some:(fun f -> f v) end let validate = Validators.validate let fields str = fields0 str |> StrMap.of_seq let valid_keys map = StrMap.to_seq map |> Seq.map fst |> StrSet.of_seq |> valid_set let valid_map map = valid_keys map && StrMap.for_all validate map let%test_module _ = (module struct let valid_list lst = List.to_seq lst |> StrMap.of_seq |> valid_map let%test "ex1" = not @@ valid_list [("eyr", "1972"); ("cid", "100"); ("hcl", "#18171d"); ("ecl", "amb"); ("hgt", "170"); ("pid", "186cm"); ("iyr", "2018"); ("byr", "1926")] let%test "ex2" = not @@ valid_list [("iyr", "2019"); ("hcl", "#602927"); ("eyr", "1967"); ("hgt", "170cm"); ("ecl", "grn"); ("pid", "012533040"); ("byr", "1946")] let%test "ex3" = not @@ valid_list [("hcl", "dab227"); ("iyr", "2012"); ("ecl", "brn"); ("hgt", "182cm"); ("pid", "021572410"); ("eyr", "2020"); ("byr", "1992"); ("cid", "277")] let%test "ex4" = not @@ valid_list [("hgt", "59cm"); ("ecl", "zzz"); ("eyr", "2038"); ("hcl", "74454a"); ("iyr", "2023"); ("pid", "3556412378"); ("byr", "2007")] let%test "ex5" = valid_list [("pid", "087499704"); ("hgt", "74in"); ("ecl", "grn"); ("iyr", "2012"); ("eyr", "2030"); ("byr", "1980"); ("hcl", "#623a2f")] let%test "ex6" = valid_list [("eyr", "2029"); ("ecl", "blu"); ("cid", "129"); ("byr", "1989"); ("iyr", "2014"); ("pid", "896056539"); ("hcl", "#a97842"); ("hgt", "165cm")] let%test "ex7" = valid_list [("hcl", "#888785"); ("hgt", "164cm"); ("byr", "2001"); ("iyr", "2015"); ("cid", "88"); ("pid", "545766238"); ("ecl", "hzl"); ("eyr", "2022")] let%test "ex8" = valid_list [("iyr", "2010"); ("hgt", "158cm"); ("hcl", "#b6652a"); ("ecl", "blu"); ("byr", "1944"); ("eyr", "2021"); ("pid", "093154719")] end) let main2 = main' fields valid_map let%expect_test _ = main2 "../../data/day4"; [%expect{| 111 valid out of 288 |}] let main = Misc.main 4 [|main1; main2|]