diff --git a/CHANGELOG.md b/CHANGELOG.md index 9eaf613bce4..c9c3a0ff0e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 0.24.8 +* `Coroutines`: + * Add `SortedBinaryTreeNode` + ## 0.24.7 * `Versions`: diff --git a/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Comparator.kt b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Comparator.kt new file mode 100644 index 00000000000..61ce00847fe --- /dev/null +++ b/common/src/commonMain/kotlin/dev/inmo/micro_utils/common/Comparator.kt @@ -0,0 +1,6 @@ +package dev.inmo.micro_utils.common + +/** + * Creates simple [Comparator] which will use [compareTo] of [T] for both objects + */ +fun , C : T> T.createComparator() = Comparator { o1, o2 -> o1.compareTo(o2) } diff --git a/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/collections/SortedBinaryTreeNode.kt b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/collections/SortedBinaryTreeNode.kt new file mode 100644 index 00000000000..fade45c3e61 --- /dev/null +++ b/coroutines/src/commonMain/kotlin/dev/inmo/micro_utils/coroutines/collections/SortedBinaryTreeNode.kt @@ -0,0 +1,319 @@ +package dev.inmo.micro_utils.coroutines.collections + +import dev.inmo.micro_utils.coroutines.SmartRWLocker +import dev.inmo.micro_utils.coroutines.waitReadRelease +import dev.inmo.micro_utils.coroutines.withReadAcquire +import dev.inmo.micro_utils.coroutines.withWriteLock +import kotlinx.coroutines.job +import kotlinx.serialization.Serializable +import kotlin.coroutines.coroutineContext + +/** + * Creates simple [Comparator] which will use [compareTo] of [T] for both objects + */ +private fun , C : T> T.createComparator() = Comparator { o1, o2 -> o1.compareTo(o2) } + +@Serializable +class SortedBinaryTreeNode( + val data: T, + internal val comparator: Comparator, +) : Iterable> { + internal var leftNode: SortedBinaryTreeNode? = null + internal var rightNode: SortedBinaryTreeNode? = null + internal val locker: SmartRWLocker by lazy { + SmartRWLocker() + } + + suspend fun getLeftNode() = locker.withReadAcquire { + leftNode + } + + suspend fun getRightNode() = locker.withReadAcquire { + rightNode + } + + suspend fun getLeft() = getLeftNode() ?.data + + suspend fun getRight() = getRightNode() ?.data + + override fun equals(other: Any?): Boolean { + return other === this || (other is SortedBinaryTreeNode<*> && other.data == data && other.rightNode == rightNode && other.leftNode == leftNode) + } + + override fun hashCode(): Int { + return data.hashCode() * 31 + rightNode.hashCode() + leftNode.hashCode() + } + + suspend fun size(): Int { + return locker.withReadAcquire { + 1 + (leftNode ?.size() ?: 0) + (rightNode ?.size() ?: 0) + } + } + + /** + * This [Iterator] will run from less to greater values of nodes starting the + * [dev.inmo.micro_utils.coroutines.collections.SortedBinaryTreeNode]-receiver. Due to non-suspending + * nature of [iterator] builder, this [Iterator] **DO NOT** guarantee consistent content due to iterations. It + * means, that tree can be changed during to iteration process + */ + override fun iterator(): Iterator> = iterator { + leftNode ?.let { + it.iterator().forEach { yield(it) } + } + yield(this@SortedBinaryTreeNode) + rightNode ?.let { + it.iterator().forEach { yield(it) } + } + } + + override fun toString(): String { + return "$data($leftNode;$rightNode)" + } + + companion object { + operator fun > invoke( + data: T, + ) = SortedBinaryTreeNode( + data, + data.createComparator() + ) + } +} + +/** + * Will add subnode in tree if there are no any node with [newData] + * + * * If [newData] is greater than [SortedBinaryTreeNode.data] of currently checking node, + * will be used [SortedBinaryTreeNode.rightNode] + * * If [newData] is equal to [SortedBinaryTreeNode.data] of currently + * checking node - will be returned currently checking node + * * If [newData] is less than [SortedBinaryTreeNode.data] of currently + * checking node - will be used [SortedBinaryTreeNode.leftNode] + * + * This process will continue until function will not find place to put [SortedBinaryTreeNode] with data or + * [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found + */ +private suspend fun SortedBinaryTreeNode.addSubNode( + subNode: SortedBinaryTreeNode, + skipLockers: Set = emptySet() +): SortedBinaryTreeNode { + var currentlyChecking = this + val lockedLockers = mutableSetOf() + try { + while (coroutineContext.job.isActive) { + if (currentlyChecking.locker !in lockedLockers && currentlyChecking.locker !in skipLockers) { + currentlyChecking.locker.lockWrite() + lockedLockers.add(currentlyChecking.locker) + } + val left = currentlyChecking.leftNode + val right = currentlyChecking.rightNode + val comparingResult = currentlyChecking.comparator.compare(subNode.data, currentlyChecking.data) + val isGreater = comparingResult > 0 + when { + comparingResult == 0 -> return currentlyChecking + isGreater && right == null -> { + currentlyChecking.rightNode = subNode + return subNode + } + isGreater && right != null -> { + currentlyChecking = right + } + left == null -> { + currentlyChecking.leftNode = subNode + return subNode + } + else -> { + currentlyChecking = left + } + } + } + } finally { + lockedLockers.forEach { + runCatching { it.unlockWrite() } + } + } + error("Unable to add node") +} + +/** + * Will add subnode in tree if there are no any node with [newData] + * + * * If [newData] is greater than [SortedBinaryTreeNode.data] of currently checking node, + * will be used [SortedBinaryTreeNode.rightNode] + * * If [newData] is equal to [SortedBinaryTreeNode.data] of currently + * checking node - will be returned currently checking node + * * If [newData] is less than [SortedBinaryTreeNode.data] of currently + * checking node - will be used [SortedBinaryTreeNode.leftNode] + * + * This process will continue until function will not find place to put [SortedBinaryTreeNode] with data or + * [SortedBinaryTreeNode] with [SortedBinaryTreeNode.data] same as [newData] will be found + */ +suspend fun SortedBinaryTreeNode.addSubNode(newData: T): SortedBinaryTreeNode { + return addSubNode( + SortedBinaryTreeNode(newData, comparator) + ) +} + +suspend fun SortedBinaryTreeNode.findParentNode(data: T): SortedBinaryTreeNode? { + var currentParent: SortedBinaryTreeNode? = null + var currentlyChecking: SortedBinaryTreeNode? = this + val lockedLockers = mutableSetOf() + try { + while (coroutineContext.job.isActive) { + if (currentlyChecking == null) { + return null + } + if (currentlyChecking.locker !in lockedLockers) { + currentlyChecking.locker.acquireRead() + lockedLockers.add(currentlyChecking.locker) + } + val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.data) + when { + comparingResult > 0 -> { + currentParent = currentlyChecking + currentlyChecking = currentlyChecking.rightNode + continue + } + comparingResult < 0 -> { + currentParent = currentlyChecking + currentlyChecking = currentlyChecking.leftNode + continue + } + else -> return currentParent + } + } + } finally { + lockedLockers.forEach { + runCatching { it.releaseRead() } + } + } + error("Unable to find node") +} + +/** + * Will remove (detach) node from tree starting with [this] [SortedBinaryTreeNode] + * + * @return If data were found, [Pair] where [Pair.first] is the parent node where from [Pair.second] has been detached; + * null otherwise + */ +suspend fun SortedBinaryTreeNode.removeSubNode(data: T): Pair, SortedBinaryTreeNode>? { + val onFoundToRemoveCallback: suspend SortedBinaryTreeNode.(left: SortedBinaryTreeNode?, right: SortedBinaryTreeNode?) -> Unit = { left, right -> + left ?.also { leftNode -> addSubNode(leftNode, setOf(locker)) } + right ?.also { rightNode -> addSubNode(rightNode, setOf(locker)) } + } + while (coroutineContext.job.isActive) { + val foundParentNode = findParentNode(data) ?: return null + foundParentNode.locker.withWriteLock { + val left = foundParentNode.leftNode + val right = foundParentNode.rightNode + when { + left != null && left.comparator.compare(data, left.data) == 0 -> { + foundParentNode.leftNode = null + foundParentNode.onFoundToRemoveCallback(left.leftNode, left.rightNode) + return foundParentNode to left + } + right != null && right.comparator.compare(data, right.data) == 0 -> { + foundParentNode.rightNode = null + foundParentNode.onFoundToRemoveCallback(right.leftNode, right.rightNode) + return foundParentNode to right + } + else -> { + return@withWriteLock // data has been changed, new search required + } + } + } + } + error("Unable to remove node") +} +suspend fun SortedBinaryTreeNode.findNode(data: T): SortedBinaryTreeNode? { + var currentlyChecking: SortedBinaryTreeNode? = this + val lockedLockers = mutableSetOf() + try { + while (coroutineContext.job.isActive) { + if (currentlyChecking == null) { + return null + } + if (currentlyChecking.locker !in lockedLockers) { + currentlyChecking.locker.acquireRead() + lockedLockers.add(currentlyChecking.locker) + } + val comparingResult = currentlyChecking.comparator.compare(data, currentlyChecking.data) + when { + comparingResult > 0 -> { + currentlyChecking = currentlyChecking.rightNode + continue + } + comparingResult < 0 -> { + currentlyChecking = currentlyChecking.leftNode + continue + } + else -> return currentlyChecking + } + } + } finally { + lockedLockers.forEach { + runCatching { it.releaseRead() } + } + } + error("Unable to find node") +} +suspend fun SortedBinaryTreeNode.contains(data: T): Boolean = findNode(data) != null + +suspend fun SortedBinaryTreeNode.findNodesInRange(from: T, to: T, fromInclusiveMode: Boolean, toInclusiveMode: Boolean): Set> { + val results = mutableSetOf>() + val leftToCheck = mutableSetOf(this) + val lockedLockers = mutableSetOf() + val fromComparingFun: (SortedBinaryTreeNode) -> Boolean = if (fromInclusiveMode) { + { it.comparator.compare(from, it.data) <= 0 } + } else { + { it.comparator.compare(from, it.data) < 0 } + } + val toComparingFun: (SortedBinaryTreeNode) -> Boolean = if (toInclusiveMode) { + { it.comparator.compare(to, it.data) >= 0 } + } else { + { it.comparator.compare(to, it.data) > 0 } + } + try { + while (coroutineContext.job.isActive && leftToCheck.isNotEmpty()) { + val currentlyChecking = leftToCheck.first() + leftToCheck.remove(currentlyChecking) + if (currentlyChecking in results) { + continue + } + currentlyChecking.locker.acquireRead() + lockedLockers.add(currentlyChecking.locker) + if (fromComparingFun(currentlyChecking) && toComparingFun(currentlyChecking)) { + results.add(currentlyChecking) + currentlyChecking.leftNode ?.let { leftToCheck.add(it) } + currentlyChecking.rightNode ?.let { leftToCheck.add(it) } + continue + } + when { + currentlyChecking.comparator.compare(to, currentlyChecking.data) < 0 -> currentlyChecking.leftNode ?.let { leftToCheck.add(it) } + currentlyChecking.comparator.compare(from, currentlyChecking.data) > 0 -> currentlyChecking.rightNode ?.let { leftToCheck.add(it) } + } + } + return results.toSet() + } finally { + lockedLockers.forEach { + runCatching { it.releaseRead() } + } + } + error("Unable to find nodes range") +} +suspend fun SortedBinaryTreeNode.findNodesInRange(from: T, to: T): Set> = findNodesInRange( + from = from, + to = to, + fromInclusiveMode = true, + toInclusiveMode = true +) +suspend fun SortedBinaryTreeNode.findNodesInRangeExcluding(from: T, to: T): Set> = findNodesInRange( + from = from, + to = to, + fromInclusiveMode = false, + toInclusiveMode = false +) +suspend fun > SortedBinaryTreeNode.findNodesInRange(range: ClosedRange): Set> = findNodesInRange( + from = range.start, + to = range.endInclusive, +) \ No newline at end of file diff --git a/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt b/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt new file mode 100644 index 00000000000..884c85ea061 --- /dev/null +++ b/coroutines/src/commonTest/kotlin/SortedBinaryTreeNodeTests.kt @@ -0,0 +1,138 @@ +import dev.inmo.micro_utils.coroutines.collections.SortedBinaryTreeNode +import dev.inmo.micro_utils.coroutines.collections.addSubNode +import dev.inmo.micro_utils.coroutines.collections.findNode +import dev.inmo.micro_utils.coroutines.collections.findNodesInRange +import dev.inmo.micro_utils.coroutines.collections.findParentNode +import dev.inmo.micro_utils.coroutines.collections.removeSubNode +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class SortedBinaryTreeNodeTests { + @Test + fun insertOnZeroLevelWorks() = runTest { + val zeroNode = SortedBinaryTreeNode(0) + zeroNode.addSubNode(1) + zeroNode.addSubNode(-1) + + assertEquals(0, zeroNode.data) + assertEquals(1, zeroNode.getRightNode() ?.data) + assertEquals(-1, zeroNode.getLeftNode() ?.data) + } + @Test + fun searchOnZeroLevelWorks() = runTest { + val zeroNode = SortedBinaryTreeNode(0) + val oneNode = zeroNode.addSubNode(1) + val minusOneNode = zeroNode.addSubNode(-1) + + val assertingNodesToSearchQuery = mapOf( + setOf(oneNode) to (1 .. 1), + setOf(zeroNode, oneNode) to (0 .. 1), + setOf(minusOneNode, zeroNode, oneNode) to (-1 .. 1), + setOf(minusOneNode, zeroNode) to (-1 .. 0), + setOf(minusOneNode) to (-1 .. -1), + setOf(zeroNode) to (0 .. 0), + ) + + assertingNodesToSearchQuery.forEach { + val foundData = zeroNode.findNodesInRange(it.value) + assertTrue(foundData.containsAll(it.key)) + assertTrue(it.key.containsAll(foundData)) + } + } + @Test + fun deepInsertOnWorks() = runTest { + val zeroNode = SortedBinaryTreeNode(0) + val rangeRadius = 500 + val nodes = mutableMapOf>() + for (i in -rangeRadius .. rangeRadius) { + nodes[i] = zeroNode.addSubNode(i) + } + + for (i in -rangeRadius .. rangeRadius) { + val expectedNode = nodes.getValue(i) + val foundNode = zeroNode.findNode(i) + + assertTrue(expectedNode === foundNode) + + if (expectedNode === zeroNode) continue + + val parentNode = zeroNode.findParentNode(i) + assertTrue( + parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode, + "It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}" + ) + } + + val sourceTreeSize = zeroNode.size() + assertTrue(sourceTreeSize == nodes.size) + assertTrue(sourceTreeSize == (rangeRadius * 2 + 1)) + + for (i in -rangeRadius .. rangeRadius) { + val expectedNode = nodes.getValue(i) + val parentNode = zeroNode.findParentNode(i) + + if (parentNode == null && i == zeroNode.data && expectedNode === zeroNode) continue + + assertTrue(parentNode != null, "It is expected, that parent node of ${expectedNode.data} will not be null") + + assertTrue( + parentNode.getLeftNode() === expectedNode || parentNode.getRightNode() === expectedNode, + "It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}" + ) + + val removeResult = zeroNode.removeSubNode(i) + assertTrue(removeResult ?.first === parentNode) + assertTrue(removeResult.second === expectedNode) + + nodes[i] = zeroNode.addSubNode(i) + assertTrue(nodes[i] != null) + assertTrue(nodes[i] != expectedNode) + assertTrue(nodes[i] ?.data == i) + } + + assertTrue(sourceTreeSize == zeroNode.size()) + + for (i in -rangeRadius .. rangeRadius) { + val expectedNode = nodes.getValue(i) + val foundNode = zeroNode.findNode(i) + + assertTrue(expectedNode === foundNode) + + if (expectedNode === zeroNode) continue + + val parentNode = zeroNode.findParentNode(i) + assertTrue( + parentNode ?.getLeftNode() === expectedNode || parentNode ?.getRightNode() === expectedNode, + "It is expected, that parent node with data ${parentNode ?.data} will be parent of ${expectedNode.data}, but its left subnode is ${parentNode ?.getLeftNode() ?.data} and right one is ${parentNode ?.getRightNode() ?.data}" + ) + } + + var previousData = -rangeRadius - 1 + for (node in zeroNode) { + assertTrue(nodes[node.data] === node) + assertTrue(previousData == node.data - 1) + previousData = node.data + } + + assertTrue(sourceTreeSize == zeroNode.size()) + } + @Test + fun deepInsertIteratorWorking() = runTest { + val zeroNode = SortedBinaryTreeNode(0) + val rangeRadius = 500 + val nodes = mutableMapOf>() + for (i in -rangeRadius .. rangeRadius) { + nodes[i] = zeroNode.addSubNode(i) + } + + var previousData = -rangeRadius - 1 + for (node in zeroNode) { + assertTrue(nodes[node.data] === node) + assertTrue(previousData == node.data - 1) + previousData = node.data + } + assertTrue(previousData == rangeRadius) + } +} \ No newline at end of file