diff --git a/core/src/main/scala/tf/bug/aoc/Main.scala b/core/src/main/scala/tf/bug/aoc/Main.scala index 827527b..79338fa 100644 --- a/core/src/main/scala/tf/bug/aoc/Main.scala +++ b/core/src/main/scala/tf/bug/aoc/Main.scala @@ -20,7 +20,6 @@ object Main extends IOApp: part.flatMap { p => io.stdinUtf8[IO](1024) .through(text.lines) - .takeWhile(_.nonEmpty) .through(p) .through(io.stdoutLines(StandardCharsets.UTF_8)) .compile.drain diff --git a/core/src/main/scala/tf/bug/aoc/y2020/Day4.scala b/core/src/main/scala/tf/bug/aoc/y2020/Day4.scala new file mode 100644 index 0000000..a97f7a9 --- /dev/null +++ b/core/src/main/scala/tf/bug/aoc/y2020/Day4.scala @@ -0,0 +1,72 @@ +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) + } diff --git a/core/src/main/scala/tf/bug/aoc/y2020/y2020.scala b/core/src/main/scala/tf/bug/aoc/y2020/y2020.scala index 72315a9..d9d47fe 100644 --- a/core/src/main/scala/tf/bug/aoc/y2020/y2020.scala +++ b/core/src/main/scala/tf/bug/aoc/y2020/y2020.scala @@ -7,4 +7,5 @@ object y2020 extends Year: 1 -> Day1, 2 -> Day2, 3 -> Day3, + 4 -> Day4, )