Created
December 4, 2023 01:29
-
-
Save CJSmith-0141/347a6ec4fd12dce31892046a827dbbc8 to your computer and use it in GitHub Desktop.
AoC 2023 Day 3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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