Compare commits

...

4 Commits

Author SHA1 Message Date
yura ca6b4df548 Adding MatrixStorageTest 2026-05-13 09:38:06 +02:00
yura 512ba38209 Adding some extensions 2026-05-12 12:37:53 +02:00
yura 49b6dffdba Adding companion create method 2026-05-12 08:55:24 +02:00
yura 9f20223e92 Basic Matrix and Matrix Storage implementation 2026-05-11 22:57:06 +02:00
25 changed files with 497 additions and 2 deletions
+1 -1
View File
@@ -35,7 +35,7 @@ kotlin {
kotlin {
jvmToolchain {
languageVersion = JavaLanguageVersion.of(25)
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.ADOPTIUM
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ mockk = "1.14.9" # https://mvnrepository.com/artifact/io.mockk/mockk
kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin" }
memoize = {group = "org.duckdns.davygora", name = "memoize", version.ref="memoize"}
memoize = { group = "org.duckdns.davygora", name = "memoize", version.ref = "memoize" }
# Test
@@ -0,0 +1,34 @@
package org.duckdns.davygora.matrix
import org.duckdns.davygora.matrix.storage.MatrixStorage
abstract class AbstractMatrix<T>(
override val xSize: Int,
override val ySize: Int,
protected val matrix: MatrixStorage<T>,
) : Matrix<T> {
override fun toString(): String =
(0..<ySize)
.map { y ->
"[\t${(0..<xSize).joinToString("\t") { x -> this[x, y].toString() }}\t]"
}.joinToString { "\n" }
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AbstractMatrix<T>
if (xSize != other.xSize) return false
if (ySize != other.ySize) return false
if (matrix != other.matrix) return false
return true
}
override fun hashCode(): Int {
var result = xSize
result = 31 * result + ySize
result = 31 * result + matrix.hashCode()
return result
}
}
@@ -0,0 +1,10 @@
package org.duckdns.davygora.matrix
import org.duckdns.davygora.matrix.storage.MatrixStorage
abstract class AbstractMutableMatrix<T>(
xSize: Int,
ySize: Int,
matrix: MatrixStorage<T>,
) : AbstractMatrix<T>(xSize, ySize, matrix),
MutableMatrix<T>
@@ -0,0 +1,11 @@
package org.duckdns.davygora.matrix
interface Matrix<T> {
val xSize: Int
val ySize: Int
operator fun get(
x: Int,
y: Int,
): T
}
@@ -0,0 +1,9 @@
package org.duckdns.davygora.matrix
interface MutableMatrix<T> : Matrix<T> {
operator fun set(
x: Int,
y: Int,
value: T,
)
}
@@ -0,0 +1,7 @@
package org.duckdns.davygora.matrix.exception
class MatrixCoordinateOutOfRangeException(
coordName: Char,
coord: Int,
maxCoord: Int,
) : MatrixException("${coordName.uppercaseChar()} coordinate $coord is out of range, allowed is 0..$maxCoord")
@@ -0,0 +1,6 @@
package org.duckdns.davygora.matrix.exception
sealed class MatrixException(
message: String,
cause: Throwable? = null,
) : RuntimeException(message, cause)
@@ -0,0 +1,6 @@
package org.duckdns.davygora.matrix.exception
class MatrixInvalidSizeException(
coordinate: Char,
size: Int,
) : MatrixException("${coordinate.uppercaseChar()} size is invalid: $size (should be > 0)")
@@ -0,0 +1,5 @@
package org.duckdns.davygora.matrix.exception
class MatrixNotRectangularException(
xSizes: Collection<Int>,
) : MatrixException("Submitted matrix is not rectangular, different row sizes are $xSizes")
@@ -0,0 +1,6 @@
package org.duckdns.davygora.matrix.exception
class MatrixSizeMismatchException(
expectedSize: Int,
actualSize: Int,
) : MatrixException("Matrix size mismatch: expected $expectedSize, actual $actualSize")
@@ -0,0 +1,77 @@
package org.duckdns.davygora.matrix.impl
import org.duckdns.davygora.matrix.AbstractMutableMatrix
import org.duckdns.davygora.matrix.exception.MatrixCoordinateOutOfRangeException
import org.duckdns.davygora.matrix.exception.MatrixInvalidSizeException
import org.duckdns.davygora.matrix.exception.MatrixNotRectangularException
import org.duckdns.davygora.matrix.exception.MatrixSizeMismatchException
import org.duckdns.davygora.matrix.storage.MatrixStorage
import org.duckdns.davygora.matrix.storage.toMatrixStorage
import org.duckdns.davygora.memoize.memoize
class ArrayMatrix<T>(
xSize: Int,
ySize: Int,
matrix: MatrixStorage<T>,
) : AbstractMutableMatrix<T>(xSize, ySize, matrix) {
private val size = xSize * ySize
private val maxX = xSize - 1
private val maxY = ySize - 1
init {
if (ySize < 1) throw MatrixInvalidSizeException('y', ySize)
if (xSize < 1) throw MatrixInvalidSizeException('x', xSize)
if (size != matrix.size) throw MatrixSizeMismatchException(size, matrix.size)
}
override operator fun get(
x: Int,
y: Int,
): T = matrix[index(x, y)]
override operator fun set(
x: Int,
y: Int,
value: T,
) {
matrix[index(x, y)] = value
}
private val index = { x: Int, y: Int -> validateY(y) * xSize + validateX(x) }.memoize()
private val validateX = { x: Int -> validateCoord('x', x, maxX) }.memoize()
private val validateY = { y: Int -> validateCoord('y', y, maxY) }.memoize()
private fun validateCoord(
coordName: Char,
coord: Int,
maxCoord: Int,
) = if (coord in
0..maxCoord
) {
coord
} else {
throw MatrixCoordinateOutOfRangeException(coordName, coord, maxCoord)
}
companion object {
inline fun <reified T> create(values: List<List<T>>): ArrayMatrix<T> {
val ySize = values.size
val xSize =
when {
ySize > 0 -> {
values.map { row -> row.size }.toSet().let { xSizes ->
if (xSizes.size == 1) xSizes.first() else throw MatrixNotRectangularException(xSizes)
}
}
else -> {
0
}
}
return ArrayMatrix(xSize, ySize, values.flatten().toMatrixStorage())
}
}
}
@@ -0,0 +1,24 @@
package org.duckdns.davygora.matrix.impl
import org.duckdns.davygora.matrix.Matrix
import org.duckdns.davygora.matrix.MutableMatrix
import org.duckdns.davygora.matrix.storage.toMatrixStorage
import org.duckdns.davygora.matrix.util.toListChecked
inline fun <reified T> mutableMatrixOf(
xSize: Int,
ySize: Int,
vararg matrix: T,
) = ArrayMatrix(xSize, ySize, matrix.asList().toMatrixStorage()) as MutableMatrix<T>
inline fun <reified T> matrixOf(
xSize: Int,
ySize: Int,
vararg matrix: T,
) = mutableMatrixOf(xSize, ySize, *matrix) as Matrix<T>
inline fun <reified T> Iterable<Any>.toMatrix() = ArrayMatrix.create(this.map { it.toListChecked<T>() })
inline fun <reified T> Array<Any>.toMatrix() = this.asList().toMatrix<T>()
inline fun <reified T> Sequence<Any>.toMatrix() = this.toList().toMatrix<T>()
@@ -0,0 +1,21 @@
package org.duckdns.davygora.matrix.storage
@JvmInline
value class BooleanStorage(
override val data: BooleanArray,
) : MatrixStorage<Boolean> {
override val size get() = data.size
override operator fun get(index: Int) = data[index]
override fun set(
index: Int,
value: Boolean,
) {
data[index] = value
}
override fun toString(): String = data.contentToString()
}
fun BooleanArray.toMatrixStorage() = BooleanStorage(this)
@@ -0,0 +1,21 @@
package org.duckdns.davygora.matrix.storage
@JvmInline
value class ByteStorage(
override val data: ByteArray,
) : MatrixStorage<Byte> {
override val size get() = data.size
override operator fun get(index: Int) = data[index]
override fun set(
index: Int,
value: Byte,
) {
data[index] = value
}
override fun toString(): String = data.contentToString()
}
fun ByteArray.toMatrixStorage() = ByteStorage(this)
@@ -0,0 +1,21 @@
package org.duckdns.davygora.matrix.storage
@JvmInline
value class CharStorage(
override val data: CharArray,
) : MatrixStorage<Char> {
override val size get() = data.size
override operator fun get(index: Int) = data[index]
override fun set(
index: Int,
value: Char,
) {
data[index] = value
}
override fun toString(): String = data.contentToString()
}
fun CharArray.toMatrixStorage() = CharStorage(this)
@@ -0,0 +1,21 @@
package org.duckdns.davygora.matrix.storage
@JvmInline
value class DoubleStorage(
override val data: DoubleArray,
) : MatrixStorage<Double> {
override val size get() = data.size
override operator fun get(index: Int) = data[index]
override fun set(
index: Int,
value: Double,
) {
data[index] = value
}
override fun toString(): String = data.contentToString()
}
fun DoubleArray.toMatrixStorage() = DoubleStorage(this)
@@ -0,0 +1,21 @@
package org.duckdns.davygora.matrix.storage
@JvmInline
value class FloatStorage(
override val data: FloatArray,
) : MatrixStorage<Float> {
override val size get() = data.size
override operator fun get(index: Int) = data[index]
override fun set(
index: Int,
value: Float,
) {
data[index] = value
}
override fun toString(): String = data.contentToString()
}
fun FloatArray.toMatrixStorage() = FloatStorage(this)
@@ -0,0 +1,21 @@
package org.duckdns.davygora.matrix.storage
@JvmInline
value class IntStorage(
override val data: IntArray,
) : MatrixStorage<Int> {
override val size get() = data.size
override operator fun get(index: Int) = data[index]
override fun set(
index: Int,
value: Int,
) {
data[index] = value
}
override fun toString(): String = data.contentToString()
}
fun IntArray.toMatrixStorage() = IntStorage(this)
@@ -0,0 +1,21 @@
package org.duckdns.davygora.matrix.storage
@JvmInline
value class LongStorage(
override val data: LongArray,
) : MatrixStorage<Long> {
override val size get() = data.size
override operator fun get(index: Int) = data[index]
override fun set(
index: Int,
value: Long,
) {
data[index] = value
}
override fun toString(): String = data.contentToString()
}
fun LongArray.toMatrixStorage() = LongStorage(this)
@@ -0,0 +1,39 @@
package org.duckdns.davygora.matrix.storage
sealed interface MatrixStorage<T> {
val data: Any
val size: Int
operator fun get(index: Int): T
operator fun set(
index: Int,
value: T,
)
}
@Suppress("UNCHECKED_CAST")
inline fun <reified T> List<T>.toMatrixStorage(): MatrixStorage<T> {
firstNotNullOfOrNull { it }
val elementClass =
firstOrNull()
?.let { first -> first::class }
?.takeIf { clazz ->
all { it == null || it::class == clazz }
}
?: T::class
return when (elementClass) {
Boolean::class -> (this as List<Boolean>).toBooleanArray().toMatrixStorage()
Byte::class -> (this as List<Byte>).toByteArray().toMatrixStorage()
Char::class -> (this as List<Char>).toCharArray().toMatrixStorage()
Double::class -> (this as List<Double>).toDoubleArray().toMatrixStorage()
Float::class -> (this as List<Float>).toFloatArray().toMatrixStorage()
Int::class -> (this as List<Int>).toIntArray().toMatrixStorage()
Long::class -> (this as List<Long>).toLongArray().toMatrixStorage()
Short::class -> (this as List<Short>).toShortArray().toMatrixStorage()
else -> this.toTypedArray().toMatrixStorage()
} as MatrixStorage<T>
}
@@ -0,0 +1,21 @@
package org.duckdns.davygora.matrix.storage
@JvmInline
value class ObjectStorage<T>(
override val data: Array<T>,
) : MatrixStorage<T> {
override val size get() = data.size
override operator fun get(index: Int) = data[index]
override fun set(
index: Int,
value: T,
) {
data[index] = value
}
override fun toString(): String = data.contentToString()
}
fun <T> Array<T>.toMatrixStorage() = ObjectStorage(this)
@@ -0,0 +1,21 @@
package org.duckdns.davygora.matrix.storage
@JvmInline
value class ShortStorage(
override val data: ShortArray,
) : MatrixStorage<Short> {
override val size get() = data.size
override operator fun get(index: Int) = data[index]
override fun set(
index: Int,
value: Short,
) {
data[index] = value
}
override fun toString(): String = data.contentToString()
}
fun ShortArray.toMatrixStorage() = ShortStorage(this)
@@ -0,0 +1,22 @@
package org.duckdns.davygora.matrix.util
@Suppress("UNCHECKED_CAST")
inline fun <reified T> Any.toListChecked(): List<T> =
when (this) {
is List<*> -> this
is Collection<*> -> this.toList()
is Iterable<*> -> this.toList()
is Sequence<*> -> this.toList()
is Array<*> -> this.asList()
is BooleanArray -> this.asList()
is ByteArray -> this.asList()
is CharArray -> this.asList()
is DoubleArray -> this.asList()
is FloatArray -> this.asList()
is IntArray -> this.asList()
is LongArray -> this.asList()
is ShortArray -> this.asList()
else -> error("Unsupported type: ${this::class}")
}.also { list ->
require(list.all { it is T })
} as List<T>
@@ -0,0 +1,50 @@
package org.duckdns.davygora.matrix.storage
import io.kotest.core.spec.style.FunSpec
import io.kotest.datatest.withData
import io.kotest.matchers.shouldBe
import kotlin.reflect.KClass
class MatrixStorageTest :
FunSpec({
val testCases: Map<String, Triple<List<*>, KClass<*>, KClass<*>>> =
mapOf(
"boolean" to Triple(listOf(true, false), BooleanStorage::class, BooleanArray::class),
"byte" to Triple((0..100).map { it.toByte() }, ByteStorage::class, ByteArray::class),
"char" to Triple(('a'..'z') + ('A'..'Z') + ('0'..'9'), CharStorage::class, CharArray::class),
"double" to Triple((0..100).map { it.toDouble() }, DoubleStorage::class, DoubleArray::class),
"float" to Triple((0..100).map { it.toFloat() }, FloatStorage::class, FloatArray::class),
"int" to Triple((0..100).map { it }, IntStorage::class, IntArray::class),
"long" to Triple((0..100).map { it.toLong() }, LongStorage::class, LongArray::class),
"short" to Triple((0..100).map { it.toShort() }, ShortStorage::class, ShortArray::class),
"object" to Triple((0..100).map { first -> Pair(first, (0..100).random()) }, ObjectStorage::class, Array<Any>::class),
)
context("list of elements of a given type should produce corresponding storage, size, get and set should work") {
withData(
testCases,
) { (allowedValues, storageClass, dataClass) ->
// Arrange
val size = (10..1000).random()
val list = List(size) { allowedValues.random() }
val newList = List(size) { allowedValues.random() }
// Act
val storage = list.toMatrixStorage()
// Assert
storage::class shouldBe storageClass
storage.data::class shouldBe dataClass
storage.size shouldBe size
list.forEachIndexed { index, value -> storage[index] shouldBe value }
newList.forEachIndexed { index, value -> storage[index] = value }
newList.forEachIndexed { index, value -> storage[index] shouldBe value }
}
}
})