Skip to content

Commit 2de5565

Browse files
committed
Grid: Factor out a ListGridBackend
The new backend encapsulates the implementation details of using mutable lists for managing the grid's data.
1 parent bcb3b14 commit 2de5565

File tree

3 files changed

+150
-40
lines changed

3 files changed

+150
-40
lines changed

src/main/kotlin/de/ronny_h/aoc/extensions/grids/Grid.kt

Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ abstract class Grid<T>(
2323
private val fallbackElement: T = nullElement,
2424
) {
2525

26-
private val grid: MutableList<MutableList<T>> = MutableList(height) { MutableList(width) { nullElement } }
26+
private val grid: GridBackend<T> = ListGridBackend(width, height, nullElement)
2727

2828
/**
2929
* A function that maps each `Char` that may occur in the `input: List<String>` to a value of type [T].
@@ -41,9 +41,7 @@ abstract class Grid<T>(
4141
overrideElement: T = nullElement,
4242
overrides: List<Coordinates> = emptyList()
4343
) : this(height, width, nullElement, overrideElement) {
44-
overrides.forEach {
45-
grid[it.y][it.x] = overrideElement
46-
}
44+
overrides.forEach { grid[it] = overrideElement }
4745
}
4846

4947
/**
@@ -64,27 +62,22 @@ abstract class Grid<T>(
6462

6563
protected fun initGrid(input: List<String>) = input.forEachIndexed { y, line ->
6664
line.forEachIndexed { x, char ->
67-
grid[y][x] = char.toElementType()
65+
grid[x, y] = char.toElementType()
6866
}
6967
}
7068

7169
/**
7270
* @return The value at the specified cell.
7371
*/
74-
operator fun get(x: Int, y: Int): T {
75-
return grid
76-
.getOrNull(y)
77-
?.getOrNull(x)
78-
?: fallbackElement
79-
}
72+
operator fun get(x: Int, y: Int): T = grid.getOrNull(x, y) ?: fallbackElement
8073

8174
operator fun set(x: Int, y: Int, value: T) {
82-
grid[y][x] = value
75+
grid[x, y] = value
8376
}
8477

85-
fun getAt(position: Coordinates) = get(position.x, position.y)
78+
fun getAt(position: Coordinates) = grid.getOrNull(position) ?: fallbackElement
8679
fun setAt(position: Coordinates, element: T) {
87-
this.set(position.x, position.y, element)
80+
grid[position] = element
8881
}
8982

9083
/**
@@ -93,37 +86,18 @@ abstract class Grid<T>(
9386
* in this Grid, and vice-versa.
9487
* Structural changes in the base Grid make the behavior of the view undefined.
9588
*/
96-
fun subGridAt(x: Int, y: Int, width: Int, height: Int = width): List<List<T>> {
97-
return buildList {
98-
for (r in y..<y + height) {
99-
add(grid[r].subList(x, x + width))
100-
}
101-
}
102-
}
89+
fun subGridAt(x: Int, y: Int, width: Int, height: Int = width): List<List<T>> = grid.subGridAt(x, y, width, height)
10390

104-
fun <R> forEachIndex(action: (x: Int, y: Int) -> R): Sequence<R> = sequence {
105-
for (y in grid.indices) {
106-
for (x in grid[0].indices) {
107-
yield(action(x, y))
108-
}
109-
}
110-
}
91+
fun <R> forEachIndex(action: (x: Int, y: Int) -> R): Sequence<R> = grid.mapToSequence(action)
11192

112-
fun <R> forEachElement(action: (x: Int, y: Int, element: T) -> R): Sequence<R> = sequence {
113-
for (y in grid.indices) {
114-
for (x in grid[0].indices) {
115-
yield(action(x, y, get(x, y)))
116-
}
117-
}
93+
fun <R> forEachElement(action: (x: Int, y: Int, element: T) -> R): Sequence<R> = grid.mapToSequence { x, y ->
94+
action(x, y, get(x, y))
11895
}
11996

120-
fun <R> forEachCoordinates(action: (position: Coordinates, element: T) -> R): Sequence<R> = sequence {
121-
for (y in grid.indices) {
122-
for (x in grid[0].indices) {
123-
yield(action(Coordinates(x, y), get(x, y)))
124-
}
97+
fun <R> forEachCoordinates(action: (position: Coordinates, element: T) -> R): Sequence<R> =
98+
grid.mapToSequence { x, y ->
99+
action(Coordinates(x, y), get(x, y))
125100
}
126-
}
127101

128102
/**
129103
* Returns the first element matching the given [value].
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package de.ronny_h.aoc.extensions.grids
2+
3+
interface GridBackend<T> {
4+
operator fun get(x: Int, y: Int): T
5+
operator fun get(at: Coordinates): T
6+
7+
operator fun set(x: Int, y: Int, value: T)
8+
operator fun set(at: Coordinates, value: T)
9+
10+
/**
11+
* @return an element at the given [x] and [y] coordinates or null if the index is out of bounds of this GridBackend.
12+
*/
13+
fun getOrNull(x: Int, y: Int): T?
14+
fun getOrNull(at: Coordinates): T?
15+
16+
/**
17+
* @return a view of the portion of this Grid between the specified [x], [y] (inclusive) and [x] + [width], [y] + [height] (exclusive).
18+
* The returned list of lists is backed by this Grid, so non-structural changes in the returned list are reflected
19+
* in this Grid, and vice-versa.
20+
* Structural changes in the base Grid make the behavior of the view undefined.
21+
*/
22+
fun subGridAt(x: Int, y: Int, width: Int, height: Int = width): List<List<T>>
23+
24+
fun <R> mapToSequence(transform: (x: Int, y: Int) -> R): Sequence<R>
25+
}
26+
27+
class ListGridBackend<T>(width: Int, height: Int, nullElement: T) : GridBackend<T> {
28+
private val grid: MutableList<MutableList<T>> = MutableList(height) { MutableList(width) { nullElement } }
29+
30+
override fun get(x: Int, y: Int) = grid[y][x]
31+
override fun get(at: Coordinates): T = grid[at.y][at.x]
32+
33+
override fun set(x: Int, y: Int, value: T) {
34+
grid[y][x] = value
35+
}
36+
37+
override fun set(at: Coordinates, value: T) {
38+
grid[at.y][at.x] = value
39+
}
40+
41+
override fun getOrNull(x: Int, y: Int): T? = grid
42+
.getOrNull(y)
43+
?.getOrNull(x)
44+
45+
override fun getOrNull(at: Coordinates): T? = getOrNull(at.x, at.y)
46+
47+
override fun subGridAt(x: Int, y: Int, width: Int, height: Int): List<List<T>> = buildList {
48+
for (row in y..<y + height) {
49+
add(grid[row].subList(x, x + width))
50+
}
51+
}
52+
53+
override fun <R> mapToSequence(transform: (x: Int, y: Int) -> R): Sequence<R> = sequence {
54+
for (y in grid.indices) {
55+
for (x in grid[0].indices) {
56+
yield(transform(x, y))
57+
}
58+
}
59+
}
60+
61+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package de.ronny_h.aoc.extensions.grids
2+
3+
import io.kotest.assertions.throwables.shouldThrow
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 ListGridBackendTest : StringSpec({
10+
11+
"get returns the existing element: the one that's set or the fallback element" {
12+
val grid: GridBackend<Char> = ListGridBackend(2, 2, '#')
13+
grid[0, 0] = '0'
14+
grid[Coordinates(1, 1)] = '1'
15+
16+
forAll(
17+
row(0, 0, '0'),
18+
row(0, 1, '#'),
19+
row(1, 0, '#'),
20+
row(1, 1, '1'),
21+
) { x, y, expected ->
22+
grid[x, y] shouldBe expected
23+
grid[Coordinates(x, y)] shouldBe expected
24+
}
25+
}
26+
27+
"get for non existing element throws an Exception" {
28+
val grid: GridBackend<Char> = ListGridBackend(2, 2, '#')
29+
30+
shouldThrow<IndexOutOfBoundsException> {
31+
grid[3, 0]
32+
}
33+
}
34+
35+
"getOrNull returns null for non existing elements" {
36+
val grid: GridBackend<Char> = ListGridBackend(2, 2, '#')
37+
grid[0, 0] = '0'
38+
grid[Coordinates(1, 1)] = '1'
39+
40+
forAll(
41+
row(0, 0, '0'),
42+
row(0, 1, '#'),
43+
row(1, 0, '#'),
44+
row(1, 1, '1'),
45+
row(2, 0, null),
46+
row(0, 2, null),
47+
) { x, y, expected ->
48+
grid.getOrNull(x, y) shouldBe expected
49+
grid.getOrNull(Coordinates(x, y)) shouldBe expected
50+
}
51+
}
52+
53+
"subGridAt returns a list of lists for the specified section" {
54+
val grid: GridBackend<Char> = ListGridBackend(3, 3, '#')
55+
grid[0, 0] = '0'
56+
grid[Coordinates(1, 1)] = '1'
57+
grid[Coordinates(2, 2)] = '2'
58+
59+
grid.subGridAt(1, 1, 2, 2) shouldBe listOf(
60+
listOf('1', '#'),
61+
listOf('#', '2'),
62+
)
63+
}
64+
65+
"mapToSequence transforms each grid coordinates row by row" {
66+
val grid = ListGridBackend(2, 2, 0)
67+
grid[1, 0] = 1
68+
grid[0, 1] = 2
69+
grid[1, 1] = 3
70+
71+
grid.mapToSequence { x, y ->
72+
"${grid[x, y]}"
73+
}.toList() shouldBe listOf("0", "1", "2", "3")
74+
}
75+
})

0 commit comments

Comments
 (0)