feat!: v4.0 breaking API cleanup + explicitApi

BREAKING changes to the core:
- treeIterator is now a read-only `val`; added `iterator(order)` and use `asSequence(order)`.
- removeChild() only removes a direct child of the receiver; added `detach()` to unhook a node.
- addChild() rejects re-parenting and cycles (throws TreeNodeException); detach() first to move.
- clear() no longer nulls the receiver's own parent; only removes descendants.
- path() returns List<TreeNode<T>>? (null) instead of throwing.

Also:
- Enable strict explicitApi() across core + both modules; add explicit `public` modifiers.
- Update tests for the new contracts + add TreeNodeV4Test; refresh .api baselines.
- README + CHANGELOG (with migration notes); bump version to 4.0.0.

47 JVM tests green.
This commit is contained in:
2026-06-07 18:47:40 +02:00
parent c9bbea59b0
commit 69d19f89e3
22 changed files with 262 additions and 97 deletions

View File

@@ -6,6 +6,33 @@ All notable changes to this project are documented here. The format is based on
## [Unreleased] ## [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<TreeNode<T>>?` (`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] ## [3.4.0]
### Added ### Added
@@ -44,7 +71,8 @@ All notable changes to this project are documented here. The format is based on
## [3.1.3] ## [3.1.3]
- iOS targets and Maven Central (Sonatype Central Portal) publishing. - 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.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.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 [3.1.4]: https://github.com/AdrianKuta/Tree-Data-Structure/releases/tag/v3.1.4

View File

@@ -104,9 +104,8 @@ World
val root = TreeNode("root") val root = TreeNode("root")
// ... build your tree // ... build your tree
// Choose iteration order (default is PreOrder) // Choose iteration order per call (the default order is set in the constructor and is read-only)
root.treeIterator = TreeNodeIterators.PostOrder for (node in root.asSequence(TreeNodeIterators.PostOrder)) println(node.value)
for (node in root) println(node.value)
// Utilities // Utilities
root.nodeCount() // number of descendants 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 root.depth() // distance from current node to the root
val path = root.path(root.children.first()) // nodes from descendant up to 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() val child = root.children.first()
root.removeChild(child) root.removeChild(child) // child is now detached from root
root.clear() // remove entire subtree root.clear() // remove all descendants of root
``` ```
### Lazy traversal with Sequence ### Lazy traversal with Sequence

View File

@@ -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 synthetic fun child (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun clear ()V public final fun clear ()V
public final fun depth ()I public final fun depth ()I
public final fun detach ()Z
public final fun getChildren ()Ljava/util/List; public final fun getChildren ()Ljava/util/List;
public final fun getParent ()Lcom/github/adriankuta/datastructure/tree/TreeNode; public final fun getParent ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun getTreeIterator ()Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators; 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 height ()I
public final fun isRoot ()Z public final fun isRoot ()Z
public fun iterator ()Ljava/util/Iterator; 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 nodeCount ()I
public final fun path (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List; public final fun path (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public final fun prettyString ()Ljava/lang/String; public final fun prettyString ()Ljava/lang/String;
public final fun removeChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z 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; public fun toString ()Ljava/lang/String;
} }

View File

@@ -11,7 +11,7 @@ plugins {
val PUBLISH_GROUP_ID = "com.github.adriankuta" val PUBLISH_GROUP_ID = "com.github.adriankuta"
val PUBLISH_ARTIFACT_ID = "tree-structure" // base artifact; KMP will add -jvm, -ios*, etc. 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 val snapshot: String? by project
@@ -60,6 +60,7 @@ repositories {
} }
kotlin { kotlin {
explicitApi()
jvmToolchain(21) jvmToolchain(21)
jvm() jvm()

View File

@@ -2,7 +2,7 @@ package com.github.adriankuta.datastructure.tree
import kotlin.jvm.JvmSynthetic import kotlin.jvm.JvmSynthetic
interface ChildDeclarationInterface<T> { public interface ChildDeclarationInterface<T> {
/** /**
* This method is used to easily create child in node. * This method is used to easily create child in node.
@@ -20,5 +20,5 @@ interface ChildDeclarationInterface<T> {
* @return New created TreeNode. * @return New created TreeNode.
*/ */
@JvmSynthetic @JvmSynthetic
fun child(value: T, childDeclaration: ChildDeclaration<T>? = null): TreeNode<T> public fun child(value: T, childDeclaration: ChildDeclaration<T>? = null): TreeNode<T>
} }

View File

@@ -27,14 +27,14 @@ import kotlin.jvm.JvmSynthetic
* @param treeIterator the default traversal order used by [iterator]. Prefer the * @param treeIterator the default traversal order used by [iterator]. Prefer the
* `asSequence(order)` / `preOrderSequence()` extensions to choose an order without mutating state. * `asSequence(order)` / `preOrderSequence()` extensions to choose an order without mutating state.
*/ */
open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOrder) : Iterable<TreeNode<T>>, ChildDeclarationInterface<T> { public open class TreeNode<T>(public val value: T, public val treeIterator: TreeNodeIterators = PreOrder) : Iterable<TreeNode<T>>, ChildDeclarationInterface<T> {
private var _parent: TreeNode<T>? = null private var _parent: TreeNode<T>? = null
/** /**
* The converse notion of a child, an immediate ancestor. * The converse notion of a child, an immediate ancestor.
*/ */
val parent: TreeNode<T>? public val parent: TreeNode<T>?
get() = _parent get() = _parent
private val _children = mutableListOf<TreeNode<T>>() private val _children = mutableListOf<TreeNode<T>>()
@@ -42,28 +42,53 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
/** /**
* A group of nodes with the same parent. * A group of nodes with the same parent.
*/ */
val children: List<TreeNode<T>> public val children: List<TreeNode<T>>
get() = _children get() = _children
/** /**
* Checks whether the current tree node is the root of the tree * 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. * @return `true` if the current tree node is root of the tree, `false` otherwise.
*/ */
val isRoot: Boolean public val isRoot: Boolean
get() = _parent == null 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<T>) { public fun addChild(child: TreeNode<T>) {
if (child._parent != null) {
throw TreeNodeException("$child already has a parent; call detach() before re-attaching it.")
}
var ancestor: TreeNode<T>? = this
while (ancestor != null) {
if (ancestor === child) {
throw TreeNodeException("Adding $child here would create a cycle.")
}
ancestor = ancestor._parent
}
child._parent = this child._parent = this
_children.add(child) _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 @JvmSynthetic
override fun child(value: T, childDeclaration: ChildDeclaration<T>?): TreeNode<T> { public override fun child(value: T, childDeclaration: ChildDeclaration<T>?): TreeNode<T> {
val newChild = TreeNode(value) val newChild = TreeNode(value)
newChild._parent = this newChild._parent = this
if (childDeclaration != null) if (childDeclaration != null)
@@ -73,21 +98,24 @@ open class TreeNode<T>(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<T>): Boolean { public fun removeChild(child: TreeNode<T>): Boolean {
val removed = child._parent?._children?.remove(child) val removed = _children.remove(child)
child._parent = null if (removed) child._parent = null
return removed ?: false return removed
} }
/** /**
* This function go through tree and counts children. Root element is not counted. * This function go through tree and counts children. Root element is not counted.
* @return All child and nested child count. * @return All child and nested child count.
*/ */
fun nodeCount(): Int { public fun nodeCount(): Int {
var count = 0 var count = 0
val stack = ArrayDeque<TreeNode<T>>() val stack = ArrayDeque<TreeNode<T>>()
stack.addAll(_children) stack.addAll(_children)
@@ -102,7 +130,7 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
/** /**
* @return The number of edges on the longest path between current node and a descendant leaf. * @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 var maxDepth = 0
val stack = ArrayDeque<Pair<TreeNode<T>, Int>>() val stack = ArrayDeque<Pair<TreeNode<T>, Int>>()
stack.addLast(this to 0) stack.addLast(this to 0)
@@ -118,7 +146,7 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
* Distance is the number of edges along the shortest path between two nodes. * Distance is the number of edges along the shortest path between two nodes.
* @return The distance between current node and the root. * @return The distance between current node and the root.
*/ */
fun depth(): Int { public fun depth(): Int {
var depth = 0 var depth = 0
var tempParent = parent var tempParent = parent
@@ -130,53 +158,52 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
} }
/** /**
* Returns the collection of nodes, which connect the current node * Returns the chain of nodes from [descendant] up to and including this node, or `null` if
* with its descendants * [descendant] is not a strict descendant of this node.
* *
* @param descendant the bottom child node for which the path is calculated * @param descendant the node to walk up from.
* @return collection of nodes, which connect the current node with its descendants * @return the path `[descendant, …, this]`, or `null` if [descendant] is the root or is not
* @throws TreeNodeException exception that may be thrown in case if the * located in this node's subtree.
* current node does not have such descendant or if the
* specified tree node is root
*/ */
@Throws(TreeNodeException::class) public fun path(descendant: TreeNode<T>): List<TreeNode<T>>? {
fun path(descendant: TreeNode<T>): List<TreeNode<T>> { if (descendant.isRoot) return null
val path = mutableListOf<TreeNode<T>>() val path = mutableListOf<TreeNode<T>>()
var node = descendant var node = descendant
path.add(node) path.add(node)
while (!node.isRoot) { while (!node.isRoot) {
node = node.parent!! node = node.parent!!
path.add(node) path.add(node)
if (node == this) if (node == this) return path
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() { public fun clear() {
val all = ArrayDeque<TreeNode<T>>() val descendants = ArrayDeque<TreeNode<T>>()
val stack = ArrayDeque<TreeNode<T>>() val stack = ArrayDeque<TreeNode<T>>()
stack.addLast(this) stack.addAll(_children)
while (stack.isNotEmpty()) { while (stack.isNotEmpty()) {
val node = stack.removeLast() val node = stack.removeLast()
all.addLast(node) descendants.addLast(node)
stack.addAll(node._children) stack.addAll(node._children)
} }
all.forEach { node -> descendants.forEach { node ->
node._parent = null node._parent = null
node._children.clear() node._children.clear()
} }
_children.clear()
} }
override fun toString(): String { public override fun toString(): String {
return value.toString() return value.toString()
} }
fun prettyString(): String { public fun prettyString(): String {
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()
print(stringBuilder, "", "") print(stringBuilder, "", "")
return stringBuilder.toString() return stringBuilder.toString()
@@ -198,9 +225,14 @@ open class TreeNode<T>(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<TreeNode<T>> = when (treeIterator) { public override fun iterator(): Iterator<TreeNode<T>> = iterator(treeIterator)
/** Returns an iterator over this node and its descendants in the given [order]. */
public fun iterator(order: TreeNodeIterators): Iterator<TreeNode<T>> = when (order) {
PreOrder -> PreOrderTreeIterator(this) PreOrder -> PreOrderTreeIterator(this)
PostOrder -> PostOrderTreeIterator(this) PostOrder -> PostOrderTreeIterator(this)
LevelOrder -> LevelOrderTreeIterator(this) LevelOrder -> LevelOrderTreeIterator(this)

View File

@@ -3,7 +3,7 @@ package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import kotlin.jvm.JvmSynthetic import kotlin.jvm.JvmSynthetic
typealias ChildDeclaration<T> = ChildDeclarationInterface<T>.() -> Unit public typealias ChildDeclaration<T> = ChildDeclarationInterface<T>.() -> Unit
/** /**
* This method can be used to initialize new tree. * This method can be used to initialize new tree.
@@ -14,7 +14,7 @@ typealias ChildDeclaration<T> = ChildDeclarationInterface<T>.() -> Unit
* @see [ChildDeclarationInterface.child] * @see [ChildDeclarationInterface.child]
*/ */
@JvmSynthetic @JvmSynthetic
inline fun <reified T> tree( public inline fun <reified T> tree(
root: T, root: T,
defaultIterator: TreeNodeIterators = TreeNodeIterators.PreOrder, defaultIterator: TreeNodeIterators = TreeNodeIterators.PreOrder,
childDeclaration: ChildDeclaration<T> childDeclaration: ChildDeclaration<T>

View File

@@ -1,34 +1,34 @@
package com.github.adriankuta.datastructure.tree package com.github.adriankuta.datastructure.tree
/** Returns the first node (pre-order) whose value matches [predicate], or `null`. Short-circuits. */ /** Returns the first node (pre-order) whose value matches [predicate], or `null`. Short-circuits. */
fun <T> TreeNode<T>.findNode(predicate: (T) -> Boolean): TreeNode<T>? = public fun <T> TreeNode<T>.findNode(predicate: (T) -> Boolean): TreeNode<T>? =
preOrderSequence().firstOrNull { predicate(it.value) } preOrderSequence().firstOrNull { predicate(it.value) }
/** All nodes (pre-order) whose value matches [predicate]. */ /** All nodes (pre-order) whose value matches [predicate]. */
fun <T> TreeNode<T>.filterNodes(predicate: (T) -> Boolean): List<TreeNode<T>> = public fun <T> TreeNode<T>.filterNodes(predicate: (T) -> Boolean): List<TreeNode<T>> =
preOrderSequence().filter { predicate(it.value) }.toList() preOrderSequence().filter { predicate(it.value) }.toList()
/** `true` if any node's value matches [predicate]. Short-circuits. */ /** `true` if any node's value matches [predicate]. Short-circuits. */
fun <T> TreeNode<T>.anyNode(predicate: (T) -> Boolean): Boolean = public fun <T> TreeNode<T>.anyNode(predicate: (T) -> Boolean): Boolean =
preOrderSequence().any { predicate(it.value) } preOrderSequence().any { predicate(it.value) }
/** `true` if every node's value matches [predicate]. Short-circuits. */ /** `true` if every node's value matches [predicate]. Short-circuits. */
fun <T> TreeNode<T>.allNodes(predicate: (T) -> Boolean): Boolean = public fun <T> TreeNode<T>.allNodes(predicate: (T) -> Boolean): Boolean =
preOrderSequence().all { predicate(it.value) } preOrderSequence().all { predicate(it.value) }
/** Counts nodes whose value matches [predicate]. */ /** Counts nodes whose value matches [predicate]. */
fun <T> TreeNode<T>.countNodes(predicate: (T) -> Boolean): Int = public fun <T> TreeNode<T>.countNodes(predicate: (T) -> Boolean): Int =
preOrderSequence().count { predicate(it.value) } preOrderSequence().count { predicate(it.value) }
/** Folds [operation] over all nodes in pre-order, starting from [initial]. */ /** Folds [operation] over all nodes in pre-order, starting from [initial]. */
fun <T, R> TreeNode<T>.foldNodes(initial: R, operation: (acc: R, node: TreeNode<T>) -> R): R = public fun <T, R> TreeNode<T>.foldNodes(initial: R, operation: (acc: R, node: TreeNode<T>) -> R): R =
preOrderSequence().fold(initial) { acc, node -> operation(acc, node) } 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 * 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. * left untouched. Stack-safe (iterative), so it handles arbitrarily deep trees.
*/ */
fun <T, R> TreeNode<T>.mapValues(transform: (T) -> R): TreeNode<R> { public fun <T, R> TreeNode<T>.mapValues(transform: (T) -> R): TreeNode<R> {
val newRoot = TreeNode(transform(value), treeIterator) val newRoot = TreeNode(transform(value), treeIterator)
val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<R>>>() val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<R>>>()
stack.addLast(this to newRoot) stack.addLast(this to newRoot)
@@ -44,13 +44,13 @@ fun <T, R> TreeNode<T>.mapValues(transform: (T) -> R): TreeNode<R> {
} }
/** Returns an independent deep copy of this subtree (same values, same shape, new nodes). */ /** Returns an independent deep copy of this subtree (same values, same shape, new nodes). */
fun <T> TreeNode<T>.deepCopy(): TreeNode<T> = mapValues { it } public fun <T> TreeNode<T>.deepCopy(): TreeNode<T> = mapValues { it }
/** /**
* Structural equality: `true` when [other] holds the same values in the same shape. Unlike * 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. * [TreeNode]'s reference equality, this compares the entire subtree. Stack-safe.
*/ */
fun <T> TreeNode<T>.structurallyEquals(other: TreeNode<T>): Boolean { public fun <T> TreeNode<T>.structurallyEquals(other: TreeNode<T>): Boolean {
val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<T>>>() val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<T>>>()
stack.addLast(this to other) stack.addLast(this to other)
while (stack.isNotEmpty()) { while (stack.isNotEmpty()) {

View File

@@ -1,13 +1,13 @@
package com.github.adriankuta.datastructure.tree package com.github.adriankuta.datastructure.tree
/** `true` when this node has no children. */ /** `true` when this node has no children. */
val <T> TreeNode<T>.isLeaf: Boolean get() = children.isEmpty() public val <T> TreeNode<T>.isLeaf: Boolean get() = children.isEmpty()
/** The number of direct children of this node. */ /** The number of direct children of this node. */
val <T> TreeNode<T>.degree: Int get() = children.size public val <T> TreeNode<T>.degree: Int get() = children.size
/** Walks up the parent chain and returns the topmost ancestor (the tree root). */ /** Walks up the parent chain and returns the topmost ancestor (the tree root). */
fun <T> TreeNode<T>.root(): TreeNode<T> { public fun <T> TreeNode<T>.root(): TreeNode<T> {
var node = this var node = this
var parent = node.parent var parent = node.parent
while (parent != null) { while (parent != null) {
@@ -18,7 +18,7 @@ fun <T> TreeNode<T>.root(): TreeNode<T> {
} }
/** The chain of ancestors from the immediate [parent] up to (and including) the root. */ /** The chain of ancestors from the immediate [parent] up to (and including) the root. */
fun <T> TreeNode<T>.ancestors(): List<TreeNode<T>> { public fun <T> TreeNode<T>.ancestors(): List<TreeNode<T>> {
val result = mutableListOf<TreeNode<T>>() val result = mutableListOf<TreeNode<T>>()
var parent = this.parent var parent = this.parent
while (parent != null) { while (parent != null) {
@@ -29,13 +29,13 @@ fun <T> TreeNode<T>.ancestors(): List<TreeNode<T>> {
} }
/** The other children of this node's parent (excludes this node). Empty for the root. */ /** The other children of this node's parent (excludes this node). Empty for the root. */
fun <T> TreeNode<T>.siblings(): List<TreeNode<T>> = public fun <T> TreeNode<T>.siblings(): List<TreeNode<T>> =
parent?.children?.filter { it !== this } ?: emptyList() parent?.children?.filter { it !== this } ?: emptyList()
/** All leaf nodes in this subtree, in pre-order. */ /** All leaf nodes in this subtree, in pre-order. */
fun <T> TreeNode<T>.leaves(): List<TreeNode<T>> = public fun <T> TreeNode<T>.leaves(): List<TreeNode<T>> =
preOrderSequence().filter { it.isLeaf }.toList() preOrderSequence().filter { it.isLeaf }.toList()
/** All nodes in this subtree except this node, in pre-order. */ /** All nodes in this subtree except this node, in pre-order. */
fun <T> TreeNode<T>.descendants(): List<TreeNode<T>> = public fun <T> TreeNode<T>.descendants(): List<TreeNode<T>> =
preOrderSequence().filter { it !== this }.toList() preOrderSequence().filter { it !== this }.toList()

View File

@@ -10,7 +10,7 @@ import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
* traversal up front. Pairs with the Kotlin stdlib, e.g. * traversal up front. Pairs with the Kotlin stdlib, e.g.
* `root.asSequence().map { it.value }.firstOrNull { it == target }`. * `root.asSequence().map { it.value }.firstOrNull { it == target }`.
*/ */
fun <T> TreeNode<T>.asSequence( public fun <T> TreeNode<T>.asSequence(
order: TreeNodeIterators = TreeNodeIterators.PreOrder, order: TreeNodeIterators = TreeNodeIterators.PreOrder,
): Sequence<TreeNode<T>> { ): Sequence<TreeNode<T>> {
val self = this val self = this
@@ -22,10 +22,10 @@ fun <T> TreeNode<T>.asSequence(
} }
/** Lazy pre-order traversal as a [Sequence]. */ /** Lazy pre-order traversal as a [Sequence]. */
fun <T> TreeNode<T>.preOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PreOrder) public fun <T> TreeNode<T>.preOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PreOrder)
/** Lazy post-order traversal as a [Sequence]. */ /** Lazy post-order traversal as a [Sequence]. */
fun <T> TreeNode<T>.postOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PostOrder) public fun <T> TreeNode<T>.postOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PostOrder)
/** Lazy level-order (breadth-first) traversal as a [Sequence]. */ /** Lazy level-order (breadth-first) traversal as a [Sequence]. */
fun <T> TreeNode<T>.levelOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.LevelOrder) public fun <T> TreeNode<T>.levelOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.LevelOrder)

View File

@@ -2,5 +2,5 @@ package com.github.adriankuta.datastructure.tree.exceptions
import kotlin.jvm.JvmOverloads 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) RuntimeException(message, cause)

View File

@@ -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 * Output: 1 2 3 4 5 6 7 8 9 10 11 12 13
* ``` * ```
*/ */
class LevelOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> { public class LevelOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
private val stack = ArrayDeque<TreeNode<T>>() private val stack = ArrayDeque<TreeNode<T>>()
@@ -28,9 +28,9 @@ class LevelOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
stack.addLast(root) stack.addLast(root)
} }
override fun hasNext(): Boolean = stack.isNotEmpty() public override fun hasNext(): Boolean = stack.isNotEmpty()
override fun next(): TreeNode<T> { public override fun next(): TreeNode<T> {
val node = stack.removeFirst() val node = stack.removeFirst()
node.children node.children
.forEach { stack.addLast(it) } .forEach { stack.addLast(it) }

View File

@@ -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 * Output: 10 5 11 12 13 6 2 3 7 8 9 4 1
* ``` * ```
*/ */
class PostOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> { public class PostOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
private val result = ArrayDeque<TreeNode<T>>() private val result = ArrayDeque<TreeNode<T>>()
@@ -37,7 +37,7 @@ class PostOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
} }
} }
override fun hasNext(): Boolean = result.isNotEmpty() public override fun hasNext(): Boolean = result.isNotEmpty()
override fun next(): TreeNode<T> = result.removeFirst() public override fun next(): TreeNode<T> = result.removeFirst()
} }

View File

@@ -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 * Output: 1 2 5 10 6 11 12 13 3 4 7 8 9
* ``` * ```
*/ */
class PreOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> { public class PreOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
private val stack = ArrayDeque<TreeNode<T>>() private val stack = ArrayDeque<TreeNode<T>>()
@@ -28,9 +28,9 @@ class PreOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
stack.addLast(root) stack.addLast(root)
} }
override fun hasNext(): Boolean = stack.isNotEmpty() public override fun hasNext(): Boolean = stack.isNotEmpty()
override fun next(): TreeNode<T> { public override fun next(): TreeNode<T> {
val node = stack.removeLast() val node = stack.removeLast()
node.children node.children
.asReversed() .asReversed()

View File

@@ -5,7 +5,7 @@ package com.github.adriankuta.datastructure.tree.iterators
* @see PostOrder * @see PostOrder
* @see LevelOrder * @see LevelOrder
*/ */
enum class TreeNodeIterators { public enum class TreeNodeIterators {
/** /**
* Tree is iterated by using `Pre-order Traversal Algorithm" * Tree is iterated by using `Pre-order Traversal Algorithm"
* The pre-order traversal is a topologically sorted one, * The pre-order traversal is a topologically sorted one,

View File

@@ -37,13 +37,11 @@ class TreeNodeStackSafetyTest {
@Test @Test
fun postOrderIterationDoesNotOverflowOnDeepTree() { fun postOrderIterationDoesNotOverflowOnDeepTree() {
val tree = deepChain().apply { treeIterator = TreeNodeIterators.PostOrder } assertEquals(depth + 1, deepChain().asSequence(TreeNodeIterators.PostOrder).count())
assertEquals(depth + 1, tree.toList().size)
} }
@Test @Test
fun preOrderIterationDoesNotOverflowOnDeepTree() { fun preOrderIterationDoesNotOverflowOnDeepTree() {
val tree = deepChain().apply { treeIterator = TreeNodeIterators.PreOrder } assertEquals(depth + 1, deepChain().asSequence(TreeNodeIterators.PreOrder).count())
assertEquals(depth + 1, tree.toList().size)
} }
} }

View File

@@ -51,7 +51,7 @@ class TreeNodeTest {
) )
root.removeChild(curdNode) root.removeChild(curdNode)
root.removeChild(gingerTeaNode) gingerTeaNode.detach()
assertEquals( assertEquals(
"Root\n" + "Root\n" +
"└── Beverages\n" + "└── Beverages\n" +

View File

@@ -1,10 +1,9 @@
package com.github.adriankuta.datastructure.tree package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.exceptions.TreeNodeException
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertContentEquals import kotlin.test.assertContentEquals
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertNull
class TreeNodeUtilitiesTest { class TreeNodeUtilitiesTest {
@@ -43,12 +42,12 @@ class TreeNodeUtilitiesTest {
} }
@Test @Test
fun pathThrowsWhenNotADescendant() { fun pathReturnsNullWhenNotADescendant() {
assertFailsWith<TreeNodeException> { root.path(TreeNode(99)) } assertNull(root.path(TreeNode(99)))
} }
@Test @Test
fun pathThrowsWhenDescendantIsRootItself() { fun pathReturnsNullWhenDescendantIsRootItself() {
assertFailsWith<TreeNodeException> { root.path(root) } assertNull(root.path(root))
} }
} }

View File

@@ -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<TreeNodeException> { 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<TreeNodeException> { child.addChild(root) }
// Attaching a node under itself is also a cycle.
assertFailsWith<TreeNodeException> { 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 })
}
}

View File

@@ -47,6 +47,7 @@ repositories {
} }
kotlin { kotlin {
explicitApi()
jvmToolchain(21) jvmToolchain(21)
jvm() jvm()

View File

@@ -48,6 +48,7 @@ repositories {
} }
kotlin { kotlin {
explicitApi()
jvmToolchain(21) jvmToolchain(21)
jvm() jvm()

View File

@@ -14,8 +14,8 @@ import kotlinx.serialization.Serializable
*/ */
@Serializable @Serializable
public data class TreeNodeDto<T>( public data class TreeNodeDto<T>(
val value: T, public val value: T,
val children: List<TreeNodeDto<T>> = emptyList(), public val children: List<TreeNodeDto<T>> = emptyList(),
) )
/** Converts this subtree into a serializable [TreeNodeDto], preserving values and shape. */ /** Converts this subtree into a serializable [TreeNodeDto], preserving values and shape. */