From 86e05d490168cf4e27a4555125b49b619ce4120b Mon Sep 17 00:00:00 2001 From: Laszlo Hornyak Date: Thu, 26 Sep 2024 07:19:14 +0200 Subject: [PATCH 01/11] first implementation of immutableArrayList Signed-off-by: Laszlo Hornyak --- kroki-collections/pom.xml | 67 +++++++++ .../kroki/collections/Collections.kt | 8 + .../kroki/collections/ImmutableArrayList.kt | 110 ++++++++++++++ .../collections/ImmutableSubArrayList.kt | 99 +++++++++++++ .../collections/ImmutableArrayListTest.kt | 138 ++++++++++++++++++ .../collections/ImmutableSubArrayListTest.kt | 81 ++++++++++ kroki-jmh/pom.xml | 5 + .../ImmutableArrayListBenchmark.kt | 65 +++++++++ .../ImmutableSubArrayListBenchmark.kt | 9 ++ pom.xml | 1 + 10 files changed, 583 insertions(+) create mode 100644 kroki-collections/pom.xml create mode 100644 kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/Collections.kt create mode 100644 kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableArrayList.kt create mode 100644 kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableSubArrayList.kt create mode 100644 kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/ImmutableArrayListTest.kt create mode 100644 kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/ImmutableSubArrayListTest.kt create mode 100644 kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ImmutableArrayListBenchmark.kt create mode 100644 kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ImmutableSubArrayListBenchmark.kt diff --git a/kroki-collections/pom.xml b/kroki-collections/pom.xml new file mode 100644 index 00000000..2e2a7f0c --- /dev/null +++ b/kroki-collections/pom.xml @@ -0,0 +1,67 @@ + + + + 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 + + + + + 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 00000000..952d39c0 --- /dev/null +++ b/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/Collections.kt @@ -0,0 +1,8 @@ +package io.github.kerubistan.kroki.collections + +fun immutableListOf(vararg items: T): List = + when (items.size) { + 0 -> emptyList() + 1 -> listOf(items.first()) + else -> ImmutableArrayList(items) + } 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 00000000..433c1c6c --- /dev/null +++ b/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableArrayList.kt @@ -0,0 +1,110 @@ +package io.github.kerubistan.kroki.collections + +import io.github.kerubistan.kroki.delegates.weak + +internal class ImmutableArrayList() : List { + + private 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 = items[index] + + 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 = items[index++] + + override fun nextIndex(): Int = index + 1 + + override fun previous(): T = 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 == 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 { + var hashCode = 1 + this.forEach { hashCode = (hashCode * 31) + it.hashCode() } + hashCode + } + + 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 +} 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 00000000..a1cfb30f --- /dev/null +++ b/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableSubArrayList.kt @@ -0,0 +1,99 @@ +package io.github.kerubistan.kroki.collections + +import io.github.kerubistan.kroki.delegates.weak + +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 must be bigger than the offset" } + require(limit <= items.size + 1) { "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 = + if (fromIndex == toIndex) + emptyList() + else + ImmutableSubArrayList(offset + fromIndex, limit - 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] } + } + + override fun hashCode(): Int { + var hashCode = 1 + items.forEach { hashCode = (hashCode * 31) + it.hashCode() } + return hashCode + } +} \ 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 00000000..8bcd1269 --- /dev/null +++ b/kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/ImmutableArrayListTest.kt @@ -0,0 +1,138 @@ +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)) + // 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", ImmutableArrayList.of("A", "B", "C")[0]) + assertEquals("B", ImmutableArrayList.of("A", "B", "C")[1]) + assertThrows { + ImmutableArrayList.of("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 00000000..14ce4067 --- /dev/null +++ b/kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/ImmutableSubArrayListTest.kt @@ -0,0 +1,81 @@ +package io.github.kerubistan.kroki.collections + +import org.junit.jupiter.api.Test + +import org.junit.jupiter.api.Assertions.* +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() { + } + + @Test + operator fun iterator() { + } + + @Test + fun listIterator() { + } + + @Test + fun testListIterator() { + } + + @Test + fun subList() { + } + + @Test + fun lastIndexOf() { + } + + @Test + fun indexOf() { + } + + @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()) + } + +} \ No newline at end of file diff --git a/kroki-jmh/pom.xml b/kroki-jmh/pom.xml index 1ba6608a..cf829943 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 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 00000000..31815512 --- /dev/null +++ b/kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ImmutableArrayListBenchmark.kt @@ -0,0 +1,65 @@ +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 +import java.util.* + +@State(Scope.Benchmark) +open class ImmutableArrayListBenchmark { + + @Param("1", "16", "1024", "4096") + var size: Int = 0 + + @Param("arraylist", "immutablearraylist") + 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()) + } + when (type) { + "arraylist" -> { + list = ArrayList(rawList) + } + + "immutablearraylist" -> { + list = immutableListOf(*rawList.toTypedArray()) + } + } + } + + @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()) + } + +} \ 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 00000000..ae04e9ea --- /dev/null +++ b/kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ImmutableSubArrayListBenchmark.kt @@ -0,0 +1,9 @@ +package io.github.kerubistan.kroki.benchmark.collections + +import org.openjdk.jmh.annotations.Scope +import org.openjdk.jmh.annotations.State + +@State(Scope.Benchmark) +open class ImmutableSubArrayListBenchmark { + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 48d54dae..a850423d 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 From be3c9aceac46c01f1adeca70de2c7fa74f39201f Mon Sep 17 00:00:00 2001 From: Laszlo Hornyak Date: Thu, 3 Oct 2024 23:34:16 +0200 Subject: [PATCH 02/11] implementation of sublists, tests Signed-off-by: Laszlo Hornyak --- .../kroki/collections/Collections.kt | 14 ++++++- .../kroki/collections/ImmutableArrayList.kt | 15 +++---- .../collections/ImmutableSubArrayList.kt | 27 +++++++----- .../kroki/collections/CollectionsKtTest.kt | 14 +++++++ .../collections/ImmutableArrayListTest.kt | 2 + .../collections/ImmutableSubArrayListTest.kt | 42 ++++++++++++++++++- 6 files changed, 91 insertions(+), 23 deletions(-) create mode 100644 kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/CollectionsKtTest.kt 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 index 952d39c0..00ebe2ba 100644 --- 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 @@ -3,6 +3,18 @@ package io.github.kerubistan.kroki.collections fun immutableListOf(vararg items: T): List = when (items.size) { 0 -> emptyList() - 1 -> listOf(items.first()) + 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 +} \ No newline at end of file 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 index 433c1c6c..ecd84e31 100644 --- 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 @@ -2,6 +2,9 @@ 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 { private lateinit var items: Array @@ -50,10 +53,8 @@ internal class ImmutableArrayList() : List { override fun subList(fromIndex: Int, toIndex: Int): List = when { - toIndex == fromIndex -> { - emptyList() - } - + toIndex == fromIndex -> emptyList() + fromIndex == toIndex - 1 -> listOf(this.items[fromIndex]) fromIndex == 0 && toIndex == size + 1 -> this else -> ImmutableSubArrayList(fromIndex, toIndex, this.items) } @@ -85,11 +86,7 @@ internal class ImmutableArrayList() : List { } } - private val hashCode by lazy { - var hashCode = 1 - this.forEach { hashCode = (hashCode * 31) + it.hashCode() } - hashCode - } + private val hashCode by lazy { listHashCode(this) } override fun hashCode(): Int = hashCode 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 index a1cfb30f..dde7ddbf 100644 --- 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 @@ -2,6 +2,11 @@ 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, @@ -10,8 +15,8 @@ internal class ImmutableSubArrayList( init { require(offset >= 0) { "offset ($offset) must be greater than 0" } - require(limit > offset) { "limit must be bigger than the offset" } - require(limit <= items.size + 1) { "limit ($limit) must be less than or equal to the size of the array (${items.size})" } + 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 @@ -54,10 +59,11 @@ internal class ImmutableSubArrayList( override fun listIterator(index: Int): ListIterator = SubListIterator(items, offset, limit, index) override fun subList(fromIndex: Int, toIndex: Int): List = - if (fromIndex == toIndex) - emptyList() - else - ImmutableSubArrayList(offset + fromIndex, limit - toIndex, items) + 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 } @@ -91,9 +97,8 @@ internal class ImmutableSubArrayList( && (0 until this.lastIndex).all { index -> this[index] == other[index] } } - override fun hashCode(): Int { - var hashCode = 1 - items.forEach { hashCode = (hashCode * 31) + it.hashCode() } - return hashCode - } + 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 00000000..00c6d600 --- /dev/null +++ b/kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/CollectionsKtTest.kt @@ -0,0 +1,14 @@ +package io.github.kerubistan.kroki.collections + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class CollectionsKtTest { + + @Test + fun immutableListOf() { + assertEquals(listOf(), listOf().toImmutableList()) + assertEquals(listOf("A"), listOf("A").toImmutableList()) + assertEquals(listOf("A", "B"), listOf("A", "B").toImmutableList()) + } +} \ 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 index 8bcd1269..45ac7a0b 100644 --- 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 @@ -81,6 +81,8 @@ class ImmutableArrayListTest { @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 } 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 index 14ce4067..23301b1a 100644 --- 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 @@ -1,8 +1,7 @@ package io.github.kerubistan.kroki.collections -import org.junit.jupiter.api.Test - import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows class ImmutableSubArrayListTest { @@ -35,14 +34,35 @@ class ImmutableSubArrayListTest { @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()) + + assertFalse(it.hasNext()) + 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 @@ -51,6 +71,10 @@ class ImmutableSubArrayListTest { @Test fun subList() { + assertEquals( + listOf("C", "D"), + immutableListOf("A", "B", "C", "D").subList(1, 4).subList(1, 3) + ) } @Test @@ -78,4 +102,18 @@ class ImmutableSubArrayListTest { 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 From d81f3219b1a71cb260d2f4eebdfe879ca918759c Mon Sep 17 00:00:00 2001 From: Laszlo Hornyak Date: Mon, 7 Oct 2024 22:19:12 +0200 Subject: [PATCH 03/11] new tests with kotest Signed-off-by: Laszlo Hornyak --- kroki-collections/pom.xml | 5 +++ .../kroki/collections/ImmutableArrayList.kt | 19 +++++++-- .../collections/ImmutableArrayListTest.kt | 10 ++--- .../collections/ImmutableSubArrayListTest.kt | 40 ++++++++++++++++++- .../ImmutableSubArrayListBenchmark.kt | 24 +++++++++++ 5 files changed, 88 insertions(+), 10 deletions(-) diff --git a/kroki-collections/pom.xml b/kroki-collections/pom.xml index 2e2a7f0c..dd17d652 100644 --- a/kroki-collections/pom.xml +++ b/kroki-collections/pom.xml @@ -44,6 +44,11 @@ kroki-delegates 1.24-SNAPSHOT + + io.kotest + kotest-assertions-core-jvm + 5.9.1 + 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 index ecd84e31..8883d572 100644 --- 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 @@ -20,7 +20,13 @@ internal class ImmutableArrayList() : List { override val size: Int get() = items.size - override fun get(index: Int): T = items[index] + 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 @@ -36,11 +42,18 @@ internal class ImmutableArrayList() : List { override fun hasNext(): Boolean = items.size > index override fun hasPrevious(): Boolean = index > 0 - override fun next(): T = items[index++] + override fun next(): T { + require(index < items.size) { "list size is ${items.size}, there is no next" } + return items[index++] + } override fun nextIndex(): Int = index + 1 - override fun previous(): T = items[--index] + 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 } 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 index 45ac7a0b..234a7b14 100644 --- 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 @@ -75,7 +75,7 @@ class ImmutableArrayListTest { assertTrue(iterator.hasNext()) assertEquals("D", iterator.next()) assertFalse (iterator.hasNext()) - assertThrows { iterator.next() } + assertThrows { iterator.next() } } @Test @@ -113,10 +113,10 @@ class ImmutableArrayListTest { @Test fun get() { - assertEquals("A", ImmutableArrayList.of("A", "B", "C")[0]) - assertEquals("B", ImmutableArrayList.of("A", "B", "C")[1]) - assertThrows { - ImmutableArrayList.of("A", "B", "C")[3] + assertEquals("A", immutableListOf("A", "B", "C")[0]) + assertEquals("B", immutableListOf("A", "B", "C")[1]) + assertThrows { + immutableListOf("A", "B", "C")[3] } } 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 index 23301b1a..cbe2bb44 100644 --- 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 @@ -1,5 +1,8 @@ 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 @@ -48,8 +51,13 @@ class ImmutableSubArrayListTest { assertTrue(it.hasNext()) assertEquals(3, it.next()) - assertFalse(it.hasNext()) - assertThrows { 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() } + } } } @@ -67,6 +75,28 @@ class ImmutableSubArrayListTest { @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 @@ -79,10 +109,16 @@ class ImmutableSubArrayListTest { @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 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 index ae04e9ea..aa24f5fa 100644 --- 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 @@ -1,9 +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 From 0b6f736cdcf48605cf4e597aa40d6cbab522438f Mon Sep 17 00:00:00 2001 From: Laszlo Hornyak Date: Mon, 7 Oct 2024 22:42:02 +0200 Subject: [PATCH 04/11] new tests with kotest Signed-off-by: Laszlo Hornyak --- .../kroki/collections/CollectionsKtTest.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) 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 index 00c6d600..fd3b7fdf 100644 --- 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 @@ -1,12 +1,29 @@ package io.github.kerubistan.kroki.collections +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldBeSingleton +import io.kotest.matchers.collections.shouldContainAll +import io.kotest.matchers.collections.shouldEndWith +import io.kotest.matchers.collections.shouldNotBeEmpty +import io.kotest.matchers.collections.shouldNotContainAll +import io.kotest.matchers.collections.shouldStartWith +import io.kotest.matchers.should import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test class CollectionsKtTest { @Test - fun immutableListOf() { + 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()) From 9d6a745630ead8c0ec9ea46c68429ee1d7b175f5 Mon Sep 17 00:00:00 2001 From: Laszlo Hornyak Date: Mon, 7 Oct 2024 23:15:06 +0200 Subject: [PATCH 05/11] immutable list sort Signed-off-by: Laszlo Hornyak --- .../kerubistan/kroki/collections/Collections.kt | 12 +++++++++--- .../kroki/collections/ImmutableArrayList.kt | 7 ++++++- .../collections/ImmutableArrayListBenchmark.kt | 16 ++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) 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 index 00ebe2ba..e12ce702 100644 --- 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 @@ -7,14 +7,20 @@ fun immutableListOf(vararg items: T): List = else -> ImmutableArrayList(items) } -inline fun List.toImmutableList() : List = when { +inline fun List.toImmutableList(): List = when { this.isEmpty() -> emptyList() this.size == 1 -> listOf(first()) else -> immutableListOf(*(this.toTypedArray())) } -internal fun listHashCode(list : List): Int { +internal fun listHashCode(list: List): Int { var hashCode = 1 list.forEach { hashCode = (hashCode * 31) + it.hashCode() } return hashCode -} \ No newline at end of file +} + +fun > List.immutableSorted(): List = + when (this) { + is ImmutableArrayList -> this.sortedToImmutable() + else -> this.sorted() + } 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 index 8883d572..56deb3a0 100644 --- 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 @@ -23,7 +23,7 @@ internal class ImmutableArrayList() : List { override fun get(index: Int): T { try { return items[index] - } catch (aiob : ArrayIndexOutOfBoundsException) { + } catch (aiob: ArrayIndexOutOfBoundsException) { throw IllegalArgumentException("size is ${items.size}, requested index is $index", aiob) } } @@ -117,4 +117,9 @@ internal class ImmutableArrayList() : List { } override fun toString(): String = stringValue + + internal fun sortedToImmutable(): List = + ImmutableArrayList( + this.items.clone().apply { sort() } + ) } 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 index 31815512..fdb89926 100644 --- 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 @@ -1,6 +1,7 @@ package io.github.kerubistan.kroki.benchmark.collections 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 @@ -62,4 +63,19 @@ open class ImmutableArrayListBenchmark { 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()) + } + } \ No newline at end of file From e1789afd6b5640b9c208979b7732c28402feb5db Mon Sep 17 00:00:00 2001 From: Laszlo Hornyak Date: Tue, 8 Oct 2024 07:29:46 +0200 Subject: [PATCH 06/11] immutable list benchmark: shuffle the list Signed-off-by: Laszlo Hornyak --- .../kroki/benchmark/collections/ImmutableArrayListBenchmark.kt | 1 + 1 file changed, 1 insertion(+) 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 index fdb89926..cd7a170c 100644 --- 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 @@ -27,6 +27,7 @@ open class ImmutableArrayListBenchmark { for (i in 0..size) { rawList.add(i.toString()) } + rawList.shuffle() when (type) { "arraylist" -> { list = ArrayList(rawList) From f78ecee852de15f07583df859a0031faad9b81ff Mon Sep 17 00:00:00 2001 From: Laszlo Hornyak Date: Thu, 10 Oct 2024 21:17:20 +0200 Subject: [PATCH 07/11] immutable list benchmark: guava added Signed-off-by: Laszlo Hornyak --- kroki-jmh/pom.xml | 5 +++++ .../collections/ImmutableArrayListBenchmark.kt | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/kroki-jmh/pom.xml b/kroki-jmh/pom.xml index cf829943..5fb31f97 100644 --- a/kroki-jmh/pom.xml +++ b/kroki-jmh/pom.xml @@ -65,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 index cd7a170c..3704216c 100644 --- 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 @@ -1,5 +1,6 @@ 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 @@ -16,7 +17,7 @@ open class ImmutableArrayListBenchmark { @Param("1", "16", "1024", "4096") var size: Int = 0 - @Param("arraylist", "immutablearraylist") + @Param("arraylist", "immutablearraylist", "guava") var type: String = "arraylist" lateinit var list: List @@ -36,6 +37,9 @@ open class ImmutableArrayListBenchmark { "immutablearraylist" -> { list = immutableListOf(*rawList.toTypedArray()) } + "guava" -> { + list = ImmutableList.builder().addAll(rawList).build() + } } } @@ -79,4 +83,13 @@ open class ImmutableArrayListBenchmark { 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 From 7972f0c0092c7db864b6ce89fdfaaa3f00ad6dde Mon Sep 17 00:00:00 2001 From: Laszlo Hornyak Date: Fri, 11 Oct 2024 19:52:04 +0200 Subject: [PATCH 08/11] immutable list iterator: checking replaced with try, gives a better performance Signed-off-by: Laszlo Hornyak --- .../kerubistan/kroki/collections/ImmutableArrayList.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 index 56deb3a0..f79c5ded 100644 --- 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 @@ -43,8 +43,13 @@ internal class ImmutableArrayList() : List { override fun hasPrevious(): Boolean = index > 0 override fun next(): T { - require(index < items.size) { "list size is ${items.size}, there is no next" } - return items[index++] + 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 From 8ba540c2a675cd9b46c5a580245cb85d218055f5 Mon Sep 17 00:00:00 2001 From: Laszlo Hornyak Date: Sat, 12 Oct 2024 23:37:01 +0200 Subject: [PATCH 09/11] immutable list builder Signed-off-by: Laszlo Hornyak --- .../kroki/collections/Collections.kt | 3 ++ .../kroki/collections/ImmutableListBuilder.kt | 26 +++++++++++++ .../kroki/collections/CollectionsKtTest.kt | 25 ++++++++---- .../collections/ListBuilderBenchmark.kt | 38 +++++++++++++++++++ 4 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableListBuilder.kt create mode 100644 kroki-jmh/src/main/kotlin/io/github/kerubistan/kroki/benchmark/collections/ListBuilderBenchmark.kt 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 index e12ce702..ba3d2491 100644 --- 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 @@ -24,3 +24,6 @@ fun > List.immutableSorted(): List = 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/ImmutableListBuilder.kt b/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableListBuilder.kt new file mode 100644 index 00000000..b7be5a81 --- /dev/null +++ b/kroki-collections/src/main/kotlin/io/github/kerubistan/kroki/collections/ImmutableListBuilder.kt @@ -0,0 +1,26 @@ +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 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/test/kotlin/io/github/kerubistan/kroki/collections/CollectionsKtTest.kt b/kroki-collections/src/test/kotlin/io/github/kerubistan/kroki/collections/CollectionsKtTest.kt index fd3b7fdf..88ae67ab 100644 --- 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 @@ -1,13 +1,8 @@ package io.github.kerubistan.kroki.collections -import io.kotest.matchers.collections.shouldBeEmpty -import io.kotest.matchers.collections.shouldBeSingleton -import io.kotest.matchers.collections.shouldContainAll -import io.kotest.matchers.collections.shouldEndWith -import io.kotest.matchers.collections.shouldNotBeEmpty -import io.kotest.matchers.collections.shouldNotContainAll -import io.kotest.matchers.collections.shouldStartWith +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 @@ -28,4 +23,20 @@ class CollectionsKtTest { 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 + } + } } \ 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 00000000..7205bc25 --- /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 From 117fe265d9451e3ecdcd0ab42719949363ce0b42 Mon Sep 17 00:00:00 2001 From: Laszlo Hornyak Date: Sun, 13 Oct 2024 18:38:22 +0200 Subject: [PATCH 10/11] minor cleanup: removed unnecessary type qualifier Signed-off-by: Laszlo Hornyak --- .../io/github/kerubistan/kroki/collections/CollectionsKtTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 88ae67ab..85def1af 100644 --- 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 @@ -26,7 +26,7 @@ class CollectionsKtTest { @Test fun buildList() { - buildList { } shouldBe emptyList() + buildList { } shouldBe emptyList() buildList { add("A") } shouldBe listOf("A") buildList { add("A") From ce9860292a8f1b8998b6224ebe2b72692c16aaa4 Mon Sep 17 00:00:00 2001 From: Laszlo Hornyak Date: Sun, 13 Oct 2024 19:41:59 +0200 Subject: [PATCH 11/11] list builder: addAll methods Signed-off-by: Laszlo Hornyak --- .../kroki/collections/ImmutableArrayList.kt | 2 +- .../kroki/collections/ImmutableListBuilder.kt | 23 +++++++++++ .../kroki/collections/CollectionsKtTest.kt | 39 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) 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 index f79c5ded..11097a77 100644 --- 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 @@ -7,7 +7,7 @@ import io.github.kerubistan.kroki.delegates.weak */ internal class ImmutableArrayList() : List { - private lateinit var items: Array + internal lateinit var items: Array internal constructor(array: Array) : this() { this.items = array 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 index b7be5a81..30a2454e 100644 --- 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 @@ -13,6 +13,29 @@ class ImmutableListBuilder { 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() 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 index 85def1af..eefdb8da 100644 --- 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 @@ -38,5 +38,44 @@ class CollectionsKtTest { 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