package tf.bug.aoc.y2020 import cats.effect._ import cats.parse.{Parser => P, Parser1, Numbers} import cats.syntax.all._ import fs2._ import tf.bug.aoc.Day object Day4 extends Day { val whitespace: Parser1[Unit] = P.charIn(" ").void val parseEntry: Parser1[(String, String)] = (P.length1(3), P.char(':'), P.until1(whitespace)).mapN { case (key, (), value) => key -> value } val parseMap: P[Map[String, String]] = (P.repSep(parseEntry, min = 0, sep = whitespace) <* P.until(P.end)).map(_.toMap) override def part1[F[_]: Async]: Pipe[F, String, String] = { val requiredKeys = Set( "byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid" ) parts { m => val keySet = m.keySet requiredKeys.forall(keySet.contains) } } override def part2[F[_]: Async]: Pipe[F, String, String] = { val hexChar: P[Unit] = P.charIn(('0' to '9') ++ ('a' to 'f')).void val validateColor: P[Unit] = P.char('#') *> hexChar.replicateA(6).void val eyeColors: Set[String] = Set( "amb", "blu", "brn", "gry", "grn", "hzl", "oth", ) val pidChar: P[Unit] = Numbers.digit.void val validatePid: P[Unit] = pidChar.replicateA(9).void parts { m => lazy val validByr = m.get("byr").flatMap(_.toIntOption).fold(false)(x => 1920 <= x && x <= 2002) lazy val validIyr = m.get("iyr").flatMap(_.toIntOption).fold(false)(x => 2010 <= x && x <= 2020) lazy val validEyr = m.get("eyr").flatMap(_.toIntOption).fold(false)(x => 2020 <= x && x <= 2030) lazy val validHgt = m.get("hgt").fold(false) { case s"${hgtCm}cm" => hgtCm.toIntOption.fold(false)(x => 150 <= x && x <= 193) case s"${hgtIn}in" => hgtIn.toIntOption.fold(false)(x => 59 <= x && x <= 76) case _ => false } lazy val validHcl = m.get("hcl").fold(false)(validateColor.parseAll(_).isRight) lazy val validEcl = m.get("ecl").fold(false)(eyeColors.contains) lazy val validPid = m.get("pid").fold(false)(validatePid.parseAll(_).isRight) validByr && validIyr && validEyr && validHgt && validHcl && validEcl && validPid } } def parts[F[_]: Async](validator: Map[String, String] => Boolean): Pipe[F, String, String] = (lines: Stream[F, String]) => { val passportStrings: Stream[F, String] = lines.groupAdjacentBy(_.nonEmpty).collect { case (true, strings) => strings.mkString_(" ") } val passportMaps: Stream[F, Map[String, String]] = passportStrings.map(parseMap.parseAll).collect { case Right(map) => map } passportMaps .map(validator) .foldMap(if(_) 1 else 0) .map(_.show) } }