From 8df313b2048774207936286ca748e4056e4f0ba5 Mon Sep 17 00:00:00 2001 From: Juris Date: Mon, 23 Dec 2024 22:12:18 +0200 Subject: [PATCH] Refactoring --- scala3/src/main/resources/2020/16-test-00.txt | 12 +++ scala3/src/main/resources/2020/16-test-01.txt | 11 +++ .../scala/jurisk/adventofcode/AdventApp.scala | 2 +- .../jurisk/adventofcode/y2020/Advent16.scala | 99 +++++++++---------- .../jurisk/adventofcode/AdventAppSpec.scala | 1 + .../adventofcode/y2020/Advent16Spec.scala | 29 ++++++ 6 files changed, 100 insertions(+), 54 deletions(-) create mode 100644 scala3/src/main/resources/2020/16-test-00.txt create mode 100644 scala3/src/main/resources/2020/16-test-01.txt create mode 100644 scala3/src/test/scala/jurisk/adventofcode/y2020/Advent16Spec.scala diff --git a/scala3/src/main/resources/2020/16-test-00.txt b/scala3/src/main/resources/2020/16-test-00.txt new file mode 100644 index 00000000..fe34e403 --- /dev/null +++ b/scala3/src/main/resources/2020/16-test-00.txt @@ -0,0 +1,12 @@ +class: 1-3 or 5-7 +row: 6-11 or 33-44 +seat: 13-40 or 45-50 + +your ticket: +7,1,14 + +nearby tickets: +7,3,47 +40,4,50 +55,2,20 +38,6,12 diff --git a/scala3/src/main/resources/2020/16-test-01.txt b/scala3/src/main/resources/2020/16-test-01.txt new file mode 100644 index 00000000..b48bf373 --- /dev/null +++ b/scala3/src/main/resources/2020/16-test-01.txt @@ -0,0 +1,11 @@ +class: 0-1 or 4-19 +row: 0-5 or 8-19 +seat: 0-13 or 16-19 + +your ticket: +11,12,13 + +nearby tickets: +3,9,18 +15,1,5 +5,14,9 diff --git a/scala3/src/main/scala/jurisk/adventofcode/AdventApp.scala b/scala3/src/main/scala/jurisk/adventofcode/AdventApp.scala index a78a34e8..71de17b8 100644 --- a/scala3/src/main/scala/jurisk/adventofcode/AdventApp.scala +++ b/scala3/src/main/scala/jurisk/adventofcode/AdventApp.scala @@ -11,7 +11,7 @@ object AdventApp: object ErrorMessage: def apply(message: String): ErrorMessage = message -sealed private trait AdventApp[Input, Output1, Output2] extends IOApp: +private trait AdventApp[Input, Output1, Output2] extends IOApp: def year: Int def exercise: Int diff --git a/scala3/src/main/scala/jurisk/adventofcode/y2020/Advent16.scala b/scala3/src/main/scala/jurisk/adventofcode/y2020/Advent16.scala index 64538a56..d17b959e 100644 --- a/scala3/src/main/scala/jurisk/adventofcode/y2020/Advent16.scala +++ b/scala3/src/main/scala/jurisk/adventofcode/y2020/Advent16.scala @@ -1,30 +1,34 @@ package jurisk.adventofcode.y2020 -import cats.effect.{ExitCode, IO, IOApp} - +import jurisk.adventofcode.AdventApp +import jurisk.adventofcode.AdventApp.ErrorMessage +import jurisk.adventofcode.y2020.Advent16.Data +import cats.implicits.* import scala.annotation.tailrec -import scala.io.Source import scala.util.matching.Regex -object Advent16 extends IOApp: +object Advent16 extends AdventApp[Data, Int, Long]: + override val year: Value = 2020 + override val exercise: Value = 16 + opaque type Name = String opaque type Index = Int opaque type Value = Int - + final case class Field(name: Name, constraints: Set[Interval]): def isValid(n: Value): Boolean = constraints.exists(_.contains(n)) - + final case class Interval(from: Value, to: Value): def contains(n: Value): Boolean = n >= from && n <= to - + final case class Ticket(numbers: Vector[Value]): - def fieldValue(index: Index): Value = numbers(index) - - def invalidNumbers(fields: Set[Field]): Vector[Value] = + def fieldValue(index: Index): Value = numbers(index) + + def invalidNumbers(fields: Set[Field]): Vector[Value] = numbers.filterNot(x => fields.exists(_.isValid(x))) - + def isValid(fields: Set[Field]): Boolean = invalidNumbers(fields).isEmpty - + final case class Data( fields: Set[Field], yourTicket: Ticket, @@ -33,57 +37,57 @@ object Advent16 extends IOApp: private def validNearbyTickets: Vector[Ticket] = nearbyTickets.filter(_.isValid(fields)) def fieldIndices: Range = yourTicket.numbers.indices - + def fieldValues(index: Index): Vector[Value] = yourTicket.fieldValue(index) +: validNearbyTickets.map(_.fieldValue(index)) - + def parse(input: Vector[String]): Data = def parseField(input: String): Field = val FieldRE: Regex = """([\w\s]+): (\d+)-(\d+) or (\d+)-(\d+)""".r input match - case FieldRE(name, a, b, c, d) => + case FieldRE(name, a, b, c, d) => Field(name, Set(Interval(a.toInt, b.toInt), Interval(c.toInt, d.toInt))) - - def parseTicket(ticket: String): Ticket = + + def parseTicket(ticket: String): Ticket = Ticket(ticket.split(",").map(_.toInt).toVector) - + val firstEmpty = input.indexWhere(_.isEmpty) val secondEmpty = input.indexWhere(_.isEmpty, firstEmpty + 1) val fields = input.take(firstEmpty).toSet val yourTicket = input(firstEmpty + 2) val nearbyTickets = input.drop(secondEmpty + 2) - + Data( fields = fields map parseField, yourTicket = parseTicket(yourTicket), - nearbyTickets = nearbyTickets map parseTicket, + nearbyTickets = nearbyTickets map parseTicket, ) - - def solve1(data: Data): Int = + + def solve1(data: Data): Int = val invalidNumbers = for ticket <- data.nearbyTickets number <- ticket.invalidNumbers(data.fields) yield number - + invalidNumbers.sum - + def solve2(data: Data, prefix: String): Long = - val candidateIndices: List[(Name, List[Index])] = + val candidateIndices: List[(Name, List[Index])] = data .fields .toList .map { field => val suitableIndices = data .fieldIndices - .filter { index => + .filter { index => data.fieldValues(index) forall field.isValid } .toList - - field.name -> suitableIndices + + field.name -> suitableIndices } .sortBy { case (_, indices) => indices.length } - + @tailrec def assignIndices( candidateIndices: List[(Name, List[Index])], // sorted by length of indices @@ -91,23 +95,23 @@ object Advent16 extends IOApp: ): Seq[(Name, Index)] = candidateIndices match case Nil => acc - case x :: xs => + case x :: xs => val (name, numbers) = x numbers match case identified :: Nil => assignIndices( - // removing the `identified` number from elsewhere - xs.map { case (name, numbers) => name -> numbers.filterNot(_ == identified) }, + // removing the `identified` number from elsewhere + xs.map { case (name, numbers) => name -> numbers.filterNot(_ == identified) }, name -> identified :: acc, ) - - case _ => + + case _ => sys.error(s"Expected exactly 1 number but got $numbers") - + val fieldIndices: Map[Name, Index] = assignIndices(candidateIndices).toMap - + assert(fieldIndices.values.toSet.size == fieldIndices.size, "Indices should not repeat") - + data.fields .map(_.name) .filter(_.startsWith(prefix)) @@ -118,21 +122,10 @@ object Advent16 extends IOApp: .map(_.toLong) .product - def run(args: List[String]): IO[ExitCode] = for - testInput <- IO(Source.fromResource("2020/16-test.txt").getLines().toVector) - testData = parse(testInput) - realInput <- IO(Source.fromResource("2020/16.txt").getLines().toVector) - realData = parse(realInput) - - result1 = solve1(realData) - _ = assert(result1 == 21980) - _ <- IO(println(result1)) + override def parseInput(lines: Iterator[Name]): Either[ErrorMessage, Data] = { + parse(lines.toVector).asRight + } - _ = assert(solve2(testData, "class") == 12) - _ = assert(solve2(testData, "row") == 11) - _ = assert(solve2(testData, "seat") == 13) + override def solution1(input: Data): Value = solve1(input) - result2 = solve2(realData, "departure") - _ = assert(result2 == 1439429522627L) - _ <- IO(println(result2)) - yield ExitCode.Success + override def solution2(input: Data): Long = solve2(input, "departure") diff --git a/scala3/src/test/scala/jurisk/adventofcode/AdventAppSpec.scala b/scala3/src/test/scala/jurisk/adventofcode/AdventAppSpec.scala index 6b328454..327879d6 100644 --- a/scala3/src/test/scala/jurisk/adventofcode/AdventAppSpec.scala +++ b/scala3/src/test/scala/jurisk/adventofcode/AdventAppSpec.scala @@ -6,5 +6,6 @@ import org.scalatest.freespec.AsyncFreeSpec abstract class AdventAppSpec[Input, Output1, Output2](app: AdventApp[Input, Output1, Output2]) extends AsyncFreeSpec with AsyncIOSpec { protected def loadTestData(suffix: String): Input = app.parseTestData(suffix).unsafeRunSync().getOrElse(sys.error("failed")) protected lazy val testData00: Input = loadTestData("00") + protected lazy val testData01: Input = loadTestData("01") protected val realData: Input = app.parseRealData.unsafeRunSync().getOrElse(sys.error("failed")) } diff --git a/scala3/src/test/scala/jurisk/adventofcode/y2020/Advent16Spec.scala b/scala3/src/test/scala/jurisk/adventofcode/y2020/Advent16Spec.scala new file mode 100644 index 00000000..9d82c439 --- /dev/null +++ b/scala3/src/test/scala/jurisk/adventofcode/y2020/Advent16Spec.scala @@ -0,0 +1,29 @@ +package jurisk.adventofcode.y2020 + +import jurisk.adventofcode.AdventAppSpec +import jurisk.adventofcode.y2020.Advent16.* +import org.scalatest.matchers.should.Matchers.* + +class Advent16Spec extends AdventAppSpec(Advent16): + "solution1" - { + "test" in { + solution1(testData00) shouldEqual 71 + } + + "real" in { + solution1(realData) shouldEqual 21980 + } + } + + "solution2" - { + "test" in { + solve2(testData01, "class") shouldEqual 12 + solve2(testData01, "row") shouldEqual 11 + solve2(testData01, "seat") shouldEqual 13 + } + + "real" in { + solution2(realData) shouldEqual 1439429522627L + } + } +