diff --git a/enumset/src/commonMain/kotlin/Bit32.kt b/enumset/src/commonMain/kotlin/Bit32.kt index 70bff76..682b7b9 100644 --- a/enumset/src/commonMain/kotlin/Bit32.kt +++ b/enumset/src/commonMain/kotlin/Bit32.kt @@ -26,10 +26,12 @@ import kotlin.jvm.JvmInline * Instances of this class have the following particularities: * - Operations on a single element execute in `O(1)`. * - Memory usage is `O(1)`: exactly 32 bits when unboxed. + * + * @see MutableBitSet32 Mutable alternative. */ @ExperimentalEnumSetApi @JvmInline -value class BitSet32 private constructor(private val value: Int) : Set { +value class BitSet32 internal constructor(internal val value: Int) : Set { override val size: Int get() = value.countOneBits() @@ -141,3 +143,177 @@ value class BitSet32 private constructor(private val value: Int) : Set { } } } + +/** + * [MutableSet] implementation that can store values in range `0..31`. + * + * To create an instance of this class, use one of the constructors, or the utility functions [MutableBitSet32.of], + * [MutableBitSet32.full] and [MutableBitSet32.empty]. + * + * Instances of this class have the following particularities: + * - Operations on a single element execute in `O(1)`. + * - Memory usage is `O(1)`: exactly 32 bits when unboxed. + * + * Instances of this class are not thread-safe. + * + * @see BitSet32 Immutable alternative. + */ +@ExperimentalEnumSetApi +class MutableBitSet32 internal constructor(private var storage: BitSet32) : MutableSet { + + /** + * Creates an empty [MutableBitSet32]. + */ + constructor() : this(BitSet32.empty()) + + /** + * Creates a [MutableBitSet32] that contains the specified [elements]. + */ + constructor(elements: Iterable) : this() { + addAll(elements) + } + + override val size: Int + get() = storage.size + + override fun isEmpty(): Boolean = + storage.isEmpty() + + override fun contains(element: Int): Boolean = + storage.contains(element) + + override fun containsAll(elements: Collection): Boolean = + storage.containsAll(elements) + + override fun iterator(): MutableIterator = + MutableBitSet32Iterator(this) + + private class MutableBitSet32Iterator( + private val set: MutableBitSet32, + ) : MutableIterator { + private var index = 0 + + override fun hasNext(): Boolean { + while (index < 32) { + if (index in set) { + return true + } + index++ + } + return false + } + + override fun next(): Int { + check(index < 32) { "Impossible state: 'next()' was called after this iterator reached the end of the set" } + + if (index !in set) { + throw NoSuchElementException() + } + + return index++ + } + + override fun remove() { + set.remove(index) + } + } + + override fun add(element: Int): Boolean { + require(element in 0..31) { "The element $element cannot be added to MutableBitSet32 because it is out of range" } + + val rank = 1 shl element + val alreadyExisted = storage.value and rank != 0 + storage = BitSet32(storage.value or rank) + + return !alreadyExisted + } + + override fun remove(element: Int): Boolean { + if (element !in 0..31) { + return false + } + + val rank = 1 shl element + val alreadyExisted = storage.value and rank != 0 + storage = BitSet32(storage.value and rank.inv()) + + return alreadyExisted + } + + // In the future, make this more performant + override fun addAll(elements: Collection): Boolean { + var result = false + + for (element in elements) { + if (add(element)) { + result = true + } + } + + return result + } + + // In the future, make this more performant + override fun removeAll(elements: Collection): Boolean { + var result = false + + for (element in elements) { + if (remove(element)) { + result = true + } + } + + return result + } + + // In the future, make this more performant + override fun retainAll(elements: Collection): Boolean { + var result = false + + for (element in this) { + if (element !in elements) { + remove(element) + result = true + } + } + + return result + } + + override fun clear() { + storage = BitSet32.empty() + } + + override fun hashCode(): Int = storage.hashCode() + + override fun equals(other: Any?): Boolean = + other is MutableBitSet32 && storage == other.storage + + override fun toString() = storage.toString() + + companion object { + + /** + * Creates an empty instance of [MutableBitSet32]. + */ + @ExperimentalEnumSetApi + fun empty(): MutableBitSet32 = + MutableBitSet32() + + /** + * Creates a full instance of [MutableBitSet32], which contains all elements in `0..31`. + */ + fun full(): MutableBitSet32 = + MutableBitSet32(BitSet32.full()) + + /** + * Creates an instance of [MutableBitSet32] that contains the given [elements]. + * + * If [elements] contains duplicates, they will only be present once in the resulting set (since sets cannot contain duplicates). + * + * @throws IllegalArgumentException If an element is not in the range `0..31`. + */ + fun of(vararg elements: Int): MutableBitSet32 = + MutableBitSet32(elements.asList()) + } +} diff --git a/enumset/src/commonTest/kotlin/MutableBitSetTest.kt b/enumset/src/commonTest/kotlin/MutableBitSetTest.kt new file mode 100644 index 0000000..8520f5c --- /dev/null +++ b/enumset/src/commonTest/kotlin/MutableBitSetTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025, OpenSavvy and contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package opensavvy.enumset + +import opensavvy.enumset.datatypes.testEmptySetValidity +import opensavvy.enumset.datatypes.testFullSetValidity +import opensavvy.enumset.datatypes.testMutableSetValidity +import opensavvy.prepared.runner.kotest.PreparedSpec +import opensavvy.prepared.suite.prepared + +@OptIn(ExperimentalEnumSetApi::class) +class MutableBitSetTest : PreparedSpec({ + val emptySet by prepared { MutableBitSet32.empty() } + val fullSet by prepared { MutableBitSet32.full() } + + testEmptySetValidity("MutableBitSet32", 32, emptySet) + testFullSetValidity("MutableBitSet32", 32, fullSet) + testMutableSetValidity("MutableBitSet32", 32) { MutableBitSet32.of(*it.toIntArray()) } +}) diff --git a/enumset/src/commonTest/kotlin/datatypes/SetSuite.kt b/enumset/src/commonTest/kotlin/datatypes/SetSuite.kt index 7f0d9ee..1c9c20b 100644 --- a/enumset/src/commonTest/kotlin/datatypes/SetSuite.kt +++ b/enumset/src/commonTest/kotlin/datatypes/SetSuite.kt @@ -14,11 +14,17 @@ * limitations under the License. */ +@file:OptIn(ExperimentalParameterizeApi::class) + package opensavvy.enumset.datatypes +import com.benwoodworth.parameterize.ExperimentalParameterizeApi +import com.benwoodworth.parameterize.parameterOf +import com.benwoodworth.parameterize.parameterize import io.kotest.assertions.throwables.shouldThrow import opensavvy.prepared.suite.Prepared import opensavvy.prepared.suite.SuiteDsl +import opensavvy.prepared.suite.TestDsl import opensavvy.prepared.suite.prepared import opensavvy.prepared.suite.random.nextInt import opensavvy.prepared.suite.random.random @@ -98,14 +104,9 @@ fun SuiteDsl.testSetValidity( } suite("Multiple elements") { - val targetSize by randomInt(0, maxSize - 1) + val targetSize by randomInt(1, maxSize - 1) val values by prepared { - val size = targetSize() - buildSet { - while (this.size < size) { - add(random.nextInt(0, maxSize)) - } - } + buildSetOfSize(targetSize(), maxSize) } val set by prepared { create(values().toTypedArray()) @@ -243,3 +244,204 @@ fun SuiteDsl.testFullSetValidity( check(maxSize + 2 !in set()) } } + +fun SuiteDsl.testMutableSetValidity( + name: String, + maxSize: Int, + create: (Array) -> MutableSet, +) = suite("MutableSet $name") { + testSetValidity(name, maxSize, create) + + suite("clear") { + val clearedSet by prepared { + val set = create(arrayOf(0, 12)) + set.clear() + set + } + + testEmptySetValidity(name, maxSize, clearedSet) + } + + suite("add") { + test("Add an element to the empty set") { + val set = create(emptyArray()) + check(set.add(5)) + check(5 in set) + check(set.size == 1) + } + + test("Add an element to a non-empty set") { + val set = create(arrayOf(0, 12)) + check(set.add(5)) + check(5 in set) + check(0 in set) + check(12 in set) + check(set.size == 3) + } + + test("Add an element that is already present") { + val set = create(arrayOf(0, 12)) + check(!set.add(12)) + check(0 in set) + check(12 in set) + check(set.size == 2) + } + } + + suite("addAll") { + parameterize { + val currentSize by parameterOf(0, 1, 2, 12, maxSize - 1) + + val set by prepared { + create(buildSetOfSize(currentSize, maxSize).toTypedArray()) + } + + val addingSize by parameterOf(0, 1, 4) + + val addingValues by prepared { + buildSetOfSize(addingSize, maxSize) + } + + val addingSet by parameterOf( + prepared("set of any type") { + addingValues() + }, + prepared("set of same type") { + create(addingValues().toTypedArray()) + } + ) + + test("Adding $addingSize elements to a set of size $currentSize (${addingSet.name})") { + val set = set() + + val willBeAdded = addingSet().filter { it !in set } + + check(set.addAll(addingSet()) == willBeAdded.isNotEmpty()) + check(set.size == currentSize + willBeAdded.size) + } + } + } + + suite("remove") { + test("Remove an element from the empty set") { + val set = create(emptyArray()) + check(!set.remove(5)) + check(set.isEmpty()) + } + + test("Remove the only element in a set") { + val set = create(arrayOf(5)) + check(set.remove(5)) + check(set.isEmpty()) + } + + test("Remove an element that is not contained in the set") { + val set = create(arrayOf(4, 9)) + check(!set.remove(5)) + check(!set.isEmpty()) + check(set.size == 2) + } + + test("Remove an element in a set with multiple elements") { + val set = create(arrayOf(1, 9, 3)) + check(set.remove(3)) + check(!set.isEmpty()) + check(set.size == 2) + } + + for (i in listOf(-1, maxSize, maxSize + 1, Int.MAX_VALUE, Int.MIN_VALUE)) { + test("Remove an element that is out of range: $i") { + val set = create(arrayOf(7, 6)) + check(!set.remove(i)) + check(!set.isEmpty()) + check(set.size == 2) + } + } + } + + suite("removeAll") { + parameterize { + val currentSize by parameterOf(0, 1, 2, 12, maxSize - 1) + + val set by prepared { + create(buildSetOfSize(currentSize, maxSize).toTypedArray()) + } + + val removingSize by parameterOf(0, 1, 4) + + val removingValues by prepared { + buildSetOfSize(removingSize, maxSize) + } + + val removingSet by parameterOf( + prepared("set of any type") { + removingValues() + }, + prepared("set of same type") { + create(removingValues().toTypedArray()) + } + ) + + test("Removing $removingSize elements from a set of size $currentSize (${removingSet.name})") { + val set = set() + + val willBeRemoved = removingSet().filter { it in set } + + check(set.removeAll(removingSet()) == willBeRemoved.isNotEmpty()) + check(set.size == currentSize - willBeRemoved.size) + } + } + } + + suite("Iterator") { + test("Remove an item while iterating") { + val set = create(arrayOf(5, 7, 9)) + println("Iterating through set $set") + val iter = set.iterator() + + check(iter.hasNext()) + check(iter.next() == 5) + + check(iter.hasNext()) + iter.remove() + + check(iter.hasNext()) + check(iter.next() == 9) + } + } + + suite("retainAll") { + test("Remove all elements") { + val set = create(arrayOf(5, 7, 9)) + + check(set.retainAll(emptySet())) + check(set.isEmpty()) + } + + test("Remove a single element") { + val set = create(arrayOf(5, 7, 9)) + + check(set.retainAll(setOf(1, 5, 9))) + check(!set.isEmpty()) + check(set.size == 2) + } + + test("Remove no elements") { + val set = create(arrayOf(5, 7, 9)) + + check(!set.retainAll(setOf(5, 7, 9))) + check(!set.isEmpty()) + check(set.size == 3) + } + } +} + +private suspend fun TestDsl.buildSetOfSize( + size: Int, + maxSize: Int, +): Set = + buildSet { + while (this.size < size) { + add(random.nextInt(0, maxSize - 1)) + } + }