DEV Community

Cover image for Advent of Code 2020 Solution Megathread - Day 4: Passport Processing
Ryan Palo
Ryan Palo

Posted on • Edited on

Advent of Code 2020 Solution Megathread - Day 4: Passport Processing

Day 4! I was worried as I was reading the first problem yesterday that I would have to optimize for any rational slope. I'm thankful he decided to take it easier than that for Day 3.

The Puzzle

In today’s puzzle, we're just trying to get through airport security, which is jam-packed because, obviously, Santa's magic keeps the North Pole COVID-free. It's classified as a hum-bug. 🥁 Anyways. The passport scanner is broken and we're doing a good deed by "fixing" it. And by "fixing," we mean "hacking it to let us through but also mostly fixing it."

Y'all. We are THIS close to validating YAML and I am here for it.

The Leaderboards

As always, this is the spot where I’ll plug any leaderboard codes shared from the community.

Ryan's Leaderboard: 224198-25048a19 
Enter fullscreen mode Exit fullscreen mode

If you want to generate your own leaderboard and signal boost it a little bit, send it to me either in a DEV message or in a comment on one of these posts and I'll add it to the list above.

Yesterday’s Languages

Updated 03:06PM 12/12/2020 PST.

Language Count
Python 8
JavaScript 3
Rust 2
Haskell 2
C# 1
Raku 1
COBOL 1
PHP 1
Ruby 1
Elixir 1
Go 1
C 1

Merry Coding!

Top comments (23)

Collapse
 
benwtrent profile image
Benjamin Trent

My super gross rust impl. Messy regex. I am sure there are way better ways to parse according to white space.

Also, I sort of went half-way with a struct solution. Probably would have been better to either go all the way or not have custom types at all.

#[derive(Debug)] struct Passport { birth_year: Option<usize>, issue_year: Option<usize>, exp_year: Option<usize>, height: Option<String>, hair_color: Option<String>, eye_color: Option<String>, pid: Option<String>, cid: Option<usize>, } impl From<&str> for Passport { fn from(s: &str) -> Self { let ( mut birth_year, mut issue_year, mut exp_year, mut height, mut hair_color, mut eye_color, mut pid, mut cid, ) = ( Option::None, Option::None, Option::None, Option::None, Option::None, Option::None, Option::None, Option::None, ); s.split(" ").filter(|i| !i.is_empty()).for_each(|i| { let name_var: Vec<&str> = i.split(":").collect(); match name_var[0] { "byr" => birth_year = Option::Some(name_var[1].parse().unwrap()), "iyr" => issue_year = Option::Some(name_var[1].parse().unwrap()), "eyr" => exp_year = Option::Some(name_var[1].parse().unwrap()), "hgt" => height = Option::Some(String::from(name_var[1])), "hcl" => hair_color = Option::Some(String::from(name_var[1])), "ecl" => eye_color = Option::Some(String::from(name_var[1])), "pid" => pid = Option::Some(String::from(name_var[1])), "cid" => cid = Option::Some(name_var[1].parse().unwrap()), _ => {} } }); Passport { birth_year, issue_year, exp_year, height, hair_color, eye_color, pid, cid, } } } impl Passport { pub fn is_valid(&self) -> bool { self.birth_year.is_some() && self.issue_year.is_some() && self.exp_year.is_some() && self.height.is_some() && self.hair_color.is_some() && self.eye_color.is_some() && self.pid.is_some() } pub fn is_valid_strict(&self) -> bool { self.valid_birth_year() && self.valid_issue_year() && self.valid_exp_year() && self.valid_hgt() && self.valid_hair() && self.valid_eyes() && self.valid_pid() } fn valid_birth_year(&self) -> bool { (1920..=2002).contains(&self.birth_year.unwrap_or_default()) } fn valid_issue_year(&self) -> bool { (2010..=2020).contains(&self.issue_year.unwrap_or_default()) } fn valid_exp_year(&self) -> bool { (2020..=2030).contains(&self.exp_year.unwrap_or_default()) } fn valid_hgt(&self) -> bool { if let Some(height) = self.height.as_ref() { let range = match &height[height.len() - 2..] { "in" => (59..=76), "cm" => (150..=193), _ => return false, }; range.contains(&height[0..height.len() - 2].parse::<usize>().unwrap_or(0)) } else { false } } fn valid_hair(&self) -> bool { Passport::valid_str(self.hair_color.as_ref(), r"^#[0-9a-f]{6}$") } fn valid_eyes(&self) -> bool { Passport::valid_str(self.eye_color.as_ref(), r"^amb|blu|brn|gry|grn|hzl|oth$") } fn valid_pid(&self) -> bool { Passport::valid_str(self.pid.as_ref(), r"^[0-9]{9}$") } fn valid_str(maybe_str: Option<&String>, re: &str) -> bool { if let Some(str) = maybe_str { let re = regex::Regex::new(re).unwrap(); let captures = re.captures(str.as_str()); captures.is_some() } else { false } } } #[aoc_generator(day4)] fn input_to_vec(input: &str) -> Vec<Passport> { let mut cleaned_str = String::from(""); let mut cleaned_input: Vec<Passport> = vec![]; input.lines().for_each(|i| { if i.is_empty() && !cleaned_str.is_empty() { cleaned_input.push(Passport::from(cleaned_str.as_str())); cleaned_str = String::from(""); } cleaned_str += i; cleaned_str += " "; }); if !cleaned_str.is_empty() { cleaned_input.push(Passport::from(cleaned_str.as_str())); } cleaned_input } #[aoc(day4, part1)] fn valid_count(input: &Vec<Passport>) -> usize { input.iter().filter(|p| p.is_valid()).count() } #[aoc(day4, part2)] fn strict_valid_count(input: &Vec<Passport>) -> usize { input.iter().filter(|p| p.is_valid_strict()).count() } 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ballpointcarrot profile image
Christopher Kruse

You're not alone in the ugly feeling. I had a particularly nasty bug that gave me one result too many in part two (realized I had a missing ^ and $ on the regex for pid).

I see a fair amount of similarities in approach, so I'm glad to see I'm in good company. :D

As always, on Github.

use aoc_runner_derive::{aoc, aoc_generator}; use regex::Regex; #[derive(Debug, PartialEq)] struct Height { measure: usize, unit: String, } impl Height { fn parse(hgt_str: &str) -> Option<Height> { let re = Regex::new("(\\d+)(in|cm)").expect("Unable to create Regex"); match re.captures(hgt_str) { None => None, Some(captures) => { let h = Height { measure: str::parse(captures.get(1).unwrap().as_str()) .expect("Unable to parse number"), unit: String::from(captures.get(2).unwrap().as_str()), }; Some(h) } } } fn is_valid(&self) -> bool { match self.unit.as_str() { "cm" => self.measure >= 150 && self.measure <= 193, "in" => self.measure >= 59 && self.measure <= 76, _ => panic!("Not a valid unit"), } } } #[derive(Debug, PartialEq)] struct Passport { byr: Option<usize>, iyr: Option<usize>, eyr: Option<usize>, hgt: Option<Height>, hgt_str: Option<String>, hcl: Option<String>, ecl: Option<String>, pid: Option<String>, cid: Option<String>, } impl Passport { fn new() -> Passport { Passport { byr: None, iyr: None, eyr: None, hgt: None, hgt_str: None, hcl: None, ecl: None, pid: None, cid: None, } } fn has_fields(&self) -> bool { self.byr.is_some() && self.iyr.is_some() && self.eyr.is_some() && self.hgt_str.is_some() && self.hcl.is_some() && self.ecl.is_some() && self.pid.is_some() } fn is_valid(&self) -> bool { self.valid_byr() && self.valid_iyr() && self.valid_eyr() && self.valid_hgt() && self.valid_hcl() && self.valid_ecl() && self.valid_pid() } fn valid_byr(&self) -> bool { match self.byr { None => false, Some(n) => n >= 1920 && n <= 2002, } } fn valid_iyr(&self) -> bool { match self.iyr { None => false, Some(n) => n >= 2010 && n <= 2020, } } fn valid_eyr(&self) -> bool { match self.eyr { None => false, Some(n) => n >= 2020 && n <= 2030, } } fn valid_hgt(&self) -> bool { match &self.hgt { None => false, Some(h) => h.is_valid(), } } fn valid_hcl(&self) -> bool { let re = Regex::new("^#[0-9a-f]{6}$").expect("Failed to make regex"); match &self.hcl { None => false, Some(hair) => re.is_match(hair.as_str()), } } fn valid_ecl(&self) -> bool { let valid_colors = vec!["amb", "blu", "brn", "gry", "grn", "hzl", "oth"]; match &self.ecl { None => false, Some(c) => valid_colors.contains(&c.as_str()), } } fn valid_pid(&self) -> bool { let re = Regex::new("^[0-9]{9}$").expect("Failed to build Regex"); match &self.pid { None => false, Some(pid) => re.is_match(pid.as_str()), } } } #[aoc_generator(day4)] fn parse_input_day4(input: &str) -> Vec<Passport> { input .split("\n\n") .map(|passport_str| parse_passport(passport_str)) .collect() } fn parse_passport(passport_str: &str) -> Passport { let kv: Vec<&str> = passport_str .lines() .flat_map(|line| line.split(" ")) .collect(); let mut pass = Passport::new(); for key_val in kv { let pair: Vec<&str> = key_val.split(":").collect(); match *(pair.get(0).unwrap()) { "cid" => pass.cid = Some(String::from(*pair.get(1).unwrap())), "byr" => pass.byr = Some(str::parse(*pair.get(1).unwrap()).unwrap()), "iyr" => pass.iyr = Some(str::parse(*pair.get(1).unwrap()).unwrap()), "eyr" => pass.eyr = Some(str::parse(*pair.get(1).unwrap()).unwrap()), "hgt" => { pass.hgt_str = Some(str::parse(*pair.get(1).unwrap()).unwrap()); pass.hgt = Height::parse(*pair.get(1).unwrap()); } "hcl" => pass.hcl = Some(String::from(*pair.get(1).unwrap())), "ecl" => pass.ecl = Some(String::from(*pair.get(1).unwrap())), "pid" => pass.pid = Some(String::from(*pair.get(1).unwrap())), _ => panic!("Found passport code that doesn't match"), } } pass } #[aoc(day4, part1)] fn count_valid_passports(input: &Vec<Passport>) -> usize { input.iter().filter(|pass| pass.has_fields()).count() } #[aoc(day4, part2)] fn count_valid_data_passports(input: &Vec<Passport>) -> usize { input.iter().filter(|pass| pass.is_valid()).count() } 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
aspittel profile image
Ali Spittel

I hate problems like this:

class Validator: def __init__(self, passport): self.passport = passport def check_field_count(self): return len(self.passport) == 8 or (len(self.passport) == 7 and 'cid' not in self.passport) def check_year(self, key, start, end): return len(self.passport[key]) == 4 and int(self.passport[key]) >= start and int(self.passport[key]) <= end def check_byr(self): return self.check_year('byr', 1920, 2002) def check_iyr(self): return self.check_year('iyr', 2010, 2020) def check_eyr(self): return self.check_year('eyr', 2020, 2030) def check_hcl(self): return self.passport['hcl'][0] == "#" and self.passport['hcl'][1:].isalnum() def check_ecl(self): return self.passport['ecl'] in ['amb', 'blu', 'brn', 'gry', 'grn', 'hzl', 'oth'] def check_pid(self): return len(self.passport['pid']) == 9 def check_hgt(self): if self.passport['hgt'][-2:] == "cm": return int(self.passport['hgt'][:-2]) >= 150 and int(self.passport['hgt'][:-2]) <= 193 elif self.passport['hgt'][-2:] == "in": return int(self.passport['hgt'][:-2]) >= 59 and int(self.passport['hgt'][:-2]) <= 76 def is_valid(self): return (self.check_field_count() and self.check_byr() and self.check_iyr() and self.check_eyr() and self.check_hcl() and self.check_ecl() and self.check_pid() and self.check_hgt()) def get_passports(inp): passports = [] passport = {} for line in inp: if line != "\n": line = line.rstrip().split(" ") line = [field.split(":") for field in line] for field in line: passport[field[0]] = field[1] else: passports.append(passport) passport = {} passports.append(passport) return passports with open('input.txt') as inp: passports = get_passports(inp) validators = [Validator(passport) for passport in passports] part_1_count = 0 part_2_count = 0 for validator in validators: if validator.check_field_count(): part_1_count += 1 if validator.is_valid(): part_2_count += 1 print(part_1_count) print(part_2_count) 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
grantfinneman profile image
Grant Finneman

Thank you so much for the clear and concise python, I'm new to python and thought this would be an excellent way to learn it. I figured this one out eventually and thought I would come here to see how someone else did it. I don't understand how yours doesn't throw a key error when trying to access dictionary keys that aren't there. I need to learn more about classes but I don't know why this works. If you could point me in the right direction I'd greatly appreciate it. Thank you again for the wonderful example

Collapse
 
meseta profile image
Yuan Gao

I went ahead and wrote a solution using PEG in Python (parsimonious library). Aside from the slightly compact PEG grammar, the rest of it is very readable

 from parsimonious.grammar import Grammar, NodeVisitor from parsimonious.exceptions import ParseError, IncompleteParseError, VisitationError grammar = Grammar(r""" EXPR = ITEM+ ITEM = (BYR / IYR / EYR / HGT / HCL / ECL / PID / CID) WHITE? WHITE = ~r"[\s]+" BYR = "byr:" NUMB IYR = "iyr:" NUMB EYR = "eyr:" NUMB HGT = "hgt:" (HGTCM / HGTIN) HCL = "hcl:" ~r"#[0-9a-f]{6}" ECL = "ecl:" ("amb" / "blu" / "brn" / "gry" / "grn" / "hzl" / "oth") PID = "pid:" ~r"[0-9]{9}" CID = "cid:" ~r"[0-9a-zA-Z]*" HGTCM = NUMB "cm" HGTIN = NUMB "in" NUMB = ~r"[0-9]{2,4}" """) class PassportVisitor(NodeVisitor): def visit_EXPR(self, node, visited_children): assert not {"BYR", "IYR", "EYR", "HGT", "HCL", "ECL", "PID"}.difference(visited_children) def visit_ITEM(self, node, visited_children): return node.children[0].children[0].expr_name def visit_BYR(self, node, visited_children): assert 1920 <= visited_children[1] <= 2002 def visit_IYR(self, node, visited_children): assert 2010 <= visited_children[1] <= 2020 def visit_EYR(self, node, visited_children): assert 2020 <= visited_children[1] <= 2030 def visit_HGTCM(self, node, visited_children): assert 150 <= visited_children[0] <= 193 def visit_HGTIN(self, node, visited_children): assert 59 <= visited_children[0] <= 76 def visit_NUMB(self, node, visited_children): return int(node.text) def generic_visit(self, node, visited_children): return visited_children or node pv = PassportVisitor() pv.grammar = grammar data = open("input.txt").read().split("\n\n") valid = 0 for entry in data: try: pv.parse(entry) except (ParseError, VisitationError, IncompleteParseError): continue else: valid += 1 print("Valid:", valid) 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
galoisgirl profile image
Anna

Not my finest work, I took advantage of the fact that some problems didn't arise in the test data. Then again, this isn't real life, it's a game and I got the stars. 😉

Part 2 here.

 IDENTIFICATION DIVISION. PROGRAM-ID. AOC-2020-04-1. AUTHOR. ANNA KOSIERADZKA. ENVIRONMENT DIVISION. INPUT-OUTPUT SECTION. FILE-CONTROL. SELECT INPUTFILE ASSIGN TO "d4.input" ORGANIZATION IS LINE SEQUENTIAL. DATA DIVISION. FILE SECTION. FD INPUTFILE RECORD IS VARYING IN SIZE FROM 1 to 99 DEPENDING ON REC-LEN. 01 INPUTRECORD PIC X(99). WORKING-STORAGE SECTION. 01 FILE-STATUS PIC 9 VALUE 0. 01 REC-LEN PIC 9(2) COMP. 01 WS-ROW PIC X(16) OCCURS 8 TIMES. 01 WS-CHAR PIC X. LOCAL-STORAGE SECTION. 01 CORRECT-PASSPORTS UNSIGNED-INT VALUE 0. 01 FOUND-FIELDS UNSIGNED-INT VALUE 0. 01 STRING-PTR UNSIGNED-INT VALUE 1. 01 I UNSIGNED-INT VALUE 1. PROCEDURE DIVISION. 001-MAIN. OPEN INPUT INPUTFILE. PERFORM 002-READ UNTIL FILE-STATUS = 1. CLOSE INPUTFILE. PERFORM 004-NEXT-PASSPORT. DISPLAY CORRECT-PASSPORTS. STOP RUN. 002-READ. READ INPUTFILE AT END MOVE 1 TO FILE-STATUS NOT AT END PERFORM 003-PROCESS-RECORD END-READ. 003-PROCESS-RECORD. IF REC-LEN = 0 THEN PERFORM 004-NEXT-PASSPORT ELSE PERFORM 005-PROCESS-ROW END-IF. 004-NEXT-PASSPORT. IF FOUND-FIELDS = 7 THEN ADD 1 TO CORRECT-PASSPORTS END-IF. MOVE 0 TO FOUND-FIELDS. 005-PROCESS-ROW. MOVE 1 TO STRING-PTR. PERFORM VARYING I FROM 1 BY 1 UNTIL I > 8 UNSTRING INPUTRECORD DELIMITED BY SPACE INTO WS-ROW(I) WITH POINTER STRING-PTR END-PERFORM. PERFORM VARYING I FROM 1 BY 1 UNTIL I > 8 MOVE WS-ROW(I)(1:1) TO WS-CHAR IF NOT WS-CHAR ='c' AND NOT WS-CHAR = ' ' THEN ADD 1 TO FOUND-FIELDS END-IF END-PERFORM. 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
particleflux profile image
Stefan Linke

Warning this might be offensive to some :D
My Saturday was too long, so I created a code-golfed PHP version (for both parts)

<?$r=['byr):((19[2-9]\d|200[0-2]','iyr):((201\d|2020','eyr):((202\d|2030','hgt):(((?:1[5-8]\d|19[0-3])cm|(?:59|6\d|7[0-6])in','hcl):((#[\da-f]{6}','ecl):((amb|blu|brn|gry|grn|hzl|oth','pid):((\d{9}'];foreach(explode(" ",file_get_contents('input'))as$p){$s=$t=1;for($i=0;$i<7;)if(!preg_match("/(?:^|\s)({$r[$i++]})(\s|$))?/",$p,$m))$s=$t=0;elseif(!($m[2]??0))$t=0;$x+=$s;$y+=$t;}echo"$x $y"; 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
particleflux profile image
Stefan Linke

Go again, a bit hacky with the very last element

package main import ( "bufio" "fmt" "io" "os" "regexp" "strconv" "strings" ) var requiredAttributes = []string{"byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid"} func isValid(passport map[string]string) bool { for _, attribute := range requiredAttributes { if _, exists := passport[attribute]; !exists { return false } } return true } func exists(slice []string, val string) bool { for _, item := range slice { if item == val { return true } } return false } func validateRange(val string, lower, upper int) bool { num, err := strconv.Atoi(val) return err == nil && num >= lower && num <= upper } func isValidV2(passport map[string]string) bool { for attr, val := range passport { switch attr { case "byr": if !validateRange(val, 1920, 2002) { return false } case "iyr": if !validateRange(val, 2010, 2020) { return false } case "eyr": if !validateRange(val, 2020, 2030) { return false } case "hgt": if strings.HasSuffix(val, "cm") { if !validateRange(strings.TrimSuffix(val, "cm"), 150, 193) { return false } } else if strings.HasSuffix(val, "in") { if !validateRange(strings.TrimSuffix(val, "in"), 59, 76) { return false } } else { return false } case "hcl": if match, _ := regexp.MatchString("^#[0-9a-f]{6}$", val); !match { return false } case "ecl": eyeColors := []string{"amb", "blu", "brn", "gry", "grn", "hzl", "oth"} if !exists(eyeColors, val) { return false } case "pid": if match, _ := regexp.MatchString("^[0-9]{9}$", val); !match { return false } } } return true } func main() { reader := bufio.NewReader(os.Stdin) passport := make(map[string]string, 8) numValid, numValidV2 := 0, 0 for { var line string line, err := reader.ReadString('\n') if err == io.EOF { break } line = strings.TrimSpace(line) if len(line) == 0 { // passport complete if isValid(passport) { numValid++ if isValidV2(passport) { numValidV2++ } } // reset passport passport = make(map[string]string, 8) } else { parts := strings.Split(line, " ") for _, part := range parts { attribute := strings.Split(part, ":") passport[attribute[0]] = attribute[1] } } } // last one is special snowflake, as file does not end with 2 newlines if isValid(passport) { numValid++ if isValidV2(passport) { numValidV2++ } } fmt.Println(numValid, numValidV2) } 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
willsmart profile image
willsmart • Edited

Plain forgot to break out of the comfort zone and use something other than javascript :|
I'll try C tomorrow

Here's today:

const input = require('fs') .readFileSync('4.txt', 'utf-8') .split(/\n{2,}/g) .map(s => Object.fromEntries(s.split(/\s+/g).map(s => s.split(':')))); console.log({ input: input.map(v => ({ v: JSON.stringify(v), isValid: isValid(v), check: check(v) })), count: input.reduce((acc, v) => acc + isValid(v), 0), }); function check(v) { return ['byr', 'iyr', 'eyr', 'hgt', 'hcl', 'ecl', 'pid'] .flatMap(k => (k in v && !validPair(k, v[k]) ? k : [])) .join(' '); } function isValid(v) { return ['byr', 'iyr', 'eyr', 'hgt', 'hcl', 'ecl', 'pid'].reduce( (acc, k) => acc && k in v && validPair(k, v[k]), true ); } function validPair(k, v) { let m; switch (k) { case 'byr': return v.length === 4 && v >= 1920 && v <= 2002; case 'iyr': return v.length === 4 && v >= 2010 && v <= 2020; case 'eyr': return v.length === 4 && v >= 2020 && v <= 2030; case 'hgt': if ((m = /^([\d.]+)cm$/.exec(v))) return m[1] >= 150 && m[1] <= 193; if ((m = /^([\d.]+)in$/.exec(v))) return m[1] >= 59 && m[1] <= 76; return false; case 'hcl': return /^#[0-9a-f]{6}$/.test(v); case 'ecl': return ['amb', 'blu', 'brn', 'gry', 'grn', 'hzl', 'oth'].includes(v); case 'pid': return /^[0-9]{9}$/.test(v); } } 
Enter fullscreen mode Exit fullscreen mode

(added check as a way to see which field validator was broken when I failed part 2 the first time. It was a stupid typo in 'ecl')

Collapse
 
sleeplessbyte profile image
Derk-Jan Karrenbeld • Edited

I feel this reads as quite elegant in Ruby -- or at least my implementation of it.

Hoping on something more interesting to do for Day 5!

require 'benchmark' class Passport RULES = { byr: -> (value) { /^[0-9]{4}$/.match?(value) && (1920..2002).cover?(value.to_i) }, iyr: -> (value) { /^[0-9]{4}$/.match?(value) && (2010..2020).cover?(value.to_i) }, eyr: -> (value) { /^[0-9]{4}$/.match?(value) && (2020..2030).cover?(value.to_i) }, hgt: -> (value) { match = /(^[1-9][0-9]+)(cm|in)$/.match(value) match && ( (match[2] == 'cm' && (150..193).cover?(match[1].to_i)) || (match[2] == 'in' && (59..76).cover?(match[1].to_i)) ) }, hcl: -> (value) { /^\#[0-9a-f]{6}$/.match? value }, ecl: -> (value) { %w[amb blu brn gry grn hzl oth].include?(value) }, pid: -> (value) { /^[0-9]{9}$/.match? value } } def self.from(lines) fields = lines.split(' ') new(fields.each_with_object({}) do |kv, results| k, v = kv.chomp.split(':') results[k.to_sym] = v end) end def initialize(fields) self.fields = fields end def [](field) fields[field] end def valid? RULES.keys.all? { |field| RULES[field].(self[field]) } end private attr_accessor :fields end data = File.read('input.txt') passports = data .split(/\n\n/) .map do |l| Passport.from(l) end Benchmark.bmbm do |b| b.report do puts passports.count(&:valid?) end end 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
erikbackman profile image
Erik Bäckman • Edited

Still not doing these the day the come out but here's my Haskell solution.
Problems like these seem like work to me :P

{-# LANGUAGE LambdaCase #-} module Day4 where import Text.Parsec import Text.Parsec.String import Control.Monad (guard) import Data.Maybe (mapMaybe, isJust) import Data.List.Split hiding (sepBy, endBy, oneOf) import qualified Data.Map as M import Data.Map (Map) import Data.List ((\\)) import Text.Read (readMaybe) import Data.Char import Data.Semigroup (All(All), getAll) hush :: Either e a -> Maybe a hush (Left _) = Nothing hush (Right a) = Just a parse' :: Parser a -> String -> Maybe a parse' p = hush . parse p "" -- Part 1 parsePassport :: String -> Either ParseError (Map String String) parsePassport = fmap M.fromList . parse' (kvp `sepEndBy` space) where kvp = (,) <$> manyTill anyChar (char ':') <*> many (noneOf "\n ") hasRequiredFields :: Map String String -> Bool hasRequiredFields m = null $ ["byr", "ecl", "eyr", "hcl", "hgt", "iyr", "pid"] \\ M.keys m validatePassports :: (Map String String -> Bool) -> String -> [Map String String] validatePassports v = filter v . mapMaybe (hush . parsePassport) . splitOn "\n\n" solveP1 :: String -> Int solveP1 = length . validatePassports hasRequiredFields -- Part 2 type Validation a = (a -> Maybe a) within :: (Int, Int) -> Int -> Bool within (min, max) i = i >= min && i <= max year :: String -> Maybe Int year s = parse' (4 `times` digit) s >>= readMaybe yearBetween :: (Int, Int) -> String -> Maybe String yearBetween r s = year s >>= \n -> guard (within r n) >> pure s byr :: Validation String byr = yearBetween (1929, 2020) iyr :: Validation String iyr = yearBetween (2010, 2020) eyr :: Validation String eyr = yearBetween (2020, 2030) hgt :: Validation String hgt = parse' (mappend <$> many1 digit <*> (try (string "cm") <|> try (string "in"))) hcl :: Validation String hcl = parse' (mappend <$> string "#" <*> 6 `times` anyChar) ecl :: Validation String ecl s | s == "amb" = Just s | s == "blu" = Just s | s == "brn" = Just s | s == "gry" = Just s | s == "grn" = Just s | s == "hzl" = Just s | s == "oth" = Just s | otherwise = Nothing pid :: Validation String pid s = guard (length s == 9 && isNumber s) >> pure s where isNumber = getAll . foldMap (All . isDigit) validateFields :: Map String String -> Maybe (Map String String) validateFields = M.traverseWithKey validateAtKey where validateAtKey = \case "byr" -> byr "iyr" -> iyr "eyr" -> eyr "hgt" -> hgt "hcl" -> hcl "ecl" -> ecl _ -> Just solveP2 :: String -> Int solveP2 = length . validatePassports (\x -> hasRequiredFields x && isJust (validateFields x)) 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mustafahaddara profile image
Mustafa Haddara

Reading all of these solutions, I'm glad I opted to avoid regexes in my answers! There are some messy hacks here ("1234567890abcdef".indexOf(c) stands out) and the types should probably all have been string? instead of string, which would have made this a little more concise, but I'm still pretty happy with how it turned out.

I am annoyed they decided to put multiple K:V pairs on the same line because it made the parsing a little messier but oh well.

import { SolveFunc } from './types'; export const solve: SolveFunc = (lines: string[]) => { const passports: Passport[] = parse_passports(lines); return passports.filter((p) => is_valid(p)).length.toString(); }; type Passport = { byr: string; iyr: string; eyr: string; hgt: string; hcl: string; ecl: string; pid: string; cid: string; }; const parse_passports = (lines: string[]): Passport[] => { let p: Passport = make_blank_passport(); const result = [p]; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.trim() === '') { p = make_blank_passport(); result.push(p); } line.split(' ').map((chunk) => { const [k, v] = chunk.split(':'); p[k] = v; }); } return result; }; const make_blank_passport = (): Passport => { return { byr: null, iyr: null, eyr: null, hgt: null, hcl: null, ecl: null, pid: null, cid: null, }; }; const is_valid = (p: Passport): boolean => { return ( check_int(p.byr, 1920, 2002) && check_int(p.iyr, 2010, 2020) && check_int(p.eyr, 2020, 2030) && check_height(p.hgt) && check_hair_color(p.hcl) && check_eye_color(p.ecl) && check_passport_id(p.pid) ); }; const check_int = (strval, min, max) => { if (!strval) return false; const val = parseInt(strval); return min <= val && val <= max; }; const check_height = (strval) => { if (!strval) return false; if (strval.endsWith('cm')) { const [h] = strval.split('cm'); return check_int(h, 150, 193); } else if (strval.endsWith('in')) { const [h] = strval.split('in'); return check_int(h, 59, 76); } return false; }; const check_hair_color = (strval) => { if (!strval) return false; if (strval.startsWith('#') && strval.length === 7) { return strval.split('').filter((c) => '1234567890abcdef'.indexOf(c) < 0).length === 1; } return false; }; const check_eye_color = (strval) => { if (!strval) return false; const accepted = ['amb', 'blu', 'brn', 'gry', 'grn', 'hzl', 'oth']; return accepted.indexOf(strval) >= 0; }; const check_passport_id = (strval) => { if (!strval) return false; if (strval.length !== 9) return false; strval.split('').map((c) => parseInt(c)); return true; }; 
Enter fullscreen mode Exit fullscreen mode