Skip to content

Commit 9f3d89a

Browse files
committed
Solution 2025-05 (Cafeteria)
1 parent 0789f4f commit 9f3d89a

File tree

2 files changed

+131
-0
lines changed

2 files changed

+131
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package de.ronny_h.aoc.year2025.day05
2+
3+
import de.ronny_h.aoc.AdventOfCode
4+
import de.ronny_h.aoc.extensions.collections.split
5+
6+
fun main() = Cafeteria().run(885, 348115621205535)
7+
8+
class Cafeteria : AdventOfCode<Long>(2025, 5) {
9+
override fun part1(input: List<String>): Long {
10+
with(input.parseIngredients()) {
11+
return available.count { it.isFresh(freshRanges) }.toLong()
12+
}
13+
}
14+
15+
override fun part2(input: List<String>): Long =
16+
input.parseIngredients()
17+
.freshRanges
18+
.sortedBy { it.first }
19+
.fold(mutableListOf<LongRange>()) { compactRanges, range ->
20+
compactRanges.mergeRange(range)
21+
}.sumOf {
22+
it.last - it.first + 1
23+
}
24+
}
25+
26+
data class Ingredients(val freshRanges: List<LongRange>, val available: List<Long>)
27+
28+
// invariant: ranges in this MutableList are non-overlapping and sorted
29+
// pre-condition: add operations only occur with ranges sorted by their first value
30+
fun MutableList<LongRange>.mergeRange(toAdd: LongRange): MutableList<LongRange> {
31+
if (none { toAdd.first in it || toAdd.last in it }) {
32+
// disjunct -> just add it
33+
add(toAdd)
34+
return this
35+
}
36+
for ((i, range) in withIndex()) {
37+
if (toAdd.first in range && toAdd.last in range) {
38+
// toAdd is completely contained in this range
39+
return this
40+
}
41+
if (toAdd.first !in range && toAdd.last !in range) {
42+
continue
43+
}
44+
45+
val lastOverlappingRangeIndex = indexOfLast { toAdd.last in it }
46+
if (toAdd.first in range && lastOverlappingRangeIndex == -1) {
47+
// the beginning overlaps with this range -> merge
48+
this[i] = range.first..toAdd.last
49+
return this
50+
}
51+
52+
// overlaps with several ranges -> squash them
53+
val lastOverlappingRange = this[lastOverlappingRangeIndex]
54+
for (index in lastOverlappingRangeIndex downTo i + 1) {
55+
this.removeAt(index)
56+
}
57+
this[i] = range.first..lastOverlappingRange.last
58+
return this
59+
}
60+
error("If this line is reached, a case is missing")
61+
}
62+
63+
fun Long.isFresh(freshRanges: List<LongRange>): Boolean = freshRanges.any { this in it }
64+
65+
fun List<String>.parseIngredients(): Ingredients {
66+
val (fresh, available) = split()
67+
return Ingredients(
68+
fresh.map {
69+
val (from, to) = it.split("-").map(String::toLong)
70+
from..to
71+
},
72+
available.map { it.toLong() }
73+
)
74+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package de.ronny_h.aoc.year2025.day05
2+
3+
import de.ronny_h.aoc.extensions.asList
4+
import io.kotest.core.spec.style.StringSpec
5+
import io.kotest.data.forAll
6+
import io.kotest.data.row
7+
import io.kotest.matchers.shouldBe
8+
9+
class CafeteriaTest : StringSpec({
10+
11+
val input = """
12+
3-5
13+
10-14
14+
16-20
15+
12-18
16+
17+
1
18+
5
19+
8
20+
11
21+
17
22+
32
23+
""".asList()
24+
25+
"input can be parsed" {
26+
val input = """
27+
3-5
28+
10-14
29+
30+
1
31+
5
32+
11
33+
""".asList()
34+
input.parseIngredients() shouldBe Ingredients(listOf(3L..5L, 10L..14L), listOf(1L, 5L, 11L))
35+
}
36+
37+
"mergeRange merges the added range with the existing ones" {
38+
forAll(
39+
row(mutableListOf(2L..5L), 3L..4L, listOf(2L..5L)),
40+
row(mutableListOf(2L..5L), 6L..7L, listOf(2L..5L, 6L..7L)),
41+
row(mutableListOf(2L..5L, 8L..9L), 4L..6L, listOf(2L..6L, 8L..9L)),
42+
row(mutableListOf(2L..5L, 8L..9L), 4L..8L, listOf(2L..9L)),
43+
row(mutableListOf(2L..5L, 8L..9L, 11L..13L), 4L..11L, listOf(2L..13L)),
44+
row(mutableListOf(0L..1L, 4L..5L, 8L..9L, 11L..13L, 15L..20L), 4L..11L, listOf(0L..1L, 4L..13L, 15L..20L)),
45+
) { list, range, expected ->
46+
list.mergeRange(range) shouldBe expected
47+
}
48+
}
49+
50+
"part 1: the number of available ingredient IDs that are fresh" {
51+
Cafeteria().part1(input) shouldBe 3
52+
}
53+
54+
"part 2: the total number of ingredient IDs considered to be fresh " {
55+
Cafeteria().part2(input) shouldBe 14
56+
}
57+
})

0 commit comments

Comments
 (0)