Skip to content

Commit ef32c8b

Browse files
committed
feat(aoc-2023): day05 part1 and part2
1 parent 793f34d commit ef32c8b

2 files changed

Lines changed: 170 additions & 0 deletions

File tree

2023/src/main/scala/day05.scala

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package com.markkovari.adventofcode.day05
2+
3+
import scala.util.matching.Regex.Match
4+
5+
final case class Resource(start: Long, end: Long, kind: ResourceKind)
6+
7+
enum ResourceKind:
8+
case Seed, Soil, Fertilizer, Water,
9+
Light, Temperature, Humidity, Location
10+
11+
final case class ResourceMap(
12+
from: ResourceKind,
13+
to: ResourceKind,
14+
properties: Seq[Property]
15+
)
16+
17+
final case class Property(
18+
destinationStart: Long,
19+
sourceStart: Long,
20+
rangeLength: Long
21+
):
22+
23+
lazy val sourceEnd: Long = sourceStart + rangeLength - 1
24+
end Property
25+
26+
def findNext(resource: Resource, map: ResourceMap): Seq[Resource] =
27+
val ResourceMap(from, to, properties) = map
28+
val (newResources, explore) =
29+
val initial = (Seq.empty[Resource], Option(resource))
30+
properties.foldLeft(initial) {
31+
case ((acc, Some(explore)), prop) =>
32+
val Resource(start, end, _) = explore
33+
val propStart = prop.sourceStart
34+
val propEnd = prop.sourceEnd
35+
val underRange = Option.when(start < propStart)(
36+
Resource(start, Math.min(propStart - 1, end), to)
37+
)
38+
val overlaps =
39+
start >= propStart && start <= propEnd
40+
|| end >= propStart && end <= propEnd
41+
|| start <= propStart && end >= propEnd
42+
val inRange = Option.when(overlaps) {
43+
val delay = prop.destinationStart - propStart
44+
Resource(
45+
Math.max(start, propStart) + delay,
46+
Math.min(end, propEnd) + delay,
47+
to
48+
)
49+
}
50+
val aboveRange = Option.when(end > propEnd)(
51+
Resource(Math.max(start, propEnd + 1), end, to)
52+
)
53+
(Seq(underRange, inRange, acc).flatten, aboveRange)
54+
case ((acc, None), _) => (acc, None)
55+
}
56+
Seq(newResources, explore).flatten
57+
end findNext
58+
59+
object ResourceMap:
60+
def buildFromLines(lines: Seq[String]): Seq[ResourceMap] =
61+
def isRangeLine(line: String) =
62+
line.forall(ch => ch.isDigit || ch.isSpaceChar)
63+
lines
64+
.filter(line =>
65+
!line.isBlank &&
66+
(line.endsWith("map:") || isRangeLine(line))
67+
)
68+
.foldLeft(Seq.empty[(String, Seq[String])]) {
69+
case (acc, line) if line.endsWith("map:") =>
70+
(line, Seq.empty) +: acc
71+
case (Seq((definition, properties), last*), line) =>
72+
(definition, line +: properties) +: last
73+
}
74+
.flatMap(build)
75+
76+
def build(map: String, ranges: Seq[String]): Option[ResourceMap] =
77+
val mapRow = map.replace("map:", "").trim.split("-to-")
78+
val properties = ranges
79+
.map(line => line.split(" ").flatMap(_.toLongOption))
80+
.collect:
81+
case Array(startFrom, startTo, range) =>
82+
Property(startFrom, startTo, range)
83+
def resourceKindOf(optStr: Option[String]) =
84+
optStr.map(_.capitalize).map(ResourceKind.valueOf)
85+
for
86+
from <- resourceKindOf(mapRow.headOption)
87+
to <- resourceKindOf(mapRow.lastOption)
88+
yield ResourceMap(from, to, properties.sortBy(_.sourceStart))
89+
end ResourceMap
90+
91+
object Seeds:
92+
private def parseSeedsRaw(line: String): Seq[Long] =
93+
if !line.startsWith("seeds:") then Seq.empty[Long]
94+
else
95+
line
96+
.replace("seeds:", "")
97+
.trim
98+
.split(" ")
99+
.flatMap(_.toLongOption)
100+
101+
// parse seeds without range
102+
def parseWithoutRange(line: String): Seq[Resource] =
103+
parseSeedsRaw(line).map: start =>
104+
Resource(start, start, ResourceKind.Seed)
105+
106+
// parse seeds with range
107+
def parse(line: String): Seq[Resource] =
108+
parseSeedsRaw(line)
109+
.grouped(2)
110+
.map { case Seq(start, length) =>
111+
Resource(start, start + length - 1, ResourceKind.Seed)
112+
}
113+
.toSeq
114+
end Seeds
115+
116+
def calculate(seeds: Seq[Resource], maps: Seq[ResourceMap]): Long =
117+
def inner(resource: Resource): Seq[Resource] =
118+
if resource.kind == ResourceKind.Location then Seq(resource)
119+
else
120+
val map = maps.find(_.from == resource.kind).get
121+
findNext(resource, map).flatMap(inner)
122+
seeds.flatMap(inner).minBy(_.start).start
123+
end calculate
124+
125+
type ParseSeeds = String => Seq[Resource]
126+
127+
def solution(input: String, parse: ParseSeeds): Long =
128+
val lines = input.linesIterator.toSeq
129+
val seeds = lines.headOption.map(parse).getOrElse(Seq.empty)
130+
val maps = ResourceMap.buildFromLines(lines)
131+
calculate(seeds, maps)
132+
133+
def part1(input: String): Long =
134+
solution(input, Seeds.parseWithoutRange)
135+
136+
def part2(input: String): Long =
137+
solution(input, Seeds.parse)

2023/src/test/scala/day05.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.markkovari.adventofcode
2+
3+
import org.scalatest.funsuite.AnyFunSuite
4+
import scala.io.Source
5+
import scala.collection.immutable.HashMap
6+
import day05._
7+
8+
class Day05Tests extends AnyFunSuite {
9+
10+
test("Part1 example") {
11+
val input = io.Source.fromFile("./src/test/resources/5/example_1").mkString
12+
val result = part1(input)
13+
assert(result == 35)
14+
}
15+
16+
test("Part1 is calculated") {
17+
val input = io.Source.fromFile("./src/test/resources/5/values").mkString
18+
val result = part1(input)
19+
assert(result == 1181555926)
20+
}
21+
22+
test("Part2 example") {
23+
val input = io.Source.fromFile("./src/test/resources/5/example_1").mkString
24+
val result = part2(input)
25+
assert(result == 46)
26+
}
27+
28+
test("Part2 is calculated") {
29+
val input = io.Source.fromFile("./src/test/resources/5/values").mkString
30+
val result = part2(input)
31+
assert(result == 37806486)
32+
}
33+
}

0 commit comments

Comments
 (0)