diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c031be..cdd3295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,33 @@ All notable changes to this project are documented here. The format is based on ## [Unreleased] +## [4.0.0] + +A breaking release that cleans up the core API and enforces an explicit public surface. + +### Changed (breaking) +- `TreeNode.treeIterator` is now a read-only `val` (set it via the constructor). Use + `iterator(order)` or `asSequence(order)` to traverse in a different order per call. +- `removeChild(child)` now only removes a **direct** child of the receiver (previously it removed + the node from its actual parent regardless). Use `child.detach()` to unhook a node from wherever + it lives. +- `addChild(child)` now throws `TreeNodeException` if `child` already has a parent or if the + attachment would create a cycle. Call `detach()` first to move a node. +- `clear()` no longer detaches the receiver from its own parent; it only removes its descendants. +- `path(descendant)` now returns `List>?` (`null` when `descendant` is the root or not a + descendant) instead of throwing `TreeNodeException`. + +### Added +- `TreeNode.detach()` — removes a node from its parent. +- `TreeNode.iterator(order)` — a one-shot iterator in a specific order. +- Strict `explicitApi()` mode across all modules. +- New `tree-structure-compose` module: a `LazyTree` composable for Compose Multiplatform. + +### Migration +- `node.treeIterator = PostOrder; for (n in node) { … }` → `for (n in node.asSequence(PostOrder)) { … }` +- `root.removeChild(deepNode)` → `deepNode.detach()` +- `try { node.path(x) } catch (e: TreeNodeException) { … }` → `node.path(x)?.let { … }` + ## [3.4.0] ### Added @@ -44,7 +71,8 @@ All notable changes to this project are documented here. The format is based on ## [3.1.3] - iOS targets and Maven Central (Sonatype Central Portal) publishing. -[Unreleased]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.4.0...HEAD +[Unreleased]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v4.0.0...HEAD +[4.0.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.4.0...v4.0.0 [3.4.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.1.5...v3.4.0 [3.1.5]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.1.3...v3.1.5 [3.1.4]: https://github.com/AdrianKuta/Tree-Data-Structure/releases/tag/v3.1.4 diff --git a/README.md b/README.md index 70b535f..a279370 100644 --- a/README.md +++ b/README.md @@ -104,9 +104,8 @@ World val root = TreeNode("root") // ... build your tree -// Choose iteration order (default is PreOrder) -root.treeIterator = TreeNodeIterators.PostOrder -for (node in root) println(node.value) +// Choose iteration order per call (the default order is set in the constructor and is read-only) +for (node in root.asSequence(TreeNodeIterators.PostOrder)) println(node.value) // Utilities root.nodeCount() // number of descendants @@ -114,10 +113,10 @@ root.height() // longest path to a leaf (in edges) root.depth() // distance from current node to the root val path = root.path(root.children.first()) // nodes from descendant up to root -// Mutations +// Mutations — removeChild removes a *direct* child; detach() unhooks a node from wherever it lives val child = root.children.first() -root.removeChild(child) -root.clear() // remove entire subtree +root.removeChild(child) // child is now detached from root +root.clear() // remove all descendants of root ``` ### Lazy traversal with Sequence diff --git a/api/tree-structure.api b/api/tree-structure.api index 73bc61c..2bd2935 100644 --- a/api/tree-structure.api +++ b/api/tree-structure.api @@ -13,6 +13,7 @@ public class com/github/adriankuta/datastructure/tree/TreeNode : com/github/adri public synthetic fun child (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode; public final fun clear ()V public final fun depth ()I + public final fun detach ()Z public final fun getChildren ()Ljava/util/List; public final fun getParent ()Lcom/github/adriankuta/datastructure/tree/TreeNode; public final fun getTreeIterator ()Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators; @@ -20,11 +21,11 @@ public class com/github/adriankuta/datastructure/tree/TreeNode : com/github/adri public final fun height ()I public final fun isRoot ()Z public fun iterator ()Ljava/util/Iterator; + public final fun iterator (Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)Ljava/util/Iterator; public final fun nodeCount ()I public final fun path (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List; public final fun prettyString ()Ljava/lang/String; public final fun removeChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z - public final fun setTreeIterator (Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)V public fun toString ()Ljava/lang/String; } diff --git a/build.gradle.kts b/build.gradle.kts index b64249c..0a53aa1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ plugins { val PUBLISH_GROUP_ID = "com.github.adriankuta" val PUBLISH_ARTIFACT_ID = "tree-structure" // base artifact; KMP will add -jvm, -ios*, etc. -val PUBLISH_VERSION = "3.4.0" +val PUBLISH_VERSION = "4.0.0" val snapshot: String? by project @@ -60,6 +60,7 @@ repositories { } kotlin { + explicitApi() jvmToolchain(21) jvm() diff --git a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/ChildDeclarationInterface.kt b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/ChildDeclarationInterface.kt index 2f61d95..830705f 100644 --- a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/ChildDeclarationInterface.kt +++ b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/ChildDeclarationInterface.kt @@ -2,7 +2,7 @@ package com.github.adriankuta.datastructure.tree import kotlin.jvm.JvmSynthetic -interface ChildDeclarationInterface { +public interface ChildDeclarationInterface { /** * This method is used to easily create child in node. @@ -20,5 +20,5 @@ interface ChildDeclarationInterface { * @return New created TreeNode. */ @JvmSynthetic - fun child(value: T, childDeclaration: ChildDeclaration? = null): TreeNode + public fun child(value: T, childDeclaration: ChildDeclaration? = null): TreeNode } \ No newline at end of file diff --git a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNode.kt b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNode.kt index 1dd3ec6..88f1d96 100644 --- a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNode.kt +++ b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNode.kt @@ -27,14 +27,14 @@ import kotlin.jvm.JvmSynthetic * @param treeIterator the default traversal order used by [iterator]. Prefer the * `asSequence(order)` / `preOrderSequence()` extensions to choose an order without mutating state. */ -open class TreeNode(val value: T, var treeIterator: TreeNodeIterators = PreOrder) : Iterable>, ChildDeclarationInterface { +public open class TreeNode(public val value: T, public val treeIterator: TreeNodeIterators = PreOrder) : Iterable>, ChildDeclarationInterface { private var _parent: TreeNode? = null /** * The converse notion of a child, an immediate ancestor. */ - val parent: TreeNode? + public val parent: TreeNode? get() = _parent private val _children = mutableListOf>() @@ -42,28 +42,53 @@ open class TreeNode(val value: T, var treeIterator: TreeNodeIterators = PreOr /** * A group of nodes with the same parent. */ - val children: List> + public val children: List> get() = _children /** * Checks whether the current tree node is the root of the tree * @return `true` if the current tree node is root of the tree, `false` otherwise. */ - val isRoot: Boolean + public val isRoot: Boolean get() = _parent == null /** - * Add new child to current node or root. + * Adds [child] as a direct child of this node. * - * @param child A node which will be directly connected to current node. + * @param child a node that is not already attached to a tree. To move a node that already has a + * parent, call [detach] on it first. + * @throws TreeNodeException if [child] already has a parent, or if attaching it here would create + * a cycle (i.e. [child] is this node or one of its ancestors). */ - fun addChild(child: TreeNode) { + public fun addChild(child: TreeNode) { + if (child._parent != null) { + throw TreeNodeException("$child already has a parent; call detach() before re-attaching it.") + } + var ancestor: TreeNode? = this + while (ancestor != null) { + if (ancestor === child) { + throw TreeNodeException("Adding $child here would create a cycle.") + } + ancestor = ancestor._parent + } child._parent = this _children.add(child) } + /** + * Detaches this node from its parent, removing it from the parent's [children]. + * + * @return `true` if this node was attached and is now detached; `false` if it was already a root. + */ + public fun detach(): Boolean { + val currentParent = _parent ?: return false + currentParent._children.remove(this) + _parent = null + return true + } + @JvmSynthetic - override fun child(value: T, childDeclaration: ChildDeclaration?): TreeNode { + public override fun child(value: T, childDeclaration: ChildDeclaration?): TreeNode { val newChild = TreeNode(value) newChild._parent = this if (childDeclaration != null) @@ -73,21 +98,24 @@ open class TreeNode(val value: T, var treeIterator: TreeNodeIterators = PreOr } /** - * Removes a single instance of the specified node from this tree, if it is present. + * Removes [child] from this node's direct [children], if present. * - * @return `true` if the node has been successfully removed; `false` if it was not present in the tree. + * This only removes a *direct* child of the receiver; it does not reach into other nodes. To + * remove a node from wherever it currently lives, call [detach] on it instead. + * + * @return `true` if [child] was a direct child and has been removed; `false` otherwise. */ - fun removeChild(child: TreeNode): Boolean { - val removed = child._parent?._children?.remove(child) - child._parent = null - return removed ?: false + public fun removeChild(child: TreeNode): Boolean { + val removed = _children.remove(child) + if (removed) child._parent = null + return removed } /** * This function go through tree and counts children. Root element is not counted. * @return All child and nested child count. */ - fun nodeCount(): Int { + public fun nodeCount(): Int { var count = 0 val stack = ArrayDeque>() stack.addAll(_children) @@ -102,7 +130,7 @@ open class TreeNode(val value: T, var treeIterator: TreeNodeIterators = PreOr /** * @return The number of edges on the longest path between current node and a descendant leaf. */ - fun height(): Int { + public fun height(): Int { var maxDepth = 0 val stack = ArrayDeque, Int>>() stack.addLast(this to 0) @@ -118,7 +146,7 @@ open class TreeNode(val value: T, var treeIterator: TreeNodeIterators = PreOr * Distance is the number of edges along the shortest path between two nodes. * @return The distance between current node and the root. */ - fun depth(): Int { + public fun depth(): Int { var depth = 0 var tempParent = parent @@ -130,53 +158,52 @@ open class TreeNode(val value: T, var treeIterator: TreeNodeIterators = PreOr } /** - * Returns the collection of nodes, which connect the current node - * with its descendants + * Returns the chain of nodes from [descendant] up to and including this node, or `null` if + * [descendant] is not a strict descendant of this node. * - * @param descendant the bottom child node for which the path is calculated - * @return collection of nodes, which connect the current node with its descendants - * @throws TreeNodeException exception that may be thrown in case if the - * current node does not have such descendant or if the - * specified tree node is root + * @param descendant the node to walk up from. + * @return the path `[descendant, …, this]`, or `null` if [descendant] is the root or is not + * located in this node's subtree. */ - @Throws(TreeNodeException::class) - fun path(descendant: TreeNode): List> { - + public fun path(descendant: TreeNode): List>? { + if (descendant.isRoot) return null val path = mutableListOf>() var node = descendant path.add(node) while (!node.isRoot) { node = node.parent!! path.add(node) - if (node == this) - return path + if (node == this) return path } - throw TreeNodeException("The specified tree node $descendant is not the descendant of tree node $this") + return null } /** - * Remove all children from root and every node in tree. + * Removes every descendant of this node. Afterwards [children] is empty and all former + * descendants are detached (their parent is `null`). This node itself stays attached to its own + * parent. */ - fun clear() { - val all = ArrayDeque>() + public fun clear() { + val descendants = ArrayDeque>() val stack = ArrayDeque>() - stack.addLast(this) + stack.addAll(_children) while (stack.isNotEmpty()) { val node = stack.removeLast() - all.addLast(node) + descendants.addLast(node) stack.addAll(node._children) } - all.forEach { node -> + descendants.forEach { node -> node._parent = null node._children.clear() } + _children.clear() } - override fun toString(): String { + public override fun toString(): String { return value.toString() } - fun prettyString(): String { + public fun prettyString(): String { val stringBuilder = StringBuilder() print(stringBuilder, "", "") return stringBuilder.toString() @@ -198,9 +225,14 @@ open class TreeNode(val value: T, var treeIterator: TreeNodeIterators = PreOr } /** - * You can change default iterator by changing [treeIterator] property. + * Returns an iterator over this node and its descendants using the default [treeIterator] order. + * Use [iterator] with an explicit order, or the `asSequence(order)` extension, to traverse in a + * different order without changing this node's default. */ - override fun iterator(): Iterator> = when (treeIterator) { + public override fun iterator(): Iterator> = iterator(treeIterator) + + /** Returns an iterator over this node and its descendants in the given [order]. */ + public fun iterator(order: TreeNodeIterators): Iterator> = when (order) { PreOrder -> PreOrderTreeIterator(this) PostOrder -> PostOrderTreeIterator(this) LevelOrder -> LevelOrderTreeIterator(this) diff --git a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeExt.kt b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeExt.kt index 6ad015d..fca9b1e 100644 --- a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeExt.kt +++ b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeExt.kt @@ -3,7 +3,7 @@ package com.github.adriankuta.datastructure.tree import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators import kotlin.jvm.JvmSynthetic -typealias ChildDeclaration = ChildDeclarationInterface.() -> Unit +public typealias ChildDeclaration = ChildDeclarationInterface.() -> Unit /** * This method can be used to initialize new tree. @@ -14,7 +14,7 @@ typealias ChildDeclaration = ChildDeclarationInterface.() -> Unit * @see [ChildDeclarationInterface.child] */ @JvmSynthetic -inline fun tree( +public inline fun tree( root: T, defaultIterator: TreeNodeIterators = TreeNodeIterators.PreOrder, childDeclaration: ChildDeclaration diff --git a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeFunctionalExt.kt b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeFunctionalExt.kt index a187542..57f6a51 100644 --- a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeFunctionalExt.kt +++ b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeFunctionalExt.kt @@ -1,34 +1,34 @@ package com.github.adriankuta.datastructure.tree /** Returns the first node (pre-order) whose value matches [predicate], or `null`. Short-circuits. */ -fun TreeNode.findNode(predicate: (T) -> Boolean): TreeNode? = +public fun TreeNode.findNode(predicate: (T) -> Boolean): TreeNode? = preOrderSequence().firstOrNull { predicate(it.value) } /** All nodes (pre-order) whose value matches [predicate]. */ -fun TreeNode.filterNodes(predicate: (T) -> Boolean): List> = +public fun TreeNode.filterNodes(predicate: (T) -> Boolean): List> = preOrderSequence().filter { predicate(it.value) }.toList() /** `true` if any node's value matches [predicate]. Short-circuits. */ -fun TreeNode.anyNode(predicate: (T) -> Boolean): Boolean = +public fun TreeNode.anyNode(predicate: (T) -> Boolean): Boolean = preOrderSequence().any { predicate(it.value) } /** `true` if every node's value matches [predicate]. Short-circuits. */ -fun TreeNode.allNodes(predicate: (T) -> Boolean): Boolean = +public fun TreeNode.allNodes(predicate: (T) -> Boolean): Boolean = preOrderSequence().all { predicate(it.value) } /** Counts nodes whose value matches [predicate]. */ -fun TreeNode.countNodes(predicate: (T) -> Boolean): Int = +public fun TreeNode.countNodes(predicate: (T) -> Boolean): Int = preOrderSequence().count { predicate(it.value) } /** Folds [operation] over all nodes in pre-order, starting from [initial]. */ -fun TreeNode.foldNodes(initial: R, operation: (acc: R, node: TreeNode) -> R): R = +public fun TreeNode.foldNodes(initial: R, operation: (acc: R, node: TreeNode) -> R): R = preOrderSequence().fold(initial) { acc, node -> operation(acc, node) } /** * Returns a new tree with the same shape whose values are produced by [transform]. The original is * left untouched. Stack-safe (iterative), so it handles arbitrarily deep trees. */ -fun TreeNode.mapValues(transform: (T) -> R): TreeNode { +public fun TreeNode.mapValues(transform: (T) -> R): TreeNode { val newRoot = TreeNode(transform(value), treeIterator) val stack = ArrayDeque, TreeNode>>() stack.addLast(this to newRoot) @@ -44,13 +44,13 @@ fun TreeNode.mapValues(transform: (T) -> R): TreeNode { } /** Returns an independent deep copy of this subtree (same values, same shape, new nodes). */ -fun TreeNode.deepCopy(): TreeNode = mapValues { it } +public fun TreeNode.deepCopy(): TreeNode = mapValues { it } /** * Structural equality: `true` when [other] holds the same values in the same shape. Unlike * [TreeNode]'s reference equality, this compares the entire subtree. Stack-safe. */ -fun TreeNode.structurallyEquals(other: TreeNode): Boolean { +public fun TreeNode.structurallyEquals(other: TreeNode): Boolean { val stack = ArrayDeque, TreeNode>>() stack.addLast(this to other) while (stack.isNotEmpty()) { diff --git a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeNavigationExt.kt b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeNavigationExt.kt index 1d253bc..d5b0ac9 100644 --- a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeNavigationExt.kt +++ b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeNavigationExt.kt @@ -1,13 +1,13 @@ package com.github.adriankuta.datastructure.tree /** `true` when this node has no children. */ -val TreeNode.isLeaf: Boolean get() = children.isEmpty() +public val TreeNode.isLeaf: Boolean get() = children.isEmpty() /** The number of direct children of this node. */ -val TreeNode.degree: Int get() = children.size +public val TreeNode.degree: Int get() = children.size /** Walks up the parent chain and returns the topmost ancestor (the tree root). */ -fun TreeNode.root(): TreeNode { +public fun TreeNode.root(): TreeNode { var node = this var parent = node.parent while (parent != null) { @@ -18,7 +18,7 @@ fun TreeNode.root(): TreeNode { } /** The chain of ancestors from the immediate [parent] up to (and including) the root. */ -fun TreeNode.ancestors(): List> { +public fun TreeNode.ancestors(): List> { val result = mutableListOf>() var parent = this.parent while (parent != null) { @@ -29,13 +29,13 @@ fun TreeNode.ancestors(): List> { } /** The other children of this node's parent (excludes this node). Empty for the root. */ -fun TreeNode.siblings(): List> = +public fun TreeNode.siblings(): List> = parent?.children?.filter { it !== this } ?: emptyList() /** All leaf nodes in this subtree, in pre-order. */ -fun TreeNode.leaves(): List> = +public fun TreeNode.leaves(): List> = preOrderSequence().filter { it.isLeaf }.toList() /** All nodes in this subtree except this node, in pre-order. */ -fun TreeNode.descendants(): List> = +public fun TreeNode.descendants(): List> = preOrderSequence().filter { it !== this }.toList() diff --git a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeSequenceExt.kt b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeSequenceExt.kt index 1429f56..65fdb17 100644 --- a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeSequenceExt.kt +++ b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/TreeNodeSequenceExt.kt @@ -10,7 +10,7 @@ import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators * traversal up front. Pairs with the Kotlin stdlib, e.g. * `root.asSequence().map { it.value }.firstOrNull { it == target }`. */ -fun TreeNode.asSequence( +public fun TreeNode.asSequence( order: TreeNodeIterators = TreeNodeIterators.PreOrder, ): Sequence> { val self = this @@ -22,10 +22,10 @@ fun TreeNode.asSequence( } /** Lazy pre-order traversal as a [Sequence]. */ -fun TreeNode.preOrderSequence(): Sequence> = asSequence(TreeNodeIterators.PreOrder) +public fun TreeNode.preOrderSequence(): Sequence> = asSequence(TreeNodeIterators.PreOrder) /** Lazy post-order traversal as a [Sequence]. */ -fun TreeNode.postOrderSequence(): Sequence> = asSequence(TreeNodeIterators.PostOrder) +public fun TreeNode.postOrderSequence(): Sequence> = asSequence(TreeNodeIterators.PostOrder) /** Lazy level-order (breadth-first) traversal as a [Sequence]. */ -fun TreeNode.levelOrderSequence(): Sequence> = asSequence(TreeNodeIterators.LevelOrder) +public fun TreeNode.levelOrderSequence(): Sequence> = asSequence(TreeNodeIterators.LevelOrder) diff --git a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/exceptions/TreeNodeException.kt b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/exceptions/TreeNodeException.kt index a4d4cc3..5fe23a5 100644 --- a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/exceptions/TreeNodeException.kt +++ b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/exceptions/TreeNodeException.kt @@ -2,5 +2,5 @@ package com.github.adriankuta.datastructure.tree.exceptions import kotlin.jvm.JvmOverloads -class TreeNodeException @JvmOverloads constructor(message: String? = null, cause: Throwable? = null) : +public class TreeNodeException @JvmOverloads constructor(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause) \ No newline at end of file diff --git a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/LevelOrderTreeIterator.kt b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/LevelOrderTreeIterator.kt index db852c4..506fb8f 100644 --- a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/LevelOrderTreeIterator.kt +++ b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/LevelOrderTreeIterator.kt @@ -20,7 +20,7 @@ import com.github.adriankuta.datastructure.tree.TreeNode * Output: 1 2 3 4 5 6 7 8 9 10 11 12 13 * ``` */ -class LevelOrderTreeIterator(root: TreeNode) : Iterator> { +public class LevelOrderTreeIterator(root: TreeNode) : Iterator> { private val stack = ArrayDeque>() @@ -28,9 +28,9 @@ class LevelOrderTreeIterator(root: TreeNode) : Iterator> { stack.addLast(root) } - override fun hasNext(): Boolean = stack.isNotEmpty() + public override fun hasNext(): Boolean = stack.isNotEmpty() - override fun next(): TreeNode { + public override fun next(): TreeNode { val node = stack.removeFirst() node.children .forEach { stack.addLast(it) } diff --git a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/PostOrderTreeIterator.kt b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/PostOrderTreeIterator.kt index df167cd..28d6968 100644 --- a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/PostOrderTreeIterator.kt +++ b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/PostOrderTreeIterator.kt @@ -20,7 +20,7 @@ import com.github.adriankuta.datastructure.tree.TreeNode * Output: 10 5 11 12 13 6 2 3 7 8 9 4 1 * ``` */ -class PostOrderTreeIterator(root: TreeNode) : Iterator> { +public class PostOrderTreeIterator(root: TreeNode) : Iterator> { private val result = ArrayDeque>() @@ -37,7 +37,7 @@ class PostOrderTreeIterator(root: TreeNode) : Iterator> { } } - override fun hasNext(): Boolean = result.isNotEmpty() + public override fun hasNext(): Boolean = result.isNotEmpty() - override fun next(): TreeNode = result.removeFirst() + public override fun next(): TreeNode = result.removeFirst() } \ No newline at end of file diff --git a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/PreOrderTreeIterator.kt b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/PreOrderTreeIterator.kt index 5975a88..96fe33f 100644 --- a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/PreOrderTreeIterator.kt +++ b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/PreOrderTreeIterator.kt @@ -20,7 +20,7 @@ import com.github.adriankuta.datastructure.tree.TreeNode * Output: 1 2 5 10 6 11 12 13 3 4 7 8 9 * ``` */ -class PreOrderTreeIterator(root: TreeNode) : Iterator> { +public class PreOrderTreeIterator(root: TreeNode) : Iterator> { private val stack = ArrayDeque>() @@ -28,9 +28,9 @@ class PreOrderTreeIterator(root: TreeNode) : Iterator> { stack.addLast(root) } - override fun hasNext(): Boolean = stack.isNotEmpty() + public override fun hasNext(): Boolean = stack.isNotEmpty() - override fun next(): TreeNode { + public override fun next(): TreeNode { val node = stack.removeLast() node.children .asReversed() diff --git a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/TreeNodeIterators.kt b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/TreeNodeIterators.kt index b66b2ce..3218414 100644 --- a/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/TreeNodeIterators.kt +++ b/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/iterators/TreeNodeIterators.kt @@ -5,7 +5,7 @@ package com.github.adriankuta.datastructure.tree.iterators * @see PostOrder * @see LevelOrder */ -enum class TreeNodeIterators { +public enum class TreeNodeIterators { /** * Tree is iterated by using `Pre-order Traversal Algorithm" * The pre-order traversal is a topologically sorted one, diff --git a/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeStackSafetyTest.kt b/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeStackSafetyTest.kt index 5556b0b..d926173 100644 --- a/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeStackSafetyTest.kt +++ b/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeStackSafetyTest.kt @@ -37,13 +37,11 @@ class TreeNodeStackSafetyTest { @Test fun postOrderIterationDoesNotOverflowOnDeepTree() { - val tree = deepChain().apply { treeIterator = TreeNodeIterators.PostOrder } - assertEquals(depth + 1, tree.toList().size) + assertEquals(depth + 1, deepChain().asSequence(TreeNodeIterators.PostOrder).count()) } @Test fun preOrderIterationDoesNotOverflowOnDeepTree() { - val tree = deepChain().apply { treeIterator = TreeNodeIterators.PreOrder } - assertEquals(depth + 1, tree.toList().size) + assertEquals(depth + 1, deepChain().asSequence(TreeNodeIterators.PreOrder).count()) } } diff --git a/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeTest.kt b/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeTest.kt index e54ff11..1d3d74f 100644 --- a/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeTest.kt +++ b/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeTest.kt @@ -51,7 +51,7 @@ class TreeNodeTest { ) root.removeChild(curdNode) - root.removeChild(gingerTeaNode) + gingerTeaNode.detach() assertEquals( "Root\n" + "└── Beverages\n" + diff --git a/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeUtilitiesTest.kt b/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeUtilitiesTest.kt index d3a02a5..5663f11 100644 --- a/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeUtilitiesTest.kt +++ b/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeUtilitiesTest.kt @@ -1,10 +1,9 @@ package com.github.adriankuta.datastructure.tree -import com.github.adriankuta.datastructure.tree.exceptions.TreeNodeException import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals -import kotlin.test.assertFailsWith +import kotlin.test.assertNull class TreeNodeUtilitiesTest { @@ -43,12 +42,12 @@ class TreeNodeUtilitiesTest { } @Test - fun pathThrowsWhenNotADescendant() { - assertFailsWith { root.path(TreeNode(99)) } + fun pathReturnsNullWhenNotADescendant() { + assertNull(root.path(TreeNode(99))) } @Test - fun pathThrowsWhenDescendantIsRootItself() { - assertFailsWith { root.path(root) } + fun pathReturnsNullWhenDescendantIsRootItself() { + assertNull(root.path(root)) } } diff --git a/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeV4Test.kt b/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeV4Test.kt new file mode 100644 index 0000000..163691f --- /dev/null +++ b/src/commonTest/kotlin/com.github.adriankuta/datastructure.tree/TreeNodeV4Test.kt @@ -0,0 +1,105 @@ +package com.github.adriankuta.datastructure.tree + +import com.github.adriankuta.datastructure.tree.exceptions.TreeNodeException +import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertSame +import kotlin.test.assertTrue + +class TreeNodeV4Test { + + @Test + fun addChildRejectsNodeThatAlreadyHasAParent() { + val a = TreeNode("a") + val b = TreeNode("b") + a.addChild(b) + + val other = TreeNode("other") + assertFailsWith { other.addChild(b) } + } + + @Test + fun addChildRejectsCycles() { + val root = TreeNode("root") + val child = TreeNode("child") + root.addChild(child) + + // Attaching an ancestor under its own descendant would create a cycle. + assertFailsWith { child.addChild(root) } + // Attaching a node under itself is also a cycle. + assertFailsWith { root.addChild(root) } + } + + @Test + fun detachRemovesFromParent() { + val root = TreeNode("root") + val child = TreeNode("child") + root.addChild(child) + + assertTrue(child.detach()) + assertNull(child.parent) + assertContentEquals(emptyList(), root.children) + // Detached node can now be re-attached elsewhere. + val newParent = TreeNode("newParent") + newParent.addChild(child) + assertSame(newParent, child.parent) + } + + @Test + fun detachOnRootReturnsFalse() { + assertFalse(TreeNode("root").detach()) + } + + @Test + fun removeChildOnlyRemovesDirectChildren() { + val root = TreeNode("root") + val parent = TreeNode("parent") + val grandchild = TreeNode("grandchild") + root.addChild(parent) + parent.addChild(grandchild) + + // grandchild is not a direct child of root -> no-op, returns false. + assertFalse(root.removeChild(grandchild)) + assertSame(parent, grandchild.parent) + + // direct child removal works. + assertTrue(parent.removeChild(grandchild)) + assertNull(grandchild.parent) + } + + @Test + fun clearOnNonRootKeepsItAttachedToItsParent() { + val root = TreeNode("root") + val branch = TreeNode("branch") + val leaf = TreeNode("leaf") + root.addChild(branch) + branch.addChild(leaf) + + branch.clear() + + assertContentEquals(emptyList(), branch.children) + assertSame(root, branch.parent) // branch stays attached to root + assertContentEquals(listOf(branch), root.children) + assertNull(leaf.parent) // former descendant is detached + } + + @Test + fun iteratorAcceptsExplicitOrderWithoutMutatingDefault() { + val tree = tree(1) { + child(2) { child(4) } + child(3) + } + + val postOrder = tree.iterator(TreeNodeIterators.PostOrder).asSequence().map { it.value }.toList() + assertContentEquals(listOf(4, 2, 3, 1), postOrder) + + // Default order is unchanged (PreOrder). + assertEquals(TreeNodeIterators.PreOrder, tree.treeIterator) + assertContentEquals(listOf(1, 2, 4, 3), tree.map { it.value }) + } +} diff --git a/tree-structure-coroutines/build.gradle.kts b/tree-structure-coroutines/build.gradle.kts index 73d2407..a5d8056 100644 --- a/tree-structure-coroutines/build.gradle.kts +++ b/tree-structure-coroutines/build.gradle.kts @@ -47,6 +47,7 @@ repositories { } kotlin { + explicitApi() jvmToolchain(21) jvm() diff --git a/tree-structure-serialization/build.gradle.kts b/tree-structure-serialization/build.gradle.kts index 4fa5c3b..b1d250a 100644 --- a/tree-structure-serialization/build.gradle.kts +++ b/tree-structure-serialization/build.gradle.kts @@ -48,6 +48,7 @@ repositories { } kotlin { + explicitApi() jvmToolchain(21) jvm() diff --git a/tree-structure-serialization/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/serialization/TreeNodeDto.kt b/tree-structure-serialization/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/serialization/TreeNodeDto.kt index e5e1aee..f5c63d4 100644 --- a/tree-structure-serialization/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/serialization/TreeNodeDto.kt +++ b/tree-structure-serialization/src/commonMain/kotlin/com.github.adriankuta/datastructure/tree/serialization/TreeNodeDto.kt @@ -14,8 +14,8 @@ import kotlinx.serialization.Serializable */ @Serializable public data class TreeNodeDto( - val value: T, - val children: List> = emptyList(), + public val value: T, + public val children: List> = emptyList(), ) /** Converts this subtree into a serializable [TreeNodeDto], preserving values and shape. */