mirror of
https://github.com/AdrianKuta/Tree-Data-Structure.git
synced 2026-06-20 19:30:13 +02:00
docs: rewrite README for clarity and usefulness
- One consistent example tree throughout; fixed prettyString output mismatch. - Task-oriented sections (building/traversal/navigation/functional/utilities/mutating). - Per-module usage for serialization, coroutines, and compose. - Condensed the maintainer publishing section; added a thread-safety note and CHANGELOG link.
This commit is contained in:
@@ -6,6 +6,11 @@ All notable changes to this project are documented here. The format is based on
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Rewrote the README for clarity: one consistent example tree, task-oriented sections
|
||||||
|
(building, traversal, navigation, functional, utilities, mutating), per-module usage, and a
|
||||||
|
condensed maintainer "Releasing" section.
|
||||||
|
|
||||||
## [4.0.0]
|
## [4.0.0]
|
||||||
|
|
||||||
A breaking release that cleans up the core API and enforces an explicit public surface.
|
A breaking release that cleans up the core API and enforces an explicit public surface.
|
||||||
|
|||||||
242
README.md
242
README.md
@@ -3,16 +3,23 @@
|
|||||||
[](https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/LICENSE)
|
[](https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/LICENSE)
|
||||||
[](https://github.com/AdrianKuta/Tree-Data-Structure/actions/workflows/publishRelease.yml)
|
[](https://github.com/AdrianKuta/Tree-Data-Structure/actions/workflows/publishRelease.yml)
|
||||||
|
|
||||||
Lightweight Kotlin Multiplatform tree data structure for Kotlin and Java. Includes a small DSL, multiple traversal iterators, and pretty-print support.
|
A lightweight n-ary tree for Kotlin Multiplatform. You get a generic `TreeNode<T>`, a small DSL for
|
||||||
|
building trees, three traversal orders, lazy `Sequence` traversal, and a set of navigation and
|
||||||
|
functional helpers. The core artifact has no third-party dependencies.
|
||||||
|
|
||||||
- Kotlin Multiplatform (JVM, JS, Wasm, iOS, and Native host)
|
It fits homogeneous trees of arbitrary depth: UI component hierarchies, file-system views, org
|
||||||
- Pre-order, Post-order, and Level-order iteration
|
charts, and category menus. For fixed, typed hierarchies (like a compiler AST) a sealed class is
|
||||||
- Lazy `Sequence` traversal that composes with the Kotlin stdlib (`map`/`filter`/`firstOrNull`…)
|
usually a better fit.
|
||||||
- Navigation helpers: `root()`, `ancestors()`, `siblings()`, `leaves()`, `descendants()`, `isLeaf`, `degree`
|
|
||||||
- Functional helpers: `findNode`, `filterNodes`, `anyNode`, `allNodes`, `foldNodes`, `mapValues`, `deepCopy`, `structurallyEquals`
|
## Features
|
||||||
- Stack-safe: traversal and `height()`/`nodeCount()`/`clear()` handle arbitrarily deep trees without `StackOverflowError`
|
|
||||||
- Simple DSL: tree { child(...) }
|
- Kotlin Multiplatform: JVM, JS, Wasm, iOS, and a native host target.
|
||||||
- Utilities: nodeCount(), height(), depth(), path(), prettyString(), clear(), removeChild()
|
- Build trees with a `tree { child(...) }` DSL or node by node with `addChild`.
|
||||||
|
- Pre-order, post-order, and level-order traversal, as iterators or lazy `Sequence`s.
|
||||||
|
- Navigation: `root()`, `ancestors()`, `siblings()`, `leaves()`, `descendants()`, `isLeaf`, `degree`.
|
||||||
|
- Functional helpers: `findNode`, `filterNodes`, `anyNode`, `allNodes`, `foldNodes`, `mapValues`, `deepCopy`, `structurallyEquals`.
|
||||||
|
- Utilities: `nodeCount()`, `height()`, `depth()`, `path()`, `prettyString()`.
|
||||||
|
- Stack-safe: traversal and `height()`/`nodeCount()`/`clear()` handle very deep trees without `StackOverflowError`.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -20,14 +27,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:4.0.0") // see badge above for the latest version
|
implementation("com.github.adriankuta:tree-structure:4.0.0") // latest version is on the badge above
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Gradle (Groovy):
|
Gradle (Groovy):
|
||||||
```groovy
|
```groovy
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "com.github.adriankuta:tree-structure:4.0.0" // see badge above for the latest
|
implementation "com.github.adriankuta:tree-structure:4.0.0"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -40,27 +47,9 @@ Maven:
|
|||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Building a tree
|
||||||
|
|
||||||
**Kotlin**
|
The DSL is the shortest way to build one:
|
||||||
```kotlin
|
|
||||||
val root = TreeNode("World")
|
|
||||||
val northA = TreeNode("North America")
|
|
||||||
val europe = TreeNode("Europe")
|
|
||||||
root.addChild(northA)
|
|
||||||
root.addChild(europe)
|
|
||||||
|
|
||||||
val usa = TreeNode("USA")
|
|
||||||
northA.addChild(usa)
|
|
||||||
|
|
||||||
val poland = TreeNode("Poland")
|
|
||||||
val france = TreeNode("France")
|
|
||||||
europe.addChild(poland)
|
|
||||||
europe.addChild(france)
|
|
||||||
println(root.prettyString())
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pretty Kotlin (DSL)**
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val root = tree("World") {
|
val root = tree("World") {
|
||||||
child("North America") { child("USA") }
|
child("North America") { child("USA") }
|
||||||
@@ -71,136 +60,123 @@ val root = tree("World") {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Java**
|
The same node-by-node API works from Kotlin and Java:
|
||||||
```java
|
```java
|
||||||
TreeNode<String> root = new TreeNode<>("World");
|
TreeNode<String> root = new TreeNode<>("World");
|
||||||
TreeNode<String> northA = new TreeNode<>("North America");
|
TreeNode<String> northAmerica = new TreeNode<>("North America");
|
||||||
|
root.addChild(northAmerica);
|
||||||
|
northAmerica.addChild(new TreeNode<>("USA"));
|
||||||
|
|
||||||
TreeNode<String> europe = new TreeNode<>("Europe");
|
TreeNode<String> europe = new TreeNode<>("Europe");
|
||||||
root.addChild(northA);
|
|
||||||
root.addChild(europe);
|
root.addChild(europe);
|
||||||
|
europe.addChild(new TreeNode<>("Poland"));
|
||||||
TreeNode<String> usa = new TreeNode<>("USA");
|
europe.addChild(new TreeNode<>("Germany"));
|
||||||
northA.addChild(usa);
|
|
||||||
|
|
||||||
TreeNode<String> poland = new TreeNode<>("Poland");
|
|
||||||
TreeNode<String> france = new TreeNode<>("France");
|
|
||||||
europe.addChild(poland);
|
|
||||||
europe.addChild(france);
|
|
||||||
System.out.println(root.prettyString());
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Output:
|
`prettyString()` renders the tree for logs and debugging:
|
||||||
```
|
```
|
||||||
World
|
World
|
||||||
├── North America
|
├── North America
|
||||||
│ └── USA
|
│ └── USA
|
||||||
└── Europe
|
└── Europe
|
||||||
├── Poland
|
├── Poland
|
||||||
└── France
|
└── Germany
|
||||||
```
|
```
|
||||||
|
|
||||||
### Traversal and utilities
|
## Traversal
|
||||||
```kotlin
|
|
||||||
val root = TreeNode("root")
|
|
||||||
// ... build your tree
|
|
||||||
|
|
||||||
// Choose iteration order per call (the default order is set in the constructor and is read-only)
|
Iterating a node visits the node and all of its descendants. The default order is set in the
|
||||||
|
constructor (pre-order by default) and is read-only. Pass an order per call when you need a
|
||||||
|
different one:
|
||||||
|
```kotlin
|
||||||
|
for (node in root) println(node.value) // default pre-order
|
||||||
for (node in root.asSequence(TreeNodeIterators.PostOrder)) println(node.value)
|
for (node in root.asSequence(TreeNodeIterators.PostOrder)) println(node.value)
|
||||||
|
|
||||||
// Utilities
|
|
||||||
root.nodeCount() // number of descendants
|
|
||||||
root.height() // longest path to a leaf (in edges)
|
|
||||||
root.depth() // distance from current node to the root
|
|
||||||
val path = root.path(root.children.first()) // nodes from descendant up to root
|
|
||||||
|
|
||||||
// Mutations — removeChild removes a *direct* child; detach() unhooks a node from wherever it lives
|
|
||||||
val child = root.children.first()
|
|
||||||
root.removeChild(child) // child is now detached from root
|
|
||||||
root.clear() // remove all descendants of root
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Lazy traversal with Sequence
|
Traversal is also exposed as a lazy `Sequence`, so it composes with the standard library and stops
|
||||||
|
early instead of materializing the whole tree:
|
||||||
Traversal is exposed as a lazy `Sequence`, so it composes with the Kotlin standard library and
|
|
||||||
short-circuits (it never materializes the whole tree just to find one node):
|
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val tree = tree("World") {
|
root.preOrderSequence().map { it.value }.toList() // [World, North America, USA, Europe, Poland, Germany]
|
||||||
child("North America") { child("USA") }
|
root.levelOrderSequence().first { it.value == "USA" } // stops as soon as it is found
|
||||||
child("Europe") {
|
root.asSequence(TreeNodeIterators.PostOrder).count() // 6
|
||||||
child("Poland")
|
|
||||||
child("Germany")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pick an order explicitly — no need to mutate the node's state.
|
|
||||||
tree.preOrderSequence().map { it.value }.toList() // [World, North America, USA, Europe, Poland, Germany]
|
|
||||||
tree.levelOrderSequence().first { it.value == "USA" } // stops as soon as it's found
|
|
||||||
tree.asSequence(TreeNodeIterators.PostOrder).count() // 6
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Navigation
|
## Navigation
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val usa = tree.findNode { it == "USA" }!!
|
val usa = root.findNode { it == "USA" }!!
|
||||||
|
|
||||||
usa.isLeaf // true
|
usa.isLeaf // true
|
||||||
usa.depth() // 2
|
usa.depth() // 2
|
||||||
usa.root().value // "World"
|
usa.root().value // "World"
|
||||||
usa.ancestors().map { it.value } // [North America, World]
|
usa.ancestors().map { it.value } // [North America, World]
|
||||||
tree.leaves().map { it.value } // [USA, Poland, Germany]
|
root.leaves().map { it.value } // [USA, Poland, Germany]
|
||||||
val europe = tree.findNode { it == "Europe" }!!
|
|
||||||
europe.children.first().siblings().map { it.value } // [Germany]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Functional operations
|
## Functional operations
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
tree.anyNode { it == "Poland" } // true
|
root.anyNode { it == "Poland" } // true
|
||||||
tree.filterNodes { it.length > 5 } // nodes whose value is longer than 5 chars
|
root.filterNodes { it.length > 5 } // nodes whose value is longer than 5 characters
|
||||||
tree.countNodes { it.startsWith("U") } // 1
|
root.countNodes { it.startsWith("U") } // 1
|
||||||
|
|
||||||
// Transform values into a brand-new tree (the original is untouched); stack-safe.
|
val lengths: TreeNode<Int> = root.mapValues { it.length } // a new tree; the original is untouched
|
||||||
val lengths: TreeNode<Int> = tree.mapValues { it.length }
|
val copy = root.deepCopy()
|
||||||
|
root.structurallyEquals(copy) // true: same values and shape, different nodes
|
||||||
|
```
|
||||||
|
|
||||||
// Deep copy + structural comparison.
|
## Utilities
|
||||||
val copy = tree.deepCopy()
|
```kotlin
|
||||||
tree.structurallyEquals(copy) // true (same values, same shape, different nodes)
|
root.nodeCount() // number of descendants, excluding the root
|
||||||
|
root.height() // edges on the longest path down to a leaf
|
||||||
|
root.depth() // edges from this node up to the root
|
||||||
|
root.path(usa) // [USA, North America, World], or null if usa is not a descendant
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mutating a tree
|
||||||
|
```kotlin
|
||||||
|
// addChild rejects a node that already has a parent or that would create a cycle.
|
||||||
|
root.addChild(TreeNode("Asia"))
|
||||||
|
|
||||||
|
// removeChild removes a direct child of the receiver and returns true if it was present.
|
||||||
|
root.removeChild(root.children.first())
|
||||||
|
|
||||||
|
// detach() unhooks a node from wherever it currently lives.
|
||||||
|
root.findNode { it == "Germany" }?.detach()
|
||||||
|
|
||||||
|
// clear() removes every descendant of the node.
|
||||||
|
root.clear()
|
||||||
```
|
```
|
||||||
|
|
||||||
## Optional modules
|
## Optional modules
|
||||||
|
|
||||||
The core `tree-structure` artifact has no third-party dependencies. Ecosystem integrations ship as
|
The core artifact has no third-party dependencies. Each integration is a separate, opt-in artifact
|
||||||
separate, opt-in artifacts that depend on the core.
|
that depends on the core.
|
||||||
|
|
||||||
### Serialization — `tree-structure-serialization`
|
### Serialization (`tree-structure-serialization`)
|
||||||
|
|
||||||
`kotlinx.serialization` support. A `TreeNode` holds a parent back-reference (a cycle), so it cannot
|
`kotlinx.serialization` support. A `TreeNode` keeps a reference back to its parent, so it cannot be
|
||||||
be `@Serializable` directly — convert to/from the acyclic `TreeNodeDto` instead.
|
`@Serializable` directly. Convert to and from the acyclic `TreeNodeDto` instead.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
implementation("com.github.adriankuta:tree-structure-serialization:4.0.0")
|
implementation("com.github.adriankuta:tree-structure-serialization:4.0.0")
|
||||||
```
|
```
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
val json = Json.encodeToString(tree.toDto())
|
val json = Json.encodeToString(root.toDto())
|
||||||
val restored = Json.decodeFromString<TreeNodeDto<String>>(json).toTreeNode()
|
val restored = Json.decodeFromString<TreeNodeDto<String>>(json).toTreeNode()
|
||||||
```
|
```
|
||||||
|
|
||||||
### Coroutines — `tree-structure-coroutines`
|
### Coroutines (`tree-structure-coroutines`)
|
||||||
|
|
||||||
Traverse a tree as a cold `Flow` (handy in coroutine/`ViewModel` pipelines).
|
Traverse a tree as a cold `Flow`, which is handy inside coroutine and `ViewModel` pipelines.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
implementation("com.github.adriankuta:tree-structure-coroutines:4.0.0")
|
implementation("com.github.adriankuta:tree-structure-coroutines:4.0.0")
|
||||||
```
|
```
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
tree.preOrderFlow().collect { println(it.value) }
|
root.preOrderFlow().collect { println(it.value) }
|
||||||
tree.asFlow(TreeNodeIterators.LevelOrder).map { it.value }
|
root.asFlow(TreeNodeIterators.LevelOrder).map { it.value }
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compose UI — `tree-structure-compose`
|
### Compose UI (`tree-structure-compose`)
|
||||||
|
|
||||||
A `LazyTree` composable for Compose Multiplatform (JVM/desktop, iOS, Wasm). Only the visible nodes
|
A `LazyTree` composable for Compose Multiplatform (JVM/desktop, iOS, Wasm). Only the visible nodes
|
||||||
are composed, and you decide how each node looks:
|
are composed, and you decide how each node looks:
|
||||||
@@ -208,7 +184,6 @@ are composed, and you decide how each node looks:
|
|||||||
```kotlin
|
```kotlin
|
||||||
implementation("com.github.adriankuta:tree-structure-compose:4.0.0")
|
implementation("com.github.adriankuta:tree-structure-compose:4.0.0")
|
||||||
```
|
```
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
LazyTree(root) { node, depth, expanded, toggle ->
|
LazyTree(root) { node, depth, expanded, toggle ->
|
||||||
Row(Modifier.padding(start = (depth * 16).dp).clickable(onClick = toggle)) {
|
Row(Modifier.padding(start = (depth * 16).dp).clickable(onClick = toggle)) {
|
||||||
@@ -218,37 +193,24 @@ LazyTree(root) { node, depth, expanded, toggle ->
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Publishing to Maven Central (central.sonatype.com)
|
## Notes
|
||||||
|
|
||||||
This project is configured to publish artifacts to Maven Central via the Sonatype Central Portal.
|
`TreeNode` is mutable and not thread-safe. Add your own synchronization if you share a tree across
|
||||||
|
threads, and do not modify a tree while you iterate it. Equality is by reference; use
|
||||||
|
`structurallyEquals` to compare two trees by value and shape.
|
||||||
|
|
||||||
There are two supported ways to publish:
|
Coming from 3.x? See [CHANGELOG.md](CHANGELOG.md) for the 4.0 migration notes.
|
||||||
|
|
||||||
1) Via GitHub Actions (recommended)
|
## Releasing (maintainers)
|
||||||
- Create a GitHub Release (tag) in this repository. When a release is published, the workflow .github/workflows/publishRelease.yml runs automatically.
|
|
||||||
- The workflow uses the Gradle task publishToMavenCentral to upload artifacts through the Central Portal.
|
|
||||||
- Make sure these repository secrets are configured in GitHub:
|
|
||||||
- MAVEN_CENTRAL_USERNAME — Your Sonatype Central username (not email).
|
|
||||||
- MAVEN_CENTRAL_PASSWORD — Your Sonatype Central password or a token from central.sonatype.com.
|
|
||||||
- SIGNING_KEY — ASCII‑armored GPG private key (exported, single line; for in‑memory signing).
|
|
||||||
- SIGNING_PASSWORD — Passphrase for the key above.
|
|
||||||
- The workflow uses JDK 21 and publishes the version defined in build.gradle.kts.
|
|
||||||
|
|
||||||
2) Locally via Gradle
|
Releases go to Maven Central through the Sonatype Central Portal using the
|
||||||
- Ensure you have a Sonatype Central account and that the groupId com.github.adriankuta is verified in central.sonatype.com (Namespace Rules → Verify).
|
`com.vanniktech.maven.publish` plugin. Creating a GitHub release runs
|
||||||
- Export the same credentials/signing values as environment variables or pass them as Gradle properties:
|
`.github/workflows/publishRelease.yml`, which signs and uploads every module; the deployment is then
|
||||||
- ORG_GRADLE_PROJECT_mavenCentralUsername
|
published from central.sonatype.com. The published version comes from `PUBLISH_VERSION` in
|
||||||
- ORG_GRADLE_PROJECT_mavenCentralPassword
|
`build.gradle.kts`. CI needs the `MAVEN_CENTRAL_USERNAME`, `MAVEN_CENTRAL_PASSWORD`, `SIGNING_KEY`,
|
||||||
- ORG_GRADLE_PROJECT_signingInMemoryKey
|
and `SIGNING_PASSWORD` repository secrets. To publish from a local machine, set the matching
|
||||||
- ORG_GRADLE_PROJECT_signingInMemoryKeyPassword
|
`ORG_GRADLE_PROJECT_*` properties and run `./gradlew publishToMavenCentral` (add `-Psnapshot=true`
|
||||||
- Then run:
|
for a snapshot build).
|
||||||
- ./gradlew publishToMavenCentral
|
|
||||||
- For snapshot publishing, set -Psnapshot=true (the version is derived from PUBLISH_VERSION with -SNAPSHOT).
|
|
||||||
|
|
||||||
Notes
|
|
||||||
- Publishing is powered by the com.vanniktech.maven.publish Gradle plugin and Sonatype Central Portal (no legacy Nexus staging URLs needed).
|
|
||||||
- The plugin is configured to sign all publications. Coordinates and POM metadata are defined in build.gradle.kts.
|
|
||||||
- If using the combined task is preferred, you can also run publishAndReleaseToMavenCentral when automatic release is enabled; this repository currently uploads with publishToMavenCentral from CI.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -273,5 +235,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
---
|
|
||||||
|
|||||||
Reference in New Issue
Block a user