feat: add structural mutation helpers (insert/move/replace/sort children) (#34) (#41)

This commit is contained in:
2026-06-07 22:35:04 +02:00
committed by GitHub
parent 1f60b854de
commit 06eae4841e
4 changed files with 270 additions and 2 deletions

View File

@@ -6,6 +6,10 @@ All notable changes to this project are documented here. The format is based on
## [Unreleased]
### Added
- Structural mutation helpers on `TreeNode`: `insertChild`, `removeChildAt`, `replaceChild`,
`moveChild`, `addChildren`, and `sortChildren`.
### Changed
- Rewrote the README for clarity: one consistent example tree, task-oriented sections
(building, traversal, navigation, functional, utilities, mutating), per-module usage, and a

View File

@@ -10,6 +10,7 @@ public class com/github/adriankuta/datastructure/tree/TreeNode : com/github/adri
public fun <init> (Ljava/lang/Object;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)V
public synthetic fun <init> (Ljava/lang/Object;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun addChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public final fun addChildren ([Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
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
@@ -19,13 +20,18 @@ public class com/github/adriankuta/datastructure/tree/TreeNode : com/github/adri
public final fun getTreeIterator ()Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public final fun getValue ()Ljava/lang/Object;
public final fun height ()I
public final fun insertChild (ILcom/github/adriankuta/datastructure/tree/TreeNode;)V
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 moveChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;I)Z
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 removeChildAt (I)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun replaceChild (ILcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun sortChildren (Ljava/util/Comparator;)V
public fun toString ()Ljava/lang/String;
}

View File

@@ -61,6 +61,19 @@ public open class TreeNode<T>(public val value: T, public val treeIterator: Tree
* a cycle (i.e. [child] is this node or one of its ancestors).
*/
public fun addChild(child: TreeNode<T>) {
validateAttachable(child)
child._parent = this
_children.add(child)
}
/**
* Validates that [child] can be attached as a direct child of this node, throwing if it cannot.
*
* @param child the node about to be attached.
* @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).
*/
private fun validateAttachable(child: TreeNode<T>) {
if (child._parent != null) {
throw TreeNodeException("$child already has a parent; call detach() before re-attaching it.")
}
@@ -78,8 +91,6 @@ public open class TreeNode<T>(public val value: T, public val treeIterator: Tree
ancestor = ancestor._parent
}
}
child._parent = this
_children.add(child)
}
/**
@@ -118,6 +129,102 @@ public open class TreeNode<T>(public val value: T, public val treeIterator: Tree
return removed
}
/**
* Inserts [child] as a direct child of this node at the given [index], shifting any existing
* children at and after [index] one position to the right.
*
* @param index the position at which to insert [child]; must be in `0..children.size`.
* @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 IndexOutOfBoundsException if [index] is out of range.
* @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).
*/
public fun insertChild(index: Int, child: TreeNode<T>) {
validateAttachable(child)
child._parent = this
_children.add(index, child)
}
/**
* Removes the direct child at the given [index], detaching it (its parent becomes `null`).
*
* @param index the position of the child to remove; must be in `0 until children.size`.
* @return the detached child that was at [index].
* @throws IndexOutOfBoundsException if [index] is out of range.
*/
public fun removeChildAt(index: Int): TreeNode<T> {
val removed = _children.removeAt(index)
removed._parent = null
return removed
}
/**
* Replaces the direct child at the given [index] with [child], detaching the previous child
* (its parent becomes `null`).
*
* @param index the position of the child to replace; must be in `0 until children.size`.
* @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.
* @return the previous child that was at [index], now detached.
* @throws IndexOutOfBoundsException if [index] is out of range.
* @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).
*/
public fun replaceChild(index: Int, child: TreeNode<T>): TreeNode<T> {
validateAttachable(child)
val old = _children[index]
old._parent = null
child._parent = this
_children[index] = child
return old
}
/**
* Moves an existing direct [child] to a new position within this node's [children].
*
* [toIndex] is coerced into the valid range, so out-of-range targets clamp to the first or last
* position. Because [child] is already a direct child, no re-parenting or cycle check is needed.
*
* @param child the node to reorder; must already be a direct child of this node.
* @param toIndex the target position for [child] after removal, coerced into `0..children.size`.
* @return `true` if [child] was a direct child and has been moved; `false` otherwise.
*/
public fun moveChild(child: TreeNode<T>, toIndex: Int): Boolean {
val from = _children.indexOf(child)
if (from < 0) return false
_children.removeAt(from)
_children.add(toIndex.coerceIn(0, _children.size), child)
return true
}
/**
* Adds each of [children] as a direct child of this node, in order, validating each one the same
* way as [addChild].
*
* Validation is performed per node as it is added, so if one node fails the children added before
* it remain attached (the same partial-application behaviour as calling [addChild] in a loop).
*
* @param children nodes that are not already attached to a tree.
* @throws TreeNodeException if any node already has a parent, or if attaching it here would create
* a cycle (i.e. it is this node or one of its ancestors).
*/
public fun addChildren(vararg children: TreeNode<T>) {
for (child in children) {
addChild(child)
}
}
/**
* Sorts this node's direct [children] in place according to the given [comparator]. Only the
* immediate children are reordered; their subtrees are left untouched.
*
* @param comparator the comparator used to order the children.
*/
public fun sortChildren(comparator: Comparator<TreeNode<T>>) {
_children.sortWith(comparator)
}
/**
* This function go through tree and counts children. Root element is not counted.
* @return All child and nested child count.

View File

@@ -0,0 +1,151 @@
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.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
class TreeNodeMutationTest {
@Test
fun insertChildAtStartMiddleAndEnd() {
val root = TreeNode("root")
val a = TreeNode("a")
val b = TreeNode("b")
val c = TreeNode("c")
val d = TreeNode("d")
root.insertChild(0, b) // [b]
root.insertChild(0, a) // [a, b]
root.insertChild(2, d) // [a, b, d] (end)
root.insertChild(2, c) // [a, b, c, d] (middle)
assertContentEquals(listOf(a, b, c, d), root.children)
// Each inserted node is re-parented to root.
assertSame(root, a.parent)
assertSame(root, b.parent)
assertSame(root, c.parent)
assertSame(root, d.parent)
}
@Test
fun removeChildAtReturnsDetachedNodeAndClearsParent() {
val root = TreeNode("root")
val a = TreeNode("a")
val b = TreeNode("b")
val c = TreeNode("c")
root.addChildren(a, b, c)
val removed = root.removeChildAt(1)
assertSame(b, removed)
assertNull(removed.parent)
assertContentEquals(listOf(a, c), root.children)
}
@Test
fun replaceChildSwapsAndDetachesTheOld() {
val root = TreeNode("root")
val a = TreeNode("a")
val b = TreeNode("b")
val replacement = TreeNode("replacement")
root.addChildren(a, b)
val old = root.replaceChild(0, replacement)
assertSame(a, old)
assertNull(old.parent)
assertSame(root, replacement.parent)
assertContentEquals(listOf(replacement, b), root.children)
}
@Test
fun moveChildReordersChildren() {
val root = TreeNode("root")
val a = TreeNode("a")
val b = TreeNode("b")
val c = TreeNode("c")
root.addChildren(a, b, c)
assertTrue(root.moveChild(a, 2))
assertContentEquals(listOf(b, c, a), root.children)
// Parent pointer is unchanged after a move.
assertSame(root, a.parent)
}
@Test
fun moveChildReturnsFalseForNonChild() {
val root = TreeNode("root")
val a = TreeNode("a")
root.addChild(a)
val stranger = TreeNode("stranger")
assertFalse(root.moveChild(stranger, 0))
assertContentEquals(listOf(a), root.children)
}
@Test
fun addChildrenAppendsAllInOrder() {
val root = TreeNode("root")
val a = TreeNode("a")
val b = TreeNode("b")
val c = TreeNode("c")
root.addChildren(a, b, c)
assertContentEquals(listOf(a, b, c), root.children)
assertSame(root, a.parent)
assertSame(root, b.parent)
assertSame(root, c.parent)
}
@Test
fun addChildrenRejectsNodeThatAlreadyHasAParent() {
val root = TreeNode("root")
val attached = TreeNode("attached")
TreeNode("other").addChild(attached)
assertFailsWith<TreeNodeException> { root.addChildren(attached) }
}
@Test
fun insertChildRejectsNodeThatAlreadyHasAParent() {
val root = TreeNode("root")
val attached = TreeNode("attached")
TreeNode("other").addChild(attached)
assertFailsWith<TreeNodeException> { root.insertChild(0, attached) }
}
@Test
fun replaceChildRejectsNodeThatAlreadyHasAParent() {
val root = TreeNode("root")
val existing = TreeNode("existing")
root.addChild(existing)
val attached = TreeNode("attached")
TreeNode("other").addChild(attached)
assertFailsWith<TreeNodeException> { root.replaceChild(0, attached) }
// The original child is untouched after a failed replace.
assertContentEquals(listOf(existing), root.children)
assertSame(root, existing.parent)
}
@Test
fun sortChildrenReordersByComparator() {
val root = TreeNode("root")
val c = TreeNode("c")
val a = TreeNode("a")
val b = TreeNode("b")
root.addChildren(c, a, b)
root.sortChildren(compareBy { it.value })
assertContentEquals(listOf(a, b, c), root.children)
}
}