diff --git a/kroki-collections/pom.xml b/kroki-collections/pom.xml
new file mode 100644
index 0000000..dd17d65
--- /dev/null
+++ b/kroki-collections/pom.xml
@@ -0,0 +1,72 @@
+
+
+
+ kroki
+ io.github.kerubistan.kroki
+ 1.24-SNAPSHOT
+
+
+ 4.0.0
+
+ kroki-collections
+ jar
+
+
+
+ The Apache Software License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0
+
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+
+
+ org.junit.jupiter
+ junit-jupiter
+
+
+ io.github.kerubistan.kroki
+ kroki-delegates
+ 1.24-SNAPSHOT
+
+
+ io.kotest
+ kotest-assertions-core-jvm
+ 5.9.1
+
+
+
+
+ src/main/kotlin
+ src/test/kotlin
+
+
+ org.jetbrains.dokka
+ dokka-maven-plugin
+
+
+ maven-surefire-plugin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+
+
+
+
+
diff --git a/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/Collections.kt b/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/Collections.kt
new file mode 100644
index 0000000..ba3d249
--- /dev/null
+++ b/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/Collections.kt
@@ -0,0 +1,29 @@
+package io.github.kerubistan.kroki.collections
+
+fun immutableListOf(vararg items: T): List =
+ when (items.size) {
+ 0 -> emptyList()
+ 1 -> listOf(items.single())
+ else -> ImmutableArrayList(items)
+ }
+
+inline fun List.toImmutableList(): List = when {
+ this.isEmpty() -> emptyList()
+ this.size == 1 -> listOf(first())
+ else -> immutableListOf(*(this.toTypedArray()))
+}
+
+internal fun listHashCode(list: List): Int {
+ var hashCode = 1
+ list.forEach { hashCode = (hashCode * 31) + it.hashCode() }
+ return hashCode
+}
+
+fun > List.immutableSorted(): List =
+ when (this) {
+ is ImmutableArrayList -> this.sortedToImmutable()
+ else -> this.sorted()
+ }
+
+inline fun buildList(builder: ImmutableListBuilder.() -> Unit) =
+ ImmutableListBuilder().apply(builder).build()
diff --git a/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableArrayList.kt b/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableArrayList.kt
new file mode 100644
index 0000000..11097a7
--- /dev/null
+++ b/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableArrayList.kt
@@ -0,0 +1,130 @@
+package io.github.kerubistan.kroki.collections
+
+import io.github.kerubistan.kroki.delegates.weak
+
+/**
+ * Implementation of an immutable list, internally based on an array.
+ */
+internal class ImmutableArrayList() : List {
+
+ internal lateinit var items: Array
+
+ internal constructor(array: Array) : this() {
+ this.items = array
+ }
+
+ companion object {
+ fun of(vararg items: T): List = ImmutableArrayList(items)
+ }
+
+ override val size: Int
+ get() = items.size
+
+ override fun get(index: Int): T {
+ try {
+ return items[index]
+ } catch (aiob: ArrayIndexOutOfBoundsException) {
+ throw IllegalArgumentException("size is ${items.size}, requested index is $index", aiob)
+ }
+ }
+
+ override fun isEmpty(): Boolean = size == 0
+
+ private class ImmutableArrayListIterator(private val items: Array) : ListIterator {
+ private var index = 0
+
+ constructor(items: Array, index: Int) : this(items) {
+ check(index > 0) { "Index must be greater than zero" }
+ check(index < items.size) { "Index must be less than the size of the array" }
+ this.index = index
+ }
+
+ override fun hasNext(): Boolean = items.size > index
+ override fun hasPrevious(): Boolean = index > 0
+
+ override fun next(): T {
+ try {
+ val returnValue = items[index]
+ index += 1
+ return returnValue
+ } catch (aie: ArrayIndexOutOfBoundsException) {
+ throw IllegalArgumentException("no more items left", aie)
+ }
+ }
+
+ override fun nextIndex(): Int = index + 1
+
+ override fun previous(): T {
+ require(index > 0) { "No previous item before the start of the list" }
+ return items[--index]
+ }
+
+
+ override fun previousIndex(): Int = index - 1
+ }
+
+ override fun iterator(): Iterator = ImmutableArrayListIterator(this.items)
+
+ override fun listIterator(): ListIterator = ImmutableArrayListIterator(this.items)
+
+ override fun listIterator(index: Int): ListIterator = ImmutableArrayListIterator(this.items, index)
+
+ override fun subList(fromIndex: Int, toIndex: Int): List =
+ when {
+ toIndex == fromIndex -> emptyList()
+ fromIndex == toIndex - 1 -> listOf(this.items[fromIndex])
+ fromIndex == 0 && toIndex == size + 1 -> this
+ else -> ImmutableSubArrayList(fromIndex, toIndex, this.items)
+ }
+
+ override fun lastIndexOf(element: T): Int = items.lastIndexOf(element)
+
+ override fun indexOf(element: T): Int = items.indexOf(element)
+
+ override fun containsAll(elements: Collection): Boolean = elements.all(items::contains)
+
+ override fun contains(element: T): Boolean = items.contains(element)
+
+ override fun equals(other: Any?): Boolean {
+ when (other) {
+ is ImmutableArrayList<*> -> return this.items.contentEquals(other.items)
+ is List<*> -> {
+ if (this.size != other.size)
+ return false
+ this.items.forEachIndexed { index, item ->
+ if (item != other[index]) {
+ return false
+ }
+ }
+ return true
+ }
+
+ else ->
+ return false
+ }
+ }
+
+ private val hashCode by lazy { listHashCode(this) }
+
+ override fun hashCode(): Int = hashCode
+
+ private val stringValue by weak {
+ buildString {
+ append('[')
+ this@ImmutableArrayList.items.forEachIndexed { index, it ->
+ if (index != 0) {
+ append(',')
+ }
+ append(it)
+ }
+ append(']')
+ }
+ }
+
+ override fun toString(): String = stringValue
+
+ internal fun sortedToImmutable(): List =
+ ImmutableArrayList(
+ this.items.clone().apply { sort() }
+ )
+}
diff --git a/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableListBuilder.kt b/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableListBuilder.kt
new file mode 100644
index 0000000..30a2454
--- /dev/null
+++ b/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableListBuilder.kt
@@ -0,0 +1,49 @@
+package io.github.kerubistan.kroki.collections
+
+class ImmutableListBuilder {
+ private var increment : Int = 128
+ private var size : Int = 0
+ private var items : Array = arrayOfNulls(increment)
+
+ fun add(item : T) {
+ if (size >= items.size) {
+ items = items.copyOf(size + increment)
+ }
+ items[size] = item
+ size ++
+ }
+
+ fun addAll(vararg items : T) {
+ if(size + items.size >= this.items.size) {
+ this.items = this.items.copyOf(size + maxOf(items.size, increment))
+ }
+ items.copyInto(this.items, this.size)
+ size += items.size
+ }
+
+ fun addAll(newItems : Iterable) {
+ when(newItems) {
+ is ImmutableArrayList -> {
+ if(size + newItems.items.size >= this.items.size) {
+ this.items = this.items.copyOf(size + maxOf(newItems.items.size, increment))
+ }
+ newItems.items.copyInto(this.items, size)
+ size += newItems.items.size
+ }
+ else ->
+ // this is a bit slow this way, it should be avoided if possible
+ newItems.forEach { add(it) }
+ }
+ }
+
+ fun build() : List =
+ when (size) {
+ 0 -> emptyList()
+ items.size -> {
+ ImmutableArrayList(items as Array)
+ }
+ else -> {
+ ImmutableArrayList((items.copyOf(size)) as Array)
+ }
+ }
+}
\ No newline at end of file
diff --git a/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableSubArrayList.kt b/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableSubArrayList.kt
new file mode 100644
index 0000000..dde7ddb
--- /dev/null
+++ b/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableSubArrayList.kt
@@ -0,0 +1,104 @@
+package io.github.kerubistan.kroki.collections
+
+import io.github.kerubistan.kroki.delegates.weak
+
+/**
+ * Sub-list of an immutable array list.
+ * Since the arrayList is already immutable, the array can be passed over to the sub-list, and it
+ * will give faster access to elements.
+ */
+internal class ImmutableSubArrayList(
+ private val offset: Int,
+ private val limit: Int,
+ private val items: Array
+) : List {
+
+ init {
+ require(offset >= 0) { "offset ($offset) must be greater than 0" }
+ require(limit > offset) { "limit ($limit) must be bigger than the offset ($offset)" }
+ require(limit <= items.size) { "limit ($limit) must be less than or equal to the size of the array (${items.size})" }
+ }
+
+ override val size: Int = limit - offset
+
+ override fun get(index: Int): T {
+ require(index + offset < limit) { "index $index outside of boundaries" }
+ return items[offset + index]
+ }
+
+ override fun isEmpty(): Boolean = false // because limit > offset, for empty list use emptyList
+
+ internal class SubListIterator(
+ private val items: Array,
+ private val offset: Int,
+ private val limit: Int,
+ private var position: Int = offset
+ ) : ListIterator {
+ override fun hasNext(): Boolean = position < limit
+ override fun hasPrevious(): Boolean = position > offset
+
+ override fun next(): T {
+ require(position < limit) { "reached end of the list" }
+ return items[position++]
+ }
+
+ override fun nextIndex(): Int = position + 1
+
+ override fun previous(): T {
+ require(position > offset)
+ return items[position--]
+ }
+
+ override fun previousIndex(): Int = position - 1
+ }
+
+ override fun iterator(): Iterator = SubListIterator(items, offset, limit)
+
+ override fun listIterator(): ListIterator = SubListIterator(items, offset, limit)
+
+ override fun listIterator(index: Int): ListIterator = SubListIterator(items, offset, limit, index)
+
+ override fun subList(fromIndex: Int, toIndex: Int): List =
+ when {
+ toIndex == fromIndex -> emptyList()
+ toIndex < fromIndex -> throw IllegalArgumentException("toIndex ($toIndex) < fromIndex ($fromIndex)")
+ else -> ImmutableSubArrayList(offset + fromIndex, offset + toIndex, items)
+ }
+
+ override fun lastIndexOf(element: T): Int = (offset until limit).last { this[it] == element }
+
+ override fun indexOf(element: T): Int = (offset until limit).first { this[it] == element }
+
+ override fun containsAll(elements: Collection): Boolean = elements.isEmpty() || elements.all { it in this }
+
+ override fun contains(element: T): Boolean = (offset until limit).any { items[it] == element }
+
+ private val stringValue by weak {
+ buildString {
+ append('[')
+ this@ImmutableSubArrayList.forEachIndexed { index, item ->
+ if (index > 0) {
+ append(',')
+ }
+ append(item)
+ }
+ append(']')
+ }
+ }
+
+ override fun toString(): String {
+ return stringValue
+ }
+
+ override fun equals(other: Any?): Boolean {
+ return other != null
+ && other is List<*>
+ && other.size == size
+ && (0 until this.lastIndex).all { index -> this[index] == other[index] }
+ }
+
+ private val hashCode by lazy { listHashCode(this) }
+
+ override fun hashCode() = hashCode
+
+}
\ No newline at end of file
diff --git a/kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/CollectionsKtTest.kt b/kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/CollectionsKtTest.kt
new file mode 100644
index 0000000..eefdb8d
--- /dev/null
+++ b/kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/CollectionsKtTest.kt
@@ -0,0 +1,81 @@
+package io.github.kerubistan.kroki.collections
+
+import io.kotest.matchers.collections.*
+import io.kotest.matchers.should
+import io.kotest.matchers.shouldBe
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+class CollectionsKtTest {
+
+ @Test
+ fun testImmutableListOf() {
+ immutableListOf(1).shouldNotBeEmpty()
+ immutableListOf(1).shouldBeSingleton()
+ immutableListOf().shouldBeEmpty()
+ immutableListOf(1, 2, 3).shouldStartWith(1)
+ immutableListOf(1, 2, 3).shouldEndWith(3)
+ immutableListOf(1, 2, 3).shouldContainAll(1, 2)
+ immutableListOf(1, 2, 3).shouldContainAll(1, 2, 3)
+ immutableListOf(1, 2, 3).shouldNotContainAll(1, 2, 3, 4)
+
+ assertEquals(listOf(), listOf().toImmutableList())
+ assertEquals(listOf("A"), listOf("A").toImmutableList())
+ assertEquals(listOf("A", "B"), listOf("A", "B").toImmutableList())
+ }
+
+ @Test
+ fun buildList() {
+ buildList { } shouldBe emptyList()
+ buildList { add("A") } shouldBe listOf("A")
+ buildList {
+ add("A")
+ add("B")
+ add("C")
+ } shouldBe listOf("A", "B", "C")
+ buildList { repeat(1000) { add(it.toString()) } }.let {
+ it shouldStartWith "0"
+ it shouldEndWith "999"
+ it shouldHaveSize 1000
+ }
+
+ buildList {
+ add("A")
+ addAll("B", "C")
+ add("D")
+ } shouldBe immutableListOf("A", "B", "C", "D")
+
+ buildList {
+ repeat(128) {
+ addAll("A", "B", "C", "D", "E", "F", "G", "H")
+ }
+ }.let {
+ it shouldStartWith "A"
+ it shouldEndWith "H"
+ it shouldHaveSize 8 * 128
+ }
+
+ buildList {
+ val list = immutableListOf("A", "B", "C", "D", "E", "F", "G", "H")
+ repeat(128) {
+ addAll(list)
+ }
+ }.let {
+ it shouldStartWith "A"
+ it shouldEndWith "H"
+ it shouldHaveSize (8 * 128)
+ }
+
+ buildList {
+ val list = listOf("A", "B", "C", "D", "E", "F", "G", "H")
+ repeat(128) {
+ addAll(list)
+ }
+ }.let {
+ it shouldStartWith "A"
+ it shouldEndWith "H"
+ it shouldHaveSize (8 * 128)
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/ImmutableArrayListTest.kt b/kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/ImmutableArrayListTest.kt
new file mode 100644
index 0000000..234a7b1
--- /dev/null
+++ b/kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/ImmutableArrayListTest.kt
@@ -0,0 +1,140 @@
+package io.github.kerubistan.kroki.collections
+
+import org.junit.jupiter.api.Assertions.assertNotEquals
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import java.util.ArrayList
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+
+class ImmutableArrayListTest {
+
+ @Test
+ fun equals() {
+ assertEquals(immutableListOf("A", "B", "C"), immutableListOf("A", "B", "C"))
+ assertEquals(immutableListOf("A", "B", "C"), listOf("A", "B", "C"))
+ assertNotEquals(immutableListOf("A", "B", "C"), listOf("A", "B", "C", "D"))
+ assertNotEquals(immutableListOf("A", "B", "C", "D"), listOf("A", "B", "C"))
+ assertNotEquals(immutableListOf("A", "B", "C"), listOf("A", "D", "C"))
+ assertNotEquals(immutableListOf("A", "B", "C"), setOf("A", "D", "C"))
+ }
+
+ @Test
+ fun size() {
+ assertEquals(2, ImmutableArrayList(arrayOf("A", "B")).size)
+ }
+
+ @Test
+ fun isEmpty() {
+ assertFalse {
+ ImmutableArrayList(arrayOf("A", "B")).isEmpty()
+ }
+ }
+
+ @Test
+ fun indexOf() {
+ assertEquals(1, ImmutableArrayList(arrayOf("A", "B", "C")).indexOf("B"))
+ assertEquals(-1, ImmutableArrayList(arrayOf("A", "B", "C")).indexOf("D"))
+ }
+
+ @Test
+ fun iterator() {
+ val iterator = ImmutableArrayList(arrayOf("A", "B", "C")).iterator()
+ assertTrue { iterator.hasNext() }
+ assertEquals("A", iterator.next())
+ assertTrue { iterator.hasNext() }
+ assertEquals("B", iterator.next())
+ assertTrue { iterator.hasNext() }
+ assertEquals("C", iterator.next())
+ assertFalse { iterator.hasNext() }
+ }
+
+ @Test
+ fun listIterator() {
+ val listIterator = ImmutableArrayList(arrayOf("A", "B", "C")).listIterator()
+ assertTrue { listIterator.hasNext() }
+ assertFalse { listIterator.hasPrevious() }
+ assertEquals("A", listIterator.next())
+ assertTrue { listIterator.hasNext() }
+ assertTrue { listIterator.hasPrevious() }
+ assertEquals("A", listIterator.previous())
+ assertTrue { listIterator.hasNext() }
+ assertFalse { listIterator.hasPrevious() }
+ }
+
+ @Test
+ fun listIteratorWithIndex() {
+ val list = immutableListOf("A", "B", "C", "D")
+ val iterator = list.listIterator(1)
+ assertTrue(iterator.hasNext())
+ assertEquals("B", iterator.next())
+ assertTrue(iterator.hasNext())
+ assertEquals("C", iterator.next())
+ assertTrue(iterator.hasNext())
+ assertEquals("D", iterator.next())
+ assertFalse (iterator.hasNext())
+ assertThrows { iterator.next() }
+ }
+
+ @Test
+ fun subList() {
+ assertEquals(emptyList(), immutableListOf(1, 2, 3).subList(0, 0))
+ assertEquals(listOf(1), immutableListOf(1, 2, 3).subList(0, 1))
+ assertEquals(listOf(2), immutableListOf(1, 2, 3).subList(1, 2))
+ // more in the sublist tests
+ }
+
+ @Test
+ fun testToString() {
+ assertEquals("[A,B,C]", ImmutableArrayList(arrayOf("A", "B", "C")).toString())
+ }
+
+ @Test
+ fun contains() {
+ assertTrue { ImmutableArrayList(arrayOf("A", "B", "C")).contains("C") }
+ assertFalse { ImmutableArrayList(arrayOf("A", "B", "C")).contains("D") }
+ }
+
+ @Test
+ fun containsAll() {
+ assertTrue { ImmutableArrayList(arrayOf("A", "B", "C")).containsAll(listOf("A", "B", "C")) }
+ assertTrue { ImmutableArrayList(arrayOf("A", "B", "C")).containsAll(listOf("A", "B")) }
+ assertFalse { ImmutableArrayList(arrayOf("A", "B", "C")).containsAll(listOf("A", "B", "C", "D")) }
+ assertTrue { ImmutableArrayList(arrayOf("A", "B", "C")).containsAll(listOf()) }
+ }
+
+ @Test
+ fun lastIndexOf() {
+ assertEquals(-1, ImmutableArrayList.of("A", "B", "A").lastIndexOf("C"))
+ assertEquals(2, ImmutableArrayList.of("A", "B", "A").lastIndexOf("A"))
+ }
+
+ @Test
+ fun get() {
+ assertEquals("A", immutableListOf("A", "B", "C")[0])
+ assertEquals("B", immutableListOf("A", "B", "C")[1])
+ assertThrows {
+ immutableListOf("A", "B", "C")[3]
+ }
+ }
+
+ @Test
+ fun testEquals() {
+ assertEquals(immutableListOf(1,2,3), immutableListOf(1,2,3))
+ assertEquals(immutableListOf(1,2,3), listOf(1,2,3))
+ assertEquals(immutableListOf(1,2,3), arrayListOf(1,2,3))
+
+ assertNotEquals(immutableListOf(1,2,3), setOf(1,2,3))
+ assertNotEquals(immutableListOf(1,2,3), emptyList())
+ assertNotEquals(immutableListOf(1,2,3), "")
+ }
+
+ @Test
+ fun testHashCode() {
+ // when two objects are equal, their hashcode should also be equal
+ assertEquals(immutableListOf("A", "B").hashCode(), ArrayList(listOf("A", "B")).hashCode())
+ assertEquals(immutableListOf("A", "B").hashCode(), arrayListOf("A", "B").hashCode())
+ }
+}
diff --git a/kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/ImmutableSubArrayListTest.kt b/kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/ImmutableSubArrayListTest.kt
new file mode 100644
index 0000000..cbe2bb4
--- /dev/null
+++ b/kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/ImmutableSubArrayListTest.kt
@@ -0,0 +1,155 @@
+package io.github.kerubistan.kroki.collections
+
+import io.kotest.assertions.throwables.shouldThrowAny
+import io.kotest.assertions.withClue
+import io.kotest.matchers.shouldBe
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+
+class ImmutableSubArrayListTest {
+
+ @Test
+ fun validations() {
+ assertThrows {
+ immutableListOf(1, 2, 3).subList(-1, 2)
+ }
+ assertThrows {
+ immutableListOf(1, 2, 3).subList(1, 5)
+ }
+ assertThrows {
+ immutableListOf(1, 2, 3).subList(1, 0)
+ }
+ }
+
+ @Test
+ fun getSize() {
+ assertEquals(1, immutableListOf(1,2,3).subList(0,1).size)
+ // this is not even done by a sublist, just emptyList
+ assertEquals(0, immutableListOf(1,2,3).subList(0,0).size)
+ }
+
+ @Test
+ fun get() {
+ kotlin.test.assertEquals(0, immutableListOf(0, 1, 2).subList(0, 1)[0])
+ kotlin.test.assertEquals(1, immutableListOf(0, 1, 2).subList(0, 2)[1])
+ }
+
+ @Test
+ fun isEmpty() {
+ assertFalse(immutableListOf("A", "B", "C").subList(1, 3).isEmpty())
+ assertTrue(immutableListOf("A", "B", "C").subList(3, 3).isEmpty())
+ }
+
+ @Test
+ operator fun iterator() {
+ immutableListOf(0,1,2,3).subList(1,4).iterator().let {
+ assertTrue(it.hasNext())
+ assertEquals(1, it.next())
+ assertTrue(it.hasNext())
+ assertEquals(2, it.next())
+ assertTrue(it.hasNext())
+ assertEquals(3, it.next())
+
+ withClue("only 3 items in the list, no more left") {
+ assertFalse(it.hasNext())
+ }
+
+ withClue("There is no next element, the call to next should now fail") {
+ assertThrows { it.next() }
+ }
+ }
+ }
+
+ @Test
+ fun listIterator() {
+ immutableListOf(0,1,2,3).subList(1,4).listIterator().let {
+ assertFalse(it.hasPrevious())
+ assertThrows { it.previous() }
+ assertTrue(it.hasNext())
+ assertEquals(1, it.next())
+ assertTrue(it.hasPrevious())
+ assertTrue(it.hasNext())
+ }
+ }
+
+ @Test
+ fun testListIterator() {
+ immutableListOf(1,2,3).listIterator().let {
+ it.hasPrevious() shouldBe false
+ shouldThrowAny {
+ it.previous()
+ }
+ it.hasNext() shouldBe true
+ it.next() shouldBe 1
+
+ it.hasPrevious() shouldBe true
+ it.hasNext() shouldBe true
+ it.next() shouldBe 2
+
+ it.hasPrevious() shouldBe true
+ it.hasNext() shouldBe true
+ it.next() shouldBe 3
+
+ it.hasPrevious() shouldBe true
+ it.hasNext() shouldBe false
+ shouldThrowAny {
+ it.next()
+ }
+ }
+ }
+
+ @Test
+ fun subList() {
+ assertEquals(
+ listOf("C", "D"),
+ immutableListOf("A", "B", "C", "D").subList(1, 4).subList(1, 3)
+ )
+ }
+
+ @Test
+ fun lastIndexOf() {
+ immutableListOf(1,2,3,4).lastIndexOf(3) shouldBe 2
+ immutableListOf(1,2,3,3).lastIndexOf(3) shouldBe 3
+ immutableListOf(1,2,3,3).lastIndexOf(4) shouldBe -1
+ }
+
+ @Test
+ fun indexOf() {
+ immutableListOf(1,2,3,4).indexOf(3) shouldBe 2
+ immutableListOf(1,2,3,3).indexOf(3) shouldBe 2
+ immutableListOf(1,2,3,3).indexOf(0) shouldBe -1
+ }
+
+ @Test
+ fun containsAll() {
+ assertTrue { immutableListOf(0,1,2,3,4).subList(1,3).containsAll(setOf(1,2)) }
+ }
+
+ @Test
+ fun contains() {
+ assertTrue { immutableListOf(0,1,2,3,4).subList(0,2).contains(0) }
+ assertTrue { immutableListOf(0,1,2,3,4).subList(0,2).contains(1) }
+ assertTrue { immutableListOf(0,1,2,3,4).subList(1,3).contains(1) }
+ }
+
+ @Test
+ fun testToString() {
+ assertEquals("[A,B,C]", immutableListOf("A", "B", "C").subList(0,3).toString())
+ }
+
+ @Test
+ fun testHashCode() {
+ assertEquals(
+ listOf(1,2,3,4).hashCode(),
+ immutableListOf(0,1,2,3,4).subList(1,5).hashCode()
+ )
+ }
+
+ @Test
+ fun testEquals() {
+ assertTrue(immutableListOf(0,1,2,3).subList(0,0) == emptyList())
+ assertTrue(immutableListOf(0,1,2,3).subList(0,1) == listOf(0))
+ }
+
+}
\ No newline at end of file
diff --git a/kroki-jmh/pom.xml b/kroki-jmh/pom.xml
index 1ba6608..5fb31f9 100644
--- a/kroki-jmh/pom.xml
+++ b/kroki-jmh/pom.xml
@@ -30,6 +30,11 @@
kroki-utils
1.24-SNAPSHOT
+
+ io.github.kerubistan.kroki
+ kroki-collections
+ 1.24-SNAPSHOT
+
io.github.kerubistan.kroki
kroki-coroutines
@@ -60,6 +65,11 @@
jackson-databind
${jackson.version}
+
+ com.google.guava
+ guava
+ 32.1.3-jre
+
diff --git a/kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ImmutableArrayListBenchmark.kt b/kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ImmutableArrayListBenchmark.kt
new file mode 100644
index 0000000..3704216
--- /dev/null
+++ b/kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ImmutableArrayListBenchmark.kt
@@ -0,0 +1,95 @@
+package io.github.kerubistan.kroki.benchmark.collections
+
+import com.google.common.collect.ImmutableList
+import io.github.kerubistan.kroki.collections.immutableListOf
+import io.github.kerubistan.kroki.collections.immutableSorted
+import org.openjdk.jmh.annotations.Benchmark
+import org.openjdk.jmh.annotations.Param
+import org.openjdk.jmh.annotations.Scope
+import org.openjdk.jmh.annotations.Setup
+import org.openjdk.jmh.annotations.State
+import org.openjdk.jmh.infra.Blackhole
+import java.util.*
+
+@State(Scope.Benchmark)
+open class ImmutableArrayListBenchmark {
+
+ @Param("1", "16", "1024", "4096")
+ var size: Int = 0
+
+ @Param("arraylist", "immutablearraylist", "guava")
+ var type: String = "arraylist"
+
+ lateinit var list: List
+
+ @Setup
+ fun setup() {
+ val rawList: MutableList = ArrayList(size)
+ for (i in 0..size) {
+ rawList.add(i.toString())
+ }
+ rawList.shuffle()
+ when (type) {
+ "arraylist" -> {
+ list = ArrayList(rawList)
+ }
+
+ "immutablearraylist" -> {
+ list = immutableListOf(*rawList.toTypedArray())
+ }
+ "guava" -> {
+ list = ImmutableList.builder().addAll(rawList).build()
+ }
+ }
+ }
+
+ @Benchmark
+ fun iterateWithForEach(blackhole: Blackhole) {
+ list.forEach(blackhole::consume)
+ }
+
+ @Benchmark
+ fun iterateWithForLoop(blackhole: Blackhole) {
+ for (item in list) {
+ blackhole.consume(item)
+ }
+ }
+
+ /**
+ * An example of what a faster list could do.
+ */
+ @Benchmark
+ fun map(blackhole: Blackhole) {
+ blackhole.consume(list.map { it.lowercase() })
+ }
+
+ @Benchmark
+ fun callToString(blackhole: Blackhole) {
+ blackhole.consume(list.toString())
+ }
+
+ @Benchmark
+ fun first(blackhole: Blackhole) {
+ blackhole.consume(list.first())
+ }
+
+ @Benchmark
+ fun sorted(blackhole: Blackhole) {
+ blackhole.consume(list.sorted())
+ }
+
+ @Benchmark
+ fun immutableSorted(blackhole: Blackhole) {
+ blackhole.consume(list.immutableSorted())
+ }
+
+ @Benchmark
+ fun max(blackhole: Blackhole) {
+ blackhole.consume(list.max())
+ }
+
+ @Benchmark
+ fun callHashCode(blackhole: Blackhole) {
+ blackhole.consume(list.hashCode())
+ }
+}
\ No newline at end of file
diff --git a/kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ImmutableSubArrayListBenchmark.kt b/kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ImmutableSubArrayListBenchmark.kt
new file mode 100644
index 0000000..aa24f5f
--- /dev/null
+++ b/kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ImmutableSubArrayListBenchmark.kt
@@ -0,0 +1,33 @@
+package io.github.kerubistan.kroki.benchmark.collections
+
+import io.github.kerubistan.kroki.collections.immutableListOf
+import org.openjdk.jmh.annotations.Benchmark
+import org.openjdk.jmh.annotations.Param
+import org.openjdk.jmh.annotations.Scope
+import org.openjdk.jmh.annotations.Setup
+import org.openjdk.jmh.annotations.State
+import org.openjdk.jmh.infra.Blackhole
+
+@State(Scope.Benchmark)
+open class ImmutableSubArrayListBenchmark {
+
+ @Param("immutable", "list")
+ var type = "list"
+
+ lateinit var list : List
+
+ @Setup
+ fun setup() {
+ list = when(type) {
+ "immutable" -> immutableListOf( *((1..100).map { it.toString() }.toList().toTypedArray()) )
+ "list" -> listOf( *((1..100).map { it.toString() }.toList().toTypedArray()) )
+ else -> throw IllegalArgumentException("immutable or list")
+ }
+ }
+
+ @Benchmark
+ fun subList(blackhole: Blackhole) {
+ blackhole.consume(list.subList(1, 10))
+ }
+
+}
\ No newline at end of file
diff --git a/kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ListBuilderBenchmark.kt b/kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ListBuilderBenchmark.kt
new file mode 100644
index 0000000..7205bc2
--- /dev/null
+++ b/kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ListBuilderBenchmark.kt
@@ -0,0 +1,38 @@
+package io.github.kerubistan.kroki.benchmark.collections
+
+import com.google.common.collect.ImmutableList
+import org.openjdk.jmh.annotations.Benchmark
+import org.openjdk.jmh.annotations.Param
+import org.openjdk.jmh.annotations.Scope
+import org.openjdk.jmh.annotations.State
+import org.openjdk.jmh.infra.Blackhole
+
+@State(Scope.Benchmark)
+open class ListBuilderBenchmark {
+
+ @Param("0", "1", "2", "16", "1024", "4096")
+ var size = 0
+
+ @Benchmark
+ fun buildImmutableArrayList(blackhole: Blackhole) {
+ blackhole.consume(
+ io.github.kerubistan.kroki.collections.buildList {
+ repeat(size) {
+ add(it)
+ }
+ }
+ )
+ }
+
+ @Benchmark
+ fun buildGuavaImmutableList(blackhole: Blackhole) {
+ blackhole.consume(
+ ImmutableList.builder().apply {
+ repeat(size) {
+ add(it)
+ }
+ }.build()
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 48d54da..a850423 100644
--- a/pom.xml
+++ b/pom.xml
@@ -224,6 +224,7 @@
kroki-utils
kroki-jmh
kroki-coroutines
+ kroki-collections
kroki-xml
kroki-flyweight
kroki-jdbc