mirror of
https://github.com/AdrianKuta/Tree-Data-Structure.git
synced 2026-06-20 03:10:14 +02:00
Compare commits
2 Commits
c9bbea59b0
...
v4.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bec1fe02a7 | ||
|
|
69d19f89e3 |
30
CHANGELOG.md
30
CHANGELOG.md
@@ -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
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -20,14 +20,14 @@ Gradle (Kotlin DSL):
|
|||||||
```kotlin
|
```kotlin
|
||||||
// commonMain for KMP projects, or any sourceSet/module where you need it
|
// commonMain for KMP projects, or any sourceSet/module where you need it
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.github.adriankuta:tree-structure:3.1.5") // see badge above for the latest version
|
implementation("com.github.adriankuta:tree-structure:4.0.0") // see badge above for the latest version
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Gradle (Groovy):
|
Gradle (Groovy):
|
||||||
```groovy
|
```groovy
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "com.github.adriankuta:tree-structure:3.1.5" // see badge above for the latest
|
implementation "com.github.adriankuta:tree-structure:4.0.0" // see badge above for the latest
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ Maven:
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.github.adriankuta</groupId>
|
<groupId>com.github.adriankuta</groupId>
|
||||||
<artifactId>tree-structure</artifactId>
|
<artifactId>tree-structure</artifactId>
|
||||||
<version>3.1.5</version>
|
<version>4.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -180,7 +179,7 @@ separate, opt-in artifacts that depend on the core.
|
|||||||
be `@Serializable` directly — convert to/from the acyclic `TreeNodeDto` instead.
|
be `@Serializable` directly — convert to/from the acyclic `TreeNodeDto` instead.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
implementation("com.github.adriankuta:tree-structure-serialization:3.4.0")
|
implementation("com.github.adriankuta:tree-structure-serialization:4.0.0")
|
||||||
```
|
```
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
@@ -193,7 +192,7 @@ val restored = Json.decodeFromString<TreeNodeDto<String>>(json).toTreeNode()
|
|||||||
Traverse a tree as a cold `Flow` (handy in coroutine/`ViewModel` pipelines).
|
Traverse a tree as a cold `Flow` (handy in coroutine/`ViewModel` pipelines).
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
implementation("com.github.adriankuta:tree-structure-coroutines:3.4.0")
|
implementation("com.github.adriankuta:tree-structure-coroutines:4.0.0")
|
||||||
```
|
```
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
@@ -201,6 +200,24 @@ tree.preOrderFlow().collect { println(it.value) }
|
|||||||
tree.asFlow(TreeNodeIterators.LevelOrder).map { it.value }
|
tree.asFlow(TreeNodeIterators.LevelOrder).map { it.value }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Compose UI — `tree-structure-compose`
|
||||||
|
|
||||||
|
A `LazyTree` composable for Compose Multiplatform (JVM/desktop, iOS, Wasm). Only the visible nodes
|
||||||
|
are composed, and you decide how each node looks:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
implementation("com.github.adriankuta:tree-structure-compose:4.0.0")
|
||||||
|
```
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
LazyTree(root) { node, depth, expanded, toggle ->
|
||||||
|
Row(Modifier.padding(start = (depth * 16).dp).clickable(onClick = toggle)) {
|
||||||
|
if (!node.isLeaf) Text(if (expanded) "▾ " else "▸ ")
|
||||||
|
Text(node.value.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Publishing to Maven Central (central.sonatype.com)
|
## Publishing to Maven Central (central.sonatype.com)
|
||||||
|
|
||||||
This project is configured to publish artifacts to Maven Central via the Sonatype Central Portal.
|
This project is configured to publish artifacts to Maven Central via the Sonatype Central Portal.
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ binaryCompatibilityValidator = "0.16.3"
|
|||||||
kover = "0.8.3"
|
kover = "0.8.3"
|
||||||
coroutines = "1.9.0"
|
coroutines = "1.9.0"
|
||||||
kotlinxSerialization = "1.7.3"
|
kotlinxSerialization = "1.7.3"
|
||||||
|
composeMultiplatform = "1.7.3"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||||
@@ -14,6 +15,8 @@ dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
|
|||||||
mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
|
mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
|
||||||
binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" }
|
binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" }
|
||||||
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
|
kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" }
|
||||||
|
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
|
||||||
|
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ rootProject.name = "tree-structure"
|
|||||||
|
|
||||||
include(":tree-structure-serialization")
|
include(":tree-structure-serialization")
|
||||||
include(":tree-structure-coroutines")
|
include(":tree-structure-coroutines")
|
||||||
|
include(":tree-structure-compose")
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
@@ -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,60 @@ 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.")
|
||||||
|
}
|
||||||
|
if (child === this) {
|
||||||
|
throw TreeNodeException("Adding $child here would create a cycle.")
|
||||||
|
}
|
||||||
|
// Only a node that already has its own subtree can contain `this` and thus form a cycle.
|
||||||
|
// Skipping this walk for leaves keeps building deep trees O(n) instead of O(n²).
|
||||||
|
if (child._children.isNotEmpty()) {
|
||||||
|
var ancestor: TreeNode<T>? = _parent
|
||||||
|
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 +105,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 +137,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 +153,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 +165,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 +232,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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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) }
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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" +
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
4
tree-structure-compose/api/tree-structure-compose.api
Normal file
4
tree-structure-compose/api/tree-structure-compose.api
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
public final class com/github/adriankuta/datastructure/tree/compose/LazyTreeKt {
|
||||||
|
public static final fun LazyTree (Lcom/github/adriankuta/datastructure/tree/TreeNode;Landroidx/compose/ui/Modifier;ZLkotlin/jvm/functions/Function6;Landroidx/compose/runtime/Composer;II)V
|
||||||
|
}
|
||||||
|
|
||||||
74
tree-structure-compose/build.gradle.kts
Normal file
74
tree-structure-compose/build.gradle.kts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlinMultiplatform)
|
||||||
|
alias(libs.plugins.composeMultiplatform)
|
||||||
|
alias(libs.plugins.composeCompiler)
|
||||||
|
alias(libs.plugins.dokka)
|
||||||
|
alias(libs.plugins.mavenPublish)
|
||||||
|
signing
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "com.github.adriankuta"
|
||||||
|
version = rootProject.version
|
||||||
|
|
||||||
|
mavenPublishing {
|
||||||
|
publishToMavenCentral(automaticRelease = false)
|
||||||
|
signAllPublications()
|
||||||
|
|
||||||
|
coordinates("com.github.adriankuta", "tree-structure-compose", version.toString())
|
||||||
|
|
||||||
|
pom {
|
||||||
|
name.set("Tree Data Structure — Compose Multiplatform")
|
||||||
|
description.set("A LazyTree composable (expand/collapse, lazy rendering) for the tree-structure library.")
|
||||||
|
url.set("https://github.com/AdrianKuta/Tree-Data-Structure")
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name.set("MIT License")
|
||||||
|
url.set("https://opensource.org/licenses/MIT")
|
||||||
|
distribution.set("repo")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
id.set("AdrianKuta")
|
||||||
|
name.set("Adrian Kuta")
|
||||||
|
email.set("adrian.kuta93@gmail.com")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scm {
|
||||||
|
url.set("https://github.com/AdrianKuta/Tree-Data-Structure")
|
||||||
|
connection.set("scm:git:https://github.com/AdrianKuta/Tree-Data-Structure.git")
|
||||||
|
developerConnection.set("scm:git:ssh://git@github.com/AdrianKuta/Tree-Data-Structure.git")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
google()
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
explicitApi()
|
||||||
|
jvmToolchain(21)
|
||||||
|
|
||||||
|
jvm()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalWasmDsl::class)
|
||||||
|
wasmJs {
|
||||||
|
browser()
|
||||||
|
}
|
||||||
|
|
||||||
|
iosX64()
|
||||||
|
iosArm64()
|
||||||
|
iosSimulatorArm64()
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain.dependencies {
|
||||||
|
api(project(":"))
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.foundation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.github.adriankuta.datastructure.tree.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.github.adriankuta.datastructure.tree.TreeNode
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A lazily-rendered, expand/collapse tree for Compose Multiplatform. Only the currently-visible
|
||||||
|
* nodes are composed, so it scales to large trees. Expansion state is remembered internally, keyed
|
||||||
|
* by node identity.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* LazyTree(root) { node, depth, expanded, toggle ->
|
||||||
|
* Row(Modifier.padding(start = (depth * 16).dp).clickable(onClick = toggle)) {
|
||||||
|
* if (!node.isLeaf) Text(if (expanded) "▾" else "▸")
|
||||||
|
* Text(node.value.toString())
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param root the root of the tree to display.
|
||||||
|
* @param modifier the [Modifier] applied to the underlying [LazyColumn].
|
||||||
|
* @param initiallyExpanded whether nodes start expanded.
|
||||||
|
* @param nodeContent renders a single node. Receives the node, its depth (root = 0), whether it is
|
||||||
|
* expanded, and a `toggle` callback that flips this node's expansion state.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
public fun <T> LazyTree(
|
||||||
|
root: TreeNode<T>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
initiallyExpanded: Boolean = true,
|
||||||
|
nodeContent: @Composable (node: TreeNode<T>, depth: Int, expanded: Boolean, toggle: () -> Unit) -> Unit,
|
||||||
|
) {
|
||||||
|
val expansion = remember(root) { mutableStateMapOf<TreeNode<T>, Boolean>() }
|
||||||
|
val isExpanded: (TreeNode<T>) -> Boolean = { node -> expansion[node] ?: initiallyExpanded }
|
||||||
|
|
||||||
|
val visible = flattenVisible(root, isExpanded)
|
||||||
|
|
||||||
|
LazyColumn(modifier = modifier) {
|
||||||
|
items(visible.size) { index ->
|
||||||
|
val (node, depth) = visible[index]
|
||||||
|
nodeContent(node, depth, isExpanded(node)) {
|
||||||
|
expansion[node] = !isExpanded(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flattens the tree into the list of currently-visible `(node, depth)` pairs in pre-order, skipping
|
||||||
|
* the subtrees of collapsed nodes. Iterative, so it is safe on deep trees.
|
||||||
|
*/
|
||||||
|
private fun <T> flattenVisible(
|
||||||
|
root: TreeNode<T>,
|
||||||
|
isExpanded: (TreeNode<T>) -> Boolean,
|
||||||
|
): List<Pair<TreeNode<T>, Int>> {
|
||||||
|
val result = mutableListOf<Pair<TreeNode<T>, Int>>()
|
||||||
|
val stack = ArrayDeque<Pair<TreeNode<T>, Int>>()
|
||||||
|
stack.addLast(root to 0)
|
||||||
|
while (stack.isNotEmpty()) {
|
||||||
|
val (node, depth) = stack.removeLast()
|
||||||
|
result.add(node to depth)
|
||||||
|
if (isExpanded(node)) {
|
||||||
|
node.children.asReversed().forEach { child -> stack.addLast(child to depth + 1) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
explicitApi()
|
||||||
jvmToolchain(21)
|
jvmToolchain(21)
|
||||||
|
|
||||||
jvm()
|
jvm()
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
|
explicitApi()
|
||||||
jvmToolchain(21)
|
jvmToolchain(21)
|
||||||
|
|
||||||
jvm()
|
jvm()
|
||||||
|
|||||||
@@ -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. */
|
||||||
|
|||||||
Reference in New Issue
Block a user