Skip to content

Instantly share code, notes, and snippets.

@CJSmith-0141
Created December 4, 2023 01:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CJSmith-0141/347a6ec4fd12dce31892046a827dbbc8 to your computer and use it in GitHub Desktop.
Save CJSmith-0141/347a6ec4fd12dce31892046a827dbbc8 to your computer and use it in GitHub Desktop.
AoC 2023 Day 3
package net.tazato
import cats.*
import cats.effect.*
import cats.syntax.all.*
object Day3 extends IOApp.Simple {
case class PartNumberCoordinate(xMin: Int, xMax: Int, y: Int)
case class PartNumber(partNumber: Int, coordinate: PartNumberCoordinate)
case class GearCoordinate(x: Int, y: Int)
case class GearsWithRatios(gc: GearCoordinate, parts: List[PartNumber])
val input =
"""467..114..
|...*......
|..35..633.
|......#...
|617*......
|.....+.58.
|..592.....
|......755.
|...$.*....
|.664.598..""".stripMargin
val input2: String = io.Source.fromResource("day3.txt").mkString
def blankPadInput(input: String): String = {
val lines = input.split("\n")
val leftAndRight = lines.map("." + _ + ".")
val width = leftAndRight(0).length
val header = "." * width
header +: leftAndRight :+ header mkString "\n"
}
def playAreaToVector(playArea: String): Vector[Vector[Char]] = {
playArea.split("\n").toVector.map(_.toVector)
}
def coordinateToRegion(
partNumberCoordinate: PartNumberCoordinate
)(using playArea: Vector[Vector[Char]]): Vector[Vector[Char]] =
val y = partNumberCoordinate.y
val xin = partNumberCoordinate.xMin
val xax = partNumberCoordinate.xMax
// the second argument to slice is exclusive
playArea.slice(y - 1, y + 2).map(_.slice(xin - 1, xax + 1))
val charTest: Char => Boolean = (c: Char) => c.isDigit || c == '.'
def regionHoldsSymbol(region: Vector[Vector[Char]]) =
!region.forall(_.forall(charTest))
def rowToPartNumbers(y: Int, row: String): List[PartNumber] =
"""([0-9]+)""".r.findAllMatchIn(row).toList.map { m =>
val partNumber = m.group(1).toInt
val xMin = m.start
val xMax = m.end
PartNumber(partNumber, PartNumberCoordinate(xMin, xMax, y))
}
def playAreaToPartNumbers(using
playArea: Vector[Vector[Char]]
): List[PartNumber] =
playArea.zipWithIndex.flatMap { (row, y) =>
rowToPartNumbers(y, row.mkString)
}.toList
def playAreaToGearCoordinates(using
playArea: Vector[Vector[Char]]
): List[GearCoordinate] =
playArea.zipWithIndex.flatMap { (row, y) =>
row.zipWithIndex.flatMap { (c, x) =>
if c == '*' then Some(GearCoordinate(x, y)) else None
}
}.toList
def validatePartNumbers(partNumbers: List[PartNumber])(using
playArea: Vector[Vector[Char]]
): List[PartNumber] =
partNumbers.filter { partNumber =>
val region = coordinateToRegion(partNumber.coordinate)
regionHoldsSymbol(region)
}
def sumPartNumbers(partNumbers: List[PartNumber]): Int =
partNumbers.map(_.partNumber).sum
def gearCoordinateRegionIntersectsPartNumber(
gc: GearCoordinate,
pn: PartNumber
): Boolean =
case class Region(xMin: Int, xMax: Int, yMin: Int, yMax: Int)
def regionsIntersect(r1: Region, r2: Region): Boolean =
r1.xMax >= r2.xMin && r2.xMax >= r1.xMin && r1.yMax >= r2.yMin && r2.yMax >= r1.yMin
val gearRegion = Region(gc.x - 1, gc.x + 1, gc.y - 1, gc.y + 1)
val partNumberRegion = Region(
pn.coordinate.xMin,
pn.coordinate.xMax - 1,
pn.coordinate.y,
pn.coordinate.y
)
regionsIntersect(gearRegion, partNumberRegion)
def findAllGearRatios(
gcs: List[GearCoordinate],
pns: List[PartNumber]
): List[GearsWithRatios] =
gcs
.map { gc =>
val parts = pns.filter(gearCoordinateRegionIntersectsPartNumber(gc, _))
GearsWithRatios(gc, parts)
}
.filter(_.parts.length == 2)
def answerPart2(gearsWithRatios: List[GearsWithRatios]): Int =
gearsWithRatios.map(_.parts.map(_.partNumber).product).sum
val fiddlingAboutF: IO[Unit] = for {
_ <- IO.println(blankPadInput(input))
playAreaString = blankPadInput(input)
given Vector[Vector[Char]] = playAreaToVector(playAreaString)
neighboorhood = coordinateToRegion(
PartNumberCoordinate(1, 4, 1)
)
_ <- IO.println(neighboorhood.show)
_ <- IO.println(regionHoldsSymbol(neighboorhood))
n2 <- IO(
coordinateToRegion(PartNumberCoordinate(8, 9, 6))
)
_ <- IO.println(n2.show)
_ <- IO.println(regionHoldsSymbol(n2))
_ <- IO.println(rowToPartNumbers(1, ".467..114..."))
} yield ()
val answerPart1F: IO[Unit] =
val playAreaString = blankPadInput(input2)
given playArea: Vector[Vector[Char]] = playAreaToVector(playAreaString)
val partNumbers = playAreaToPartNumbers
val validPartNumbers = validatePartNumbers(partNumbers)
val answer = sumPartNumbers(validPartNumbers)
IO.println(answer)
val answerPart2F: IO[Unit] =
val playAreaString = blankPadInput(input2)
given playArea: Vector[Vector[Char]] = playAreaToVector(playAreaString)
val gearCoordinates = playAreaToGearCoordinates
val partNumbers = playAreaToPartNumbers
val gearsWithRatios = findAllGearRatios(gearCoordinates, partNumbers)
val answer = answerPart2(gearsWithRatios)
IO.println(answer)
def run: IO[Unit] = answerPart2F
}
package net.tazato
import weaver.*
object Day3Suite extends weaver.FunSuite {
val example = Day3.input
given playArea: Vector[Vector[Char]] =
Day3.playAreaToVector(Day3.blankPadInput(example))
test("add blanks") {
val input =
"""abc
|123
|456""".stripMargin
val expected =
""".....
|.abc.
|.123.
|.456.
|.....""".stripMargin
expect.same(expected, Day3.blankPadInput(input))
}
test("play area to vector") {
val input =
""".....
|.abc.
|.123.
|.456.
|.....""".stripMargin
val expected = Vector(
Vector('.', '.', '.', '.', '.'),
Vector('.', 'a', 'b', 'c', '.'),
Vector('.', '1', '2', '3', '.'),
Vector('.', '4', '5', '6', '.'),
Vector('.', '.', '.', '.', '.')
)
expect.same(expected, Day3.playAreaToVector(input))
}
test("coordinate to neighbourhood") {
val expected = Vector(
Vector('.', '.', '.', '.', '.'),
Vector('.', '4', '6', '7', '.'),
Vector('.', '.', '.', '.', '*')
)
expect.same(
expected,
Day3.coordinateToRegion(
Day3.PartNumberCoordinate(1, 4, 1)
)
)
}
test("final answer part 1") {
val expected = 4361
val allPartNumbers = Day3.playAreaToPartNumbers
val validPartNumbers = Day3.validatePartNumbers(allPartNumbers)
expect.same(expected, Day3.sumPartNumbers(validPartNumbers))
}
test("gear coordinates") {
val expected = List(
Day3.GearCoordinate(4, 2),
Day3.GearCoordinate(4, 5),
Day3.GearCoordinate(6, 9)
)
expect.same(expected, Day3.playAreaToGearCoordinates)
}
test("gear intersect part number") {
val gc = Day3.GearCoordinate(4, 2)
val pn = Day3.PartNumber(467, Day3.PartNumberCoordinate(1, 4, 1))
expect(Day3.gearCoordinateRegionIntersectsPartNumber(gc, pn))
val gcbad = Day3.GearCoordinate(4, 5)
expect(!Day3.gearCoordinateRegionIntersectsPartNumber(gcbad, pn))
val pngood = Day3.PartNumber(35, Day3.PartNumberCoordinate(3, 4, 3))
expect(Day3.gearCoordinateRegionIntersectsPartNumber(gc, pngood))
}
test("correctly identify gears") {
val gcs = Day3.playAreaToGearCoordinates
val pns = Day3.playAreaToPartNumbers
val res = Day3.findAllGearRatios(gcs, pns)
val expected = List(
Day3.GearsWithRatios(
Day3.GearCoordinate(4, 2),
List(
Day3.PartNumber(467, Day3.PartNumberCoordinate(1, 4, 1)),
Day3.PartNumber(35, Day3.PartNumberCoordinate(3, 5, 3))
)
),
Day3.GearsWithRatios(
Day3.GearCoordinate(6, 9),
List(
Day3.PartNumber(755, Day3.PartNumberCoordinate(7, 10, 8)),
Day3.PartNumber(598, Day3.PartNumberCoordinate(6, 9, 10))
)
)
)
expect.same(expected, res)
}
test("final answer part 2") {
val gcs = Day3.playAreaToGearCoordinates
val pns = Day3.playAreaToPartNumbers
val res = Day3.findAllGearRatios(gcs, pns)
val expected = 467835
expect.same(expected, Day3.answerPart2(res))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment