$required = set(["ecl", "pid", "eyr", "hcl", "byr", "iyr", "hgt"]);
$passports = $input->split("\n\n")->map(function ($passport) use ($required) {
preg_match_all("/([a-z]{3}):([^\n ]+)/", $passport, $matches);
return $required->remove($matches[1])->count()
? false
: array_combine($matches[1], $matches[2]);
})->filter();
// ==================================================
// > PART 1
// ==================================================
$solution_1 = $passports->count();
// ==================================================
// > PART 2
// ==================================================
$solution_2 = $passports->filter(function ($passport) {
// byr (Birth Year) - four digits; at least 1920 and at most 2002.
if (!in_array((int) $passport["byr"], range(1920, 2002))) return false;
// iyr (Issue Year) - four digits; at least 2010 and at most 2020.
if (!in_array((int) $passport["iyr"], range(2010, 2020))) return false;
// eyr (Expiration Year) - four digits; at least 2020 and at most 2030.
if (!in_array((int) $passport["eyr"], range(2020, 2030))) return false;
// hgt (Height) - a number followed by either cm or in:
if (!preg_match("/^(\d+)(cm|in)$/", $passport["hgt"], $matches)) return false;
// If cm, the number must be at least 150 and at most 193.
if ($matches[2] == "cm" && !in_array((int) $matches[1], range(150, 193))) return false;
// If in, the number must be at least 59 and at most 76.
if ($matches[2] == "in" && !in_array((int) $matches[1], range(59, 76))) return false;
// hcl (Hair Color) - a # followed by exactly six characters 0-9 or a-f.
if (!preg_match("/^#[0-9a-f]{6}$/", $passport["hcl"])) return false;
// ecl (Eye Color) - exactly one of: amb blu brn gry grn hzl oth.
if (!in_array($passport["ecl"], ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"])) return false;
// pid (Passport ID) - a nine-digit number, including leading zeroes.
if (!preg_match("/^\d{9}$/", $passport["pid"])) return false;
// cid (Country ID) - ignored, missing or not.
return true;
})->count();