Skip to content

Commit

Permalink
Refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
jurisk committed Dec 23, 2024
1 parent 4cf0738 commit 8df313b
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 54 deletions.
12 changes: 12 additions & 0 deletions scala3/src/main/resources/2020/16-test-00.txt
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions scala3/src/main/resources/2020/16-test-01.txt
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion scala3/src/main/scala/jurisk/adventofcode/AdventApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
99 changes: 46 additions & 53 deletions scala3/src/main/scala/jurisk/adventofcode/y2020/Advent16.scala
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -33,81 +37,81 @@ 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
acc: List[(Name, Index)] = Nil,
): 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))
Expand All @@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
29 changes: 29 additions & 0 deletions scala3/src/test/scala/jurisk/adventofcode/y2020/Advent16Spec.scala
Original file line number Diff line number Diff line change
@@ -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
}
}

0 comments on commit 8df313b

Please sign in to comment.