12 Commits

Author SHA1 Message Date
Adrian Kuta
bec1fe02a7 feat: tree-structure-compose (LazyTree) + O(n) addChild cycle check
- New published module tree-structure-compose: a LazyTree composable for Compose
  Multiplatform (JVM/desktop, iOS, Wasm) with lazy rendering and expand/collapse.
- Fix an O(n^2) regression in addChild(): only walk ancestors for cycle detection
  when the child already has a subtree (a fresh leaf can never form a cycle), so
  building deep trees is O(n) again. Caught by the deep-chain stack-safety test on JS.
- README: Compose usage section; align all install snippets to 4.0.0.
- Version catalog: Compose Multiplatform + compose-compiler plugins.

Verified locally: JVM, JS(node), Wasm(node), iOS-simulator tests + apiCheck all green;
Compose module compiles for JVM, Wasm and iOS.
2026-06-07 18:55:07 +02:00
Adrian Kuta
69d19f89e3 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.
2026-06-07 18:47:40 +02:00
Adrian Kuta
c9bbea59b0 ci: binary-compat API baselines, multiplatform matrix CI, module docs
- Commit binary-compatibility-validator .api baselines for core + both modules.
- Replace JVM-only CI with a matrix: Ubuntu runs JVM/JS(node)/Wasm(node)/Native
  + apiCheck; macOS runs the iOS simulator tests.
- README: install + usage for tree-structure-serialization and -coroutines.
- Update kotlin-js-store/yarn.lock for the new modules' JS test deps.

Verified locally: JVM, JS(node), Wasm(node) and iOS-simulator tests pass for
core + both modules; native compiles & links (exec runs on the Linux CI runner).
2026-06-06 13:51:22 +02:00
Adrian Kuta
e1f01c4e2d feat: Kotlin 2.x/K2, version catalog, serialization & coroutines modules, docs
v3.4 modernization (continued):

- Migrate to Kotlin 2.x (K2); introduce gradle/libs.versions.toml version catalog;
  simplify the JS/Wasm/Node and iOS source-set wiring for the K2 hierarchy template.
- Apply binary-compatibility-validator and Kover plugins.
- New published module tree-structure-serialization: @Serializable TreeNodeDto with
  toDto()/toTreeNode() round-trip (kotlinx.serialization).
- New published module tree-structure-coroutines: asFlow()/pre/post/levelOrderFlow()
  (kotlinx.coroutines Flow traversal).
- Docs: README examples for Sequence/navigation/functional APIs, class-level KDoc
  (thread-safety/complexity), and a CHANGELOG.md.
- Ignore subproject build/ directories.
- Bump version to 3.4.0.

All JVM tests green (core + both modules).
2026-06-06 13:47:20 +02:00
Adrian Kuta
c45c5b7afa feat: stack-safe traversal + lazy Sequence + navigation/functional extensions
Core additive work for v3.4 (non-breaking):

- Rewrite nodeCount(), height(), clear() and the post-order iterator iteratively
  so deep/degenerate trees no longer throw StackOverflowError (verified to 50k deep).
- Add lazy Sequence traversal: asSequence(order), pre/post/levelOrderSequence().
- Add navigation extensions: isLeaf, degree, root(), ancestors(), siblings(),
  leaves(), descendants().
- Add functional extensions: findNode, filterNodes, anyNode, allNodes, countNodes,
  foldNodes, mapValues, deepCopy, structurallyEquals (all stack-safe).
- Add tests for stack-safety, the new APIs, and previously-uncovered
  height/depth/nodeCount/path (incl. exception paths). 40 tests green on JVM.
2026-06-06 13:35:39 +02:00
Adrian Kuta
6de95f7976 Release 3.1.5: remove debug println, drop worksheet leftover, modernize CI
- Remove stray println(child.value) from TreeNode.removeChild()
- Add regression test for removeChild() return value
- Delete leftover ExampleUnitTest.kt template test
- Remove Example.ws.kts worksheet and kotlin("script-runtime") from jvmMain
- Bump actions/checkout v2 -> v4 in CI workflows
- Update README install snippets to 3.1.5
- Bump version to 3.1.5
2026-06-06 13:03:00 +02:00
Adrian Kuta
ea0b4fa95c Bump version to 3.1.4 2025-12-25 22:53:43 +01:00
Adrian Kuta
9d52b152bd Merge pull request #31 from AdrianKuta/develop
Develop
2025-12-25 22:46:09 +01:00
Adrian Kuta
0fc7b16664 feat: Update Kotlin and JS dependencies and add Wasm target (#30)
- Upgraded Kotlin from `1.9.20` to `1.9.24`.
- Added a new Kotlin/Wasm target (`wasmJs`) for both browser and Node.js environments.
- Updated numerous JavaScript dependencies in `yarn.lock`, including major upgrades for `webpack`, `terser`, and various `@webpack-cli`, `@jridgewell`, and `@webassemblyjs` packages.
- Configured the Node.js version to `22.0.0` for JS and Wasm targets.
2025-12-25 22:43:29 +01:00
Adrian Kuta
d3086a5ced chore: Remove Dependabot configuration
Removes the `.github/dependabot.yml` file, disabling the Dependabot version update checks for the project.
2025-12-25 22:43:17 +01:00
Adrian Kuta
2071084964 Bump version to 3.1.3 2025-12-25 22:41:48 +01:00
Adrian Kuta
f51964831a feat: Ignore .idea directory
- Adds the entire `.idea/` directory to `.gitignore` to prevent IDE-specific files from being committed.
2025-12-25 22:29:06 +01:00
53 changed files with 2114 additions and 882 deletions

View File

@@ -1,11 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:

View File

@@ -6,18 +6,26 @@ on:
workflow_call:
jobs:
build:
name: Run unit tests
runs-on: ubuntu-latest
test:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: JVM / JS / Wasm / Native + API check
os: ubuntu-latest
tasks: jvmTest jsNodeTest wasmJsNodeTest nativeTest apiCheck
- name: iOS
os: macos-latest
tasks: iosSimulatorArm64Test
steps:
- name: Check out code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
# Builds the release artifacts of the library
- name: Test
run: ./gradlew cleanJvmTest jvmTest
run: ./gradlew ${{ matrix.tasks }} --console=plain

2
.gitignore vendored
View File

@@ -9,6 +9,7 @@
/.idea/assetWizardSettings.xml
.DS_Store
/build
build/
/captures
.externalNativeBuild
.cxx
@@ -16,3 +17,4 @@
/.idea/compiler.xml
/.idea/jarRepositories.xml
/.idea/deploymentTargetDropDown.xml
/.idea/

4
.idea/.gitignore generated vendored
View File

@@ -1,4 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
artifacts

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

View File

@@ -1,45 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="" withSubpackages="true" static="false" module="true" />
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

18
.idea/gradle.xml generated
View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -1,6 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ReplaceUntilWithRangeUntil" enabled="true" level="WEAK WARNING" enabled_by_default="true" />
</profile>
</component>

7
.idea/misc.xml generated
View File

@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

6
.idea/studiobot.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="StudioBotProjectSettings">
<option name="shareContext" value="OptedIn" />
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

79
CHANGELOG.md Normal file
View File

@@ -0,0 +1,79 @@
# Changelog
All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [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]
### Added
- Lazy `Sequence` traversal: `asSequence(order)`, `preOrderSequence()`, `postOrderSequence()`,
`levelOrderSequence()` — composes with the Kotlin stdlib and short-circuits.
- Navigation extensions: `isLeaf`, `degree`, `root()`, `ancestors()`, `siblings()`, `leaves()`,
`descendants()`.
- Functional extensions: `findNode`, `filterNodes`, `anyNode`, `allNodes`, `countNodes`,
`foldNodes`, `mapValues`, `deepCopy`, `structurallyEquals` (all stack-safe).
- New optional modules published as separate artifacts:
- `tree-structure-serialization``kotlinx.serialization` support via a `TreeNodeDto`.
- `tree-structure-coroutines``Flow` traversal (`asFlow`, `preOrderFlow`, …).
- `CHANGELOG.md`, expanded README examples, and class-level KDoc (thread-safety / complexity).
### Changed
- `nodeCount()`, `height()`, `clear()` and the post-order iterator are now iterative — deep or
degenerate (linear) trees no longer throw `StackOverflowError`.
- Migrated to Kotlin 2.x (K2 compiler) and introduced a Gradle version catalog.
- Build now uses `binary-compatibility-validator` (committed `.api` baselines) and Kover.
## [3.1.5]
### Fixed
- Removed a stray `println` in `TreeNode.removeChild()` that printed to stdout on every removal.
### Removed
- Deleted the `Example.ws.kts` worksheet and the `kotlin("script-runtime")` dependency from the
published JVM artifact, plus the leftover `ExampleUnitTest` template test.
### Changed
- Bumped `actions/checkout` v2 → v4 in CI workflows.
## [3.1.4]
- Updated Kotlin and JS dependencies; added the `wasmJs` target.
## [3.1.3]
- iOS targets and Maven Central (Sonatype Central Portal) publishing.
[Unreleased]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v4.0.0...HEAD
[4.0.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.4.0...v4.0.0
[3.4.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.1.5...v3.4.0
[3.1.5]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v3.1.3...v3.1.5
[3.1.4]: https://github.com/AdrianKuta/Tree-Data-Structure/releases/tag/v3.1.4
[3.1.3]: https://github.com/AdrianKuta/Tree-Data-Structure/releases/tag/v3.1.3

122
README.md
View File

@@ -5,8 +5,12 @@
Lightweight Kotlin Multiplatform tree data structure for Kotlin and Java. Includes a small DSL, multiple traversal iterators, and pretty-print support.
- Kotlin Multiplatform (JVM, JS, iOS, and Native host)
- Kotlin Multiplatform (JVM, JS, Wasm, iOS, and Native host)
- Pre-order, Post-order, and Level-order iteration
- Lazy `Sequence` traversal that composes with the Kotlin stdlib (`map`/`filter`/`firstOrNull`…)
- Navigation helpers: `root()`, `ancestors()`, `siblings()`, `leaves()`, `descendants()`, `isLeaf`, `degree`
- Functional helpers: `findNode`, `filterNodes`, `anyNode`, `allNodes`, `foldNodes`, `mapValues`, `deepCopy`, `structurallyEquals`
- Stack-safe: traversal and `height()`/`nodeCount()`/`clear()` handle arbitrarily deep trees without `StackOverflowError`
- Simple DSL: tree { child(...) }
- Utilities: nodeCount(), height(), depth(), path(), prettyString(), clear(), removeChild()
@@ -16,14 +20,14 @@ Gradle (Kotlin DSL):
```kotlin
// commonMain for KMP projects, or any sourceSet/module where you need it
dependencies {
implementation("com.github.adriankuta:tree-structure:3.1.1") // 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):
```groovy
dependencies {
implementation "com.github.adriankuta:tree-structure:3.1.1" // see badge above for the latest
implementation "com.github.adriankuta:tree-structure:4.0.0" // see badge above for the latest
}
```
@@ -32,7 +36,7 @@ Maven:
<dependency>
<groupId>com.github.adriankuta</groupId>
<artifactId>tree-structure</artifactId>
<version>3.1.1</version>
<version>4.0.0</version>
</dependency>
```
@@ -100,9 +104,8 @@ World
val root = TreeNode("root")
// ... build your tree
// Choose iteration order (default is PreOrder)
root.treeIterator = TreeNodeIterators.PostOrder
for (node in root) println(node.value)
// Choose iteration order per call (the default order is set in the constructor and is read-only)
for (node in root.asSequence(TreeNodeIterators.PostOrder)) println(node.value)
// Utilities
root.nodeCount() // number of descendants
@@ -110,10 +113,109 @@ root.height() // longest path to a leaf (in edges)
root.depth() // distance from current node to the root
val path = root.path(root.children.first()) // nodes from descendant up to root
// Mutations
// Mutations — removeChild removes a *direct* child; detach() unhooks a node from wherever it lives
val child = root.children.first()
root.removeChild(child)
root.clear() // remove entire subtree
root.removeChild(child) // child is now detached from root
root.clear() // remove all descendants of root
```
### Lazy traversal with Sequence
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
val tree = tree("World") {
child("North America") { child("USA") }
child("Europe") {
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
```kotlin
val usa = tree.findNode { it == "USA" }!!
usa.isLeaf // true
usa.depth() // 2
usa.root().value // "World"
usa.ancestors().map { it.value } // [North America, World]
tree.leaves().map { it.value } // [USA, Poland, Germany]
val europe = tree.findNode { it == "Europe" }!!
europe.children.first().siblings().map { it.value } // [Germany]
```
### Functional operations
```kotlin
tree.anyNode { it == "Poland" } // true
tree.filterNodes { it.length > 5 } // nodes whose value is longer than 5 chars
tree.countNodes { it.startsWith("U") } // 1
// Transform values into a brand-new tree (the original is untouched); stack-safe.
val lengths: TreeNode<Int> = tree.mapValues { it.length }
// Deep copy + structural comparison.
val copy = tree.deepCopy()
tree.structurallyEquals(copy) // true (same values, same shape, different nodes)
```
## Optional modules
The core `tree-structure` artifact has no third-party dependencies. Ecosystem integrations ship as
separate, opt-in artifacts that depend on the core.
### Serialization — `tree-structure-serialization`
`kotlinx.serialization` support. A `TreeNode` holds a parent back-reference (a cycle), so it cannot
be `@Serializable` directly — convert to/from the acyclic `TreeNodeDto` instead.
```kotlin
implementation("com.github.adriankuta:tree-structure-serialization:4.0.0")
```
```kotlin
val json = Json.encodeToString(tree.toDto())
val restored = Json.decodeFromString<TreeNodeDto<String>>(json).toTreeNode()
```
### Coroutines — `tree-structure-coroutines`
Traverse a tree as a cold `Flow` (handy in coroutine/`ViewModel` pipelines).
```kotlin
implementation("com.github.adriankuta:tree-structure-coroutines:4.0.0")
```
```kotlin
tree.preOrderFlow().collect { println(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)

101
api/tree-structure.api Normal file
View File

@@ -0,0 +1,101 @@
public abstract interface class com/github/adriankuta/datastructure/tree/ChildDeclarationInterface {
public abstract synthetic fun child (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
}
public final class com/github/adriankuta/datastructure/tree/ChildDeclarationInterface$DefaultImpls {
public static synthetic fun child$default (Lcom/github/adriankuta/datastructure/tree/ChildDeclarationInterface;Ljava/lang/Object;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
}
public class com/github/adriankuta/datastructure/tree/TreeNode : com/github/adriankuta/datastructure/tree/ChildDeclarationInterface, java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker {
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 synthetic fun child (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun clear ()V
public final fun depth ()I
public final fun detach ()Z
public final fun getChildren ()Ljava/util/List;
public final fun getParent ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun getTreeIterator ()Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public final fun getValue ()Ljava/lang/Object;
public final fun height ()I
public final fun isRoot ()Z
public fun iterator ()Ljava/util/Iterator;
public final fun iterator (Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)Ljava/util/Iterator;
public final fun nodeCount ()I
public final fun path (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public final fun prettyString ()Ljava/lang/String;
public final fun removeChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z
public fun toString ()Ljava/lang/String;
}
public final class com/github/adriankuta/datastructure/tree/TreeNodeFunctionalExtKt {
public static final fun allNodes (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Z
public static final fun anyNode (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Z
public static final fun countNodes (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)I
public static final fun deepCopy (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun filterNodes (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
public static final fun findNode (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun foldNodes (Lcom/github/adriankuta/datastructure/tree/TreeNode;Ljava/lang/Object;Lkotlin/jvm/functions/Function2;)Ljava/lang/Object;
public static final fun mapValues (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun structurallyEquals (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z
}
public final class com/github/adriankuta/datastructure/tree/TreeNodeNavigationExtKt {
public static final fun ancestors (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public static final fun descendants (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public static final fun getDegree (Lcom/github/adriankuta/datastructure/tree/TreeNode;)I
public static final fun isLeaf (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z
public static final fun leaves (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public static final fun root (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun siblings (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
}
public final class com/github/adriankuta/datastructure/tree/TreeNodeSequenceExtKt {
public static final fun asSequence (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)Lkotlin/sequences/Sequence;
public static synthetic fun asSequence$default (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;ILjava/lang/Object;)Lkotlin/sequences/Sequence;
public static final fun levelOrderSequence (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlin/sequences/Sequence;
public static final fun postOrderSequence (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlin/sequences/Sequence;
public static final fun preOrderSequence (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlin/sequences/Sequence;
}
public final class com/github/adriankuta/datastructure/tree/exceptions/TreeNodeException : java/lang/RuntimeException {
public fun <init> ()V
public fun <init> (Ljava/lang/String;)V
public fun <init> (Ljava/lang/String;Ljava/lang/Throwable;)V
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public final class com/github/adriankuta/datastructure/tree/iterators/LevelOrderTreeIterator : java/util/Iterator, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public fun hasNext ()Z
public fun next ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public synthetic fun next ()Ljava/lang/Object;
public fun remove ()V
}
public final class com/github/adriankuta/datastructure/tree/iterators/PostOrderTreeIterator : java/util/Iterator, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public fun hasNext ()Z
public fun next ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public synthetic fun next ()Ljava/lang/Object;
public fun remove ()V
}
public final class com/github/adriankuta/datastructure/tree/iterators/PreOrderTreeIterator : java/util/Iterator, kotlin/jvm/internal/markers/KMappedMarker {
public fun <init> (Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public fun hasNext ()Z
public fun next ()Lcom/github/adriankuta/datastructure/tree/TreeNode;
public synthetic fun next ()Ljava/lang/Object;
public fun remove ()V
}
public final class com/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators : java/lang/Enum {
public static final field LevelOrder Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public static final field PostOrder Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public static final field PreOrder Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
public static fun values ()[Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;
}

View File

@@ -1,15 +1,17 @@
import org.jetbrains.kotlin.konan.properties.Properties
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
kotlin("multiplatform") version "1.9.20"
id("org.jetbrains.dokka") version "1.9.20"
id("com.vanniktech.maven.publish") version "0.34.0"
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.dokka)
alias(libs.plugins.mavenPublish)
alias(libs.plugins.binaryCompatibilityValidator)
alias(libs.plugins.kover)
signing
}
val PUBLISH_GROUP_ID = "com.github.adriankuta"
val PUBLISH_ARTIFACT_ID = "tree-structure" // base artifact; KMP will add -jvm, -ios*, etc.
val PUBLISH_VERSION = "3.1.2"
val PUBLISH_VERSION = "4.0.0"
val snapshot: String? by project
@@ -25,7 +27,10 @@ mavenPublishing {
pom {
name.set("Tree Data Structure")
description.set("Simple implementation to store object in tree structure.")
description.set(
"Lightweight n-ary tree data structure for Kotlin Multiplatform (JVM, JS, Wasm, iOS, " +
"Native). DSL, pre/post/level-order traversal, lazy Sequence traversal, and pretty-print.",
)
url.set("https://github.com/AdrianKuta/Tree-Data-Structure")
licenses {
@@ -50,34 +55,28 @@ mavenPublishing {
}
}
// No legacy publishing {} block or s01 repos — Central Portal handles it.
repositories {
mavenCentral()
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(21))
}
}
kotlin {
jvmToolchain(21);
jvm {
compilations.all {
kotlinOptions {
jvmTarget = "21" // <- was "1.8"
}
}
}
explicitApi()
jvmToolchain(21)
jvm()
// JS targets (IR) for publishing
js(IR) {
browser()
nodejs()
}
// iOS targets
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
nodejs()
}
// Apple targets
iosX64()
iosArm64()
iosSimulatorArm64()
@@ -85,7 +84,7 @@ kotlin {
// Native host target
val hostOs = System.getProperty("os.name")
val isMingwX64 = hostOs.startsWith("Windows")
val nativeTarget = when {
when {
hostOs == "Mac OS X" -> macosX64("native")
hostOs == "Linux" -> linuxX64("native")
isMingwX64 -> mingwX64("native")
@@ -93,23 +92,8 @@ kotlin {
}
sourceSets {
val commonMain by getting
val commonTest by getting { dependencies { implementation(kotlin("test")) } }
val jvmMain by getting { dependencies { implementation(kotlin("script-runtime")) } }
val jvmTest by getting
val jsMain by getting
val jsTest by getting
val nativeMain by getting
val nativeTest by getting
// Shared iOS source sets
val iosMain by creating { dependsOn(commonMain) }
val iosTest by creating { dependsOn(commonTest) }
val iosX64Main by getting { dependsOn(iosMain) }
val iosArm64Main by getting { dependsOn(iosMain) }
val iosSimulatorArm64Main by getting { dependsOn(iosMain) }
val iosX64Test by getting { dependsOn(iosTest) }
val iosArm64Test by getting { dependsOn(iosTest) }
val iosSimulatorArm64Test by getting { dependsOn(iosTest) }
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}

View File

@@ -1,2 +1 @@
kotlin.code.style=official
kotlin.js.compiler=ir

24
gradle/libs.versions.toml Normal file
View File

@@ -0,0 +1,24 @@
[versions]
kotlin = "2.1.0"
dokka = "1.9.20"
mavenPublish = "0.34.0"
binaryCompatibilityValidator = "0.16.3"
kover = "0.8.3"
coroutines = "1.9.0"
kotlinxSerialization = "1.7.3"
composeMultiplatform = "1.7.3"
[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompatibilityValidator" }
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]
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
rootProject.name = "tree-structure"
include(":tree-structure-serialization")
include(":tree-structure-coroutines")
include(":tree-structure-compose")

View File

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

View File

@@ -9,16 +9,32 @@ import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators.*
import kotlin.jvm.JvmSynthetic
/**
* @param treeIterator Choose one of available iterators from [TreeNodeIterators]
* A node in a generic, mutable n-ary tree. Each node holds a [value], a reference to its [parent]
* and an ordered list of [children].
*
* Iterating a node (via [iterator], or the lazy [asSequence]/[preOrderSequence] extensions) visits
* the node and all of its descendants. Traversal and the [height]/[nodeCount]/[clear] helpers are
* implemented iteratively, so they are safe on arbitrarily deep trees.
*
* **Not thread-safe.** Nodes are mutable ([addChild]/[removeChild]/[clear] mutate the structure and
* parent pointers). Sharing a tree across threads requires external synchronization, and the tree
* must not be modified while it is being iterated.
*
* Equality is by reference (identity); use the `structurallyEquals` extension to compare two trees
* by value and shape.
*
* @param value the value stored in this node.
* @param treeIterator the default traversal order used by [iterator]. Prefer the
* `asSequence(order)` / `preOrderSequence()` extensions to choose an order without mutating state.
*/
open class TreeNode<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
/**
* The converse notion of a child, an immediate ancestor.
*/
val parent: TreeNode<T>?
public val parent: TreeNode<T>?
get() = _parent
private val _children = mutableListOf<TreeNode<T>>()
@@ -26,28 +42,60 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
/**
* A group of nodes with the same parent.
*/
val children: List<TreeNode<T>>
public val children: List<TreeNode<T>>
get() = _children
/**
* Checks whether the current tree node is the root of the tree
* @return `true` if the current tree node is root of the tree, `false` otherwise.
*/
val isRoot: Boolean
public val isRoot: Boolean
get() = _parent == null
/**
* Add new child to current node or root.
* Adds [child] as a direct child of this node.
*
* @param child A node which will be directly connected to current node.
* @param child a node that is not already attached to a tree. To move a node that already has a
* parent, call [detach] on it first.
* @throws TreeNodeException if [child] already has a parent, or if attaching it here would create
* a cycle (i.e. [child] is this node or one of its ancestors).
*/
fun addChild(child: TreeNode<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
_children.add(child)
}
/**
* Detaches this node from its parent, removing it from the parent's [children].
*
* @return `true` if this node was attached and is now detached; `false` if it was already a root.
*/
public fun detach(): Boolean {
val currentParent = _parent ?: return false
currentParent._children.remove(this)
_parent = null
return true
}
@JvmSynthetic
override fun child(value: T, childDeclaration: ChildDeclaration<T>?): TreeNode<T> {
public override fun child(value: T, childDeclaration: ChildDeclaration<T>?): TreeNode<T> {
val newChild = TreeNode(value)
newChild._parent = this
if (childDeclaration != null)
@@ -57,43 +105,55 @@ 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 {
println(child.value)
val removed = child._parent?._children?.remove(child)
child._parent = null
return removed ?: false
public fun removeChild(child: TreeNode<T>): Boolean {
val removed = _children.remove(child)
if (removed) child._parent = null
return removed
}
/**
* This function go through tree and counts children. Root element is not counted.
* @return All child and nested child count.
*/
fun nodeCount(): Int {
if (_children.isEmpty())
return 0
return _children.size +
_children.sumOf { it.nodeCount() }
public fun nodeCount(): Int {
var count = 0
val stack = ArrayDeque<TreeNode<T>>()
stack.addAll(_children)
while (stack.isNotEmpty()) {
val node = stack.removeLast()
count++
stack.addAll(node._children)
}
return count
}
/**
* @return The number of edges on the longest path between current node and a descendant leaf.
*/
fun height(): Int {
val childrenMaxDepth = _children.map { it.height() }
.maxOrNull()
?: -1 // -1 because this method counts nodes, and edges are always one less then nodes.
return childrenMaxDepth + 1
public fun height(): Int {
var maxDepth = 0
val stack = ArrayDeque<Pair<TreeNode<T>, Int>>()
stack.addLast(this to 0)
while (stack.isNotEmpty()) {
val (node, depthSoFar) = stack.removeLast()
if (depthSoFar > maxDepth) maxDepth = depthSoFar
node._children.forEach { stack.addLast(it to depthSoFar + 1) }
}
return maxDepth
}
/**
* Distance is the number of edges along the shortest path between two nodes.
* @return The distance between current node and the root.
*/
fun depth(): Int {
public fun depth(): Int {
var depth = 0
var tempParent = parent
@@ -105,44 +165,52 @@ open class TreeNode<T>(val value: T, var treeIterator: TreeNodeIterators = PreOr
}
/**
* Returns the collection of nodes, which connect the current node
* with its descendants
* Returns the chain of nodes from [descendant] up to and including this node, or `null` if
* [descendant] is not a strict descendant of this node.
*
* @param descendant the bottom child node for which the path is calculated
* @return collection of nodes, which connect the current node with its descendants
* @throws TreeNodeException exception that may be thrown in case if the
* current node does not have such descendant or if the
* specified tree node is root
* @param descendant the node to walk up from.
* @return the path `[descendant, …, this]`, or `null` if [descendant] is the root or is not
* located in this node's subtree.
*/
@Throws(TreeNodeException::class)
fun path(descendant: TreeNode<T>): List<TreeNode<T>> {
public fun path(descendant: TreeNode<T>): List<TreeNode<T>>? {
if (descendant.isRoot) return null
val path = mutableListOf<TreeNode<T>>()
var node = descendant
path.add(node)
while (!node.isRoot) {
node = node.parent!!
path.add(node)
if (node == this)
return path
if (node == this) return path
}
throw TreeNodeException("The specified tree node $descendant is not the descendant of tree node $this")
return null
}
/**
* Remove all children from root and every node in tree.
* Removes every descendant of this node. Afterwards [children] is empty and all former
* descendants are detached (their parent is `null`). This node itself stays attached to its own
* parent.
*/
fun clear() {
_parent = null
_children.forEach { it.clear() }
public fun clear() {
val descendants = ArrayDeque<TreeNode<T>>()
val stack = ArrayDeque<TreeNode<T>>()
stack.addAll(_children)
while (stack.isNotEmpty()) {
val node = stack.removeLast()
descendants.addLast(node)
stack.addAll(node._children)
}
descendants.forEach { node ->
node._parent = null
node._children.clear()
}
_children.clear()
}
override fun toString(): String {
public override fun toString(): String {
return value.toString()
}
fun prettyString(): String {
public fun prettyString(): String {
val stringBuilder = StringBuilder()
print(stringBuilder, "", "")
return stringBuilder.toString()
@@ -164,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)
PostOrder -> PostOrderTreeIterator(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 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.
@@ -14,7 +14,7 @@ typealias ChildDeclaration<T> = ChildDeclarationInterface<T>.() -> Unit
* @see [ChildDeclarationInterface.child]
*/
@JvmSynthetic
inline fun <reified T> tree(
public inline fun <reified T> tree(
root: T,
defaultIterator: TreeNodeIterators = TreeNodeIterators.PreOrder,
childDeclaration: ChildDeclaration<T>

View File

@@ -0,0 +1,65 @@
package com.github.adriankuta.datastructure.tree
/** Returns the first node (pre-order) whose value matches [predicate], or `null`. Short-circuits. */
public fun <T> TreeNode<T>.findNode(predicate: (T) -> Boolean): TreeNode<T>? =
preOrderSequence().firstOrNull { predicate(it.value) }
/** All nodes (pre-order) whose value matches [predicate]. */
public fun <T> TreeNode<T>.filterNodes(predicate: (T) -> Boolean): List<TreeNode<T>> =
preOrderSequence().filter { predicate(it.value) }.toList()
/** `true` if any node's value matches [predicate]. Short-circuits. */
public fun <T> TreeNode<T>.anyNode(predicate: (T) -> Boolean): Boolean =
preOrderSequence().any { predicate(it.value) }
/** `true` if every node's value matches [predicate]. Short-circuits. */
public fun <T> TreeNode<T>.allNodes(predicate: (T) -> Boolean): Boolean =
preOrderSequence().all { predicate(it.value) }
/** Counts nodes whose value matches [predicate]. */
public fun <T> TreeNode<T>.countNodes(predicate: (T) -> Boolean): Int =
preOrderSequence().count { predicate(it.value) }
/** Folds [operation] over all nodes in pre-order, starting from [initial]. */
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) }
/**
* 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.
*/
public fun <T, R> TreeNode<T>.mapValues(transform: (T) -> R): TreeNode<R> {
val newRoot = TreeNode(transform(value), treeIterator)
val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<R>>>()
stack.addLast(this to newRoot)
while (stack.isNotEmpty()) {
val (source, target) = stack.removeLast()
source.children.forEach { child ->
val mappedChild = TreeNode(transform(child.value), child.treeIterator)
target.addChild(mappedChild)
stack.addLast(child to mappedChild)
}
}
return newRoot
}
/** Returns an independent deep copy of this subtree (same values, same shape, new nodes). */
public fun <T> TreeNode<T>.deepCopy(): TreeNode<T> = mapValues { it }
/**
* Structural equality: `true` when [other] holds the same values in the same shape. Unlike
* [TreeNode]'s reference equality, this compares the entire subtree. Stack-safe.
*/
public fun <T> TreeNode<T>.structurallyEquals(other: TreeNode<T>): Boolean {
val stack = ArrayDeque<Pair<TreeNode<T>, TreeNode<T>>>()
stack.addLast(this to other)
while (stack.isNotEmpty()) {
val (a, b) = stack.removeLast()
if (a.value != b.value) return false
if (a.children.size != b.children.size) return false
for (i in a.children.indices) {
stack.addLast(a.children[i] to b.children[i])
}
}
return true
}

View File

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

View File

@@ -0,0 +1,31 @@
package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.iterators.LevelOrderTreeIterator
import com.github.adriankuta.datastructure.tree.iterators.PostOrderTreeIterator
import com.github.adriankuta.datastructure.tree.iterators.PreOrderTreeIterator
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
/**
* Lazily traverses this subtree in the given [order] as a [Sequence], without forcing the whole
* traversal up front. Pairs with the Kotlin stdlib, e.g.
* `root.asSequence().map { it.value }.firstOrNull { it == target }`.
*/
public fun <T> TreeNode<T>.asSequence(
order: TreeNodeIterators = TreeNodeIterators.PreOrder,
): Sequence<TreeNode<T>> {
val self = this
return when (order) {
TreeNodeIterators.PreOrder -> Sequence { PreOrderTreeIterator(self) }
TreeNodeIterators.PostOrder -> Sequence { PostOrderTreeIterator(self) }
TreeNodeIterators.LevelOrder -> Sequence { LevelOrderTreeIterator(self) }
}
}
/** Lazy pre-order traversal as a [Sequence]. */
public fun <T> TreeNode<T>.preOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PreOrder)
/** Lazy post-order traversal as a [Sequence]. */
public fun <T> TreeNode<T>.postOrderSequence(): Sequence<TreeNode<T>> = asSequence(TreeNodeIterators.PostOrder)
/** Lazy level-order (breadth-first) traversal as a [Sequence]. */
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
class TreeNodeException @JvmOverloads constructor(message: String? = null, cause: Throwable? = null) :
public class TreeNodeException @JvmOverloads constructor(message: String? = null, cause: Throwable? = null) :
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
* ```
*/
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>>()
@@ -28,9 +28,9 @@ class LevelOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
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()
node.children
.forEach { stack.addLast(it) }

View File

@@ -20,29 +20,24 @@ import com.github.adriankuta.datastructure.tree.TreeNode
* 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 stack = ArrayDeque<TreeNode<T>>()
private val result = ArrayDeque<TreeNode<T>>()
init {
stack.addAll(getChildrenStack(root))
}
override fun hasNext(): Boolean = stack.isNotEmpty()
override fun next(): TreeNode<T> {
return stack.removeFirst()
}
private fun getChildrenStack(node: TreeNode<T>): ArrayDeque<TreeNode<T>> {
// Iterative post-order: pop a node, prepend it to `result`, then push its children
// left-to-right. Reading `result` front-to-back yields post-order — without the deep
// recursion that overflowed the stack on degenerate (linear) trees.
val stack = ArrayDeque<TreeNode<T>>()
if(node.children.isEmpty()) {
return ArrayDeque(listOf(node))
stack.addLast(root)
while (stack.isNotEmpty()) {
val node = stack.removeLast()
result.addFirst(node)
node.children.forEach { stack.addLast(it) }
}
node.children.forEach {
stack.addAll(getChildrenStack(it))
}
stack.addLast(node)
return stack
}
public override fun hasNext(): Boolean = result.isNotEmpty()
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
* ```
*/
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>>()
@@ -28,9 +28,9 @@ class PreOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
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()
node.children
.asReversed()

View File

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

View File

@@ -1,16 +0,0 @@
package com.github.adriankuta.datastructure.tree
import kotlin.test.Test
import kotlin.test.assertEquals
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

View File

@@ -0,0 +1,78 @@
package com.github.adriankuta.datastructure.tree
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotSame
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
class TreeNodeFunctionalTest {
private fun sample() = tree(1) {
child(2) {
child(4)
child(5)
}
child(3) {
child(6)
}
}
@Test
fun findNode() {
assertEquals(6, sample().findNode { it == 6 }?.value)
assertNull(sample().findNode { it == 99 })
}
@Test
fun filterNodes() =
assertContentEquals(listOf(2, 4, 6), sample().filterNodes { it % 2 == 0 }.map { it.value })
@Test
fun anyNode() {
assertTrue(sample().anyNode { it == 6 })
assertFalse(sample().anyNode { it == 99 })
}
@Test
fun allNodes() {
assertTrue(sample().allNodes { it > 0 })
assertFalse(sample().allNodes { it < 5 })
}
@Test
fun countNodes() = assertEquals(3, sample().countNodes { it > 3 })
@Test
fun foldNodes() = assertEquals(21, sample().foldNodes(0) { acc, node -> acc + node.value })
@Test
fun mapPreservesStructureAndTransformsValues() {
val mapped = sample().mapValues { it * 10 }
assertContentEquals(
listOf(10, 20, 40, 50, 30, 60),
mapped.preOrderSequence().map { it.value }.toList(),
)
}
@Test
fun deepCopyIsStructurallyEqualButDistinct() {
val original = sample()
val copy = original.deepCopy()
assertNotSame(original, copy)
assertTrue(original.structurallyEquals(copy))
}
@Test
fun structurallyEqualsDistinguishesByValueAndShape() {
assertTrue(sample().structurallyEquals(sample()))
val different = tree(1) {
child(2) { child(4) }
child(3) { child(6) }
}
assertFalse(sample().structurallyEquals(different))
}
}

View File

@@ -0,0 +1,66 @@
package com.github.adriankuta.datastructure.tree
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertSame
import kotlin.test.assertTrue
class TreeNodeNavigationTest {
private val root = TreeNode(1)
private val n2 = TreeNode(2)
private val n3 = TreeNode(3)
private val n4 = TreeNode(4)
private val n5 = TreeNode(5)
private val n6 = TreeNode(6)
init {
root.addChild(n2)
root.addChild(n3)
n2.addChild(n4)
n2.addChild(n5)
n3.addChild(n6)
}
@Test
fun isLeaf() {
assertTrue(n4.isLeaf)
assertFalse(root.isLeaf)
}
@Test
fun degree() {
assertEquals(2, root.degree)
assertEquals(0, n4.degree)
}
@Test
fun root() {
assertSame(root, n6.root())
assertSame(root, root.root())
}
@Test
fun ancestors() {
assertContentEquals(listOf(n2, root), n4.ancestors())
assertContentEquals(emptyList(), root.ancestors())
}
@Test
fun siblings() {
assertContentEquals(listOf(n5), n4.siblings())
assertContentEquals(emptyList(), root.siblings())
}
@Test
fun leaves() {
assertContentEquals(listOf(n4, n5, n6), root.leaves())
}
@Test
fun descendants() {
assertContentEquals(listOf(n2, n4, n5, n3, n6), root.descendants())
}
}

View File

@@ -0,0 +1,46 @@
package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
class TreeNodeSequenceTest {
private fun sample() = tree(1) {
child(2) {
child(4)
child(5)
}
child(3) {
child(6)
}
}
@Test
fun preOrderSequence() =
assertContentEquals(listOf(1, 2, 4, 5, 3, 6), sample().preOrderSequence().map { it.value }.toList())
@Test
fun postOrderSequence() =
assertContentEquals(listOf(4, 5, 2, 6, 3, 1), sample().postOrderSequence().map { it.value }.toList())
@Test
fun levelOrderSequence() =
assertContentEquals(listOf(1, 2, 3, 4, 5, 6), sample().levelOrderSequence().map { it.value }.toList())
@Test
fun asSequenceDefaultsToPreOrder() =
assertContentEquals(listOf(1, 2, 4, 5, 3, 6), sample().asSequence().map { it.value }.toList())
@Test
fun asSequenceHonorsExplicitOrder() =
assertContentEquals(
listOf(1, 2, 3, 4, 5, 6),
sample().asSequence(TreeNodeIterators.LevelOrder).map { it.value }.toList(),
)
@Test
fun sequenceShortCircuitsLazily() =
assertEquals(4, sample().preOrderSequence().map { it.value }.first { it == 4 })
}

View File

@@ -0,0 +1,47 @@
package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import kotlin.test.Test
import kotlin.test.assertEquals
/**
* A deep, degenerate (linear) tree must not overflow the call stack. These tests build a chain
* thousands of nodes deep — recursive implementations of [TreeNode.height], [TreeNode.nodeCount]
* and the post-order iterator blow the stack here, so they pin the iterative rewrites.
*/
class TreeNodeStackSafetyTest {
private val depth = 50_000
private fun deepChain(): TreeNode<Int> {
val root = TreeNode(0)
var current = root
for (i in 1..depth) {
val child = TreeNode(i)
current.addChild(child)
current = child
}
return root
}
@Test
fun heightDoesNotOverflowOnDeepTree() {
assertEquals(depth, deepChain().height())
}
@Test
fun nodeCountDoesNotOverflowOnDeepTree() {
// nodeCount() excludes the root, so a chain of `depth` extra nodes counts as `depth`.
assertEquals(depth, deepChain().nodeCount())
}
@Test
fun postOrderIterationDoesNotOverflowOnDeepTree() {
assertEquals(depth + 1, deepChain().asSequence(TreeNodeIterators.PostOrder).count())
}
@Test
fun preOrderIterationDoesNotOverflowOnDeepTree() {
assertEquals(depth + 1, deepChain().asSequence(TreeNodeIterators.PreOrder).count())
}
}

View File

@@ -4,7 +4,9 @@ import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
class TreeNodeTest {
@@ -49,7 +51,7 @@ class TreeNodeTest {
)
root.removeChild(curdNode)
root.removeChild(gingerTeaNode)
gingerTeaNode.detach()
assertEquals(
"Root\n" +
"└── Beverages\n" +
@@ -62,6 +64,18 @@ class TreeNodeTest {
)
}
@Test
fun removeChildReturnsTrueWhenPresentFalseOtherwise() {
val root = TreeNode("Root")
val child = TreeNode("Child")
root.addChild(child)
assertTrue(root.removeChild(child), "Removing a present child returns true")
assertFalse(root.removeChild(child), "Removing an already-removed child returns false")
assertNull(child.parent, "Removed child is detached from its parent")
assertEquals(emptyList(), root.children)
}
@Test
fun clearTest() {
val root = TreeNode("Root")

View File

@@ -0,0 +1,53 @@
package com.github.adriankuta.datastructure.tree
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertNull
class TreeNodeUtilitiesTest {
private val root = TreeNode(1)
private val a = TreeNode(2)
private val b = TreeNode(3)
init {
root.addChild(a)
a.addChild(b)
}
@Test
fun nodeCountCountsDescendantsExcludingRoot() {
assertEquals(0, TreeNode("solo").nodeCount())
assertEquals(2, root.nodeCount())
}
@Test
fun heightIsLongestEdgePathToLeaf() {
assertEquals(0, TreeNode("solo").height())
assertEquals(2, root.height())
assertEquals(1, a.height())
}
@Test
fun depthIsDistanceToRoot() {
assertEquals(0, root.depth())
assertEquals(1, a.depth())
assertEquals(2, b.depth())
}
@Test
fun pathReturnsDescendantToReceiverChain() {
assertContentEquals(listOf(b, a, root), root.path(b))
}
@Test
fun pathReturnsNullWhenNotADescendant() {
assertNull(root.path(TreeNode(99)))
}
@Test
fun pathReturnsNullWhenDescendantIsRootItself() {
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

@@ -1,14 +0,0 @@
import com.github.adriankuta.datastructure.tree.tree
val root =
tree("World") {
child("North America") {
child("USA")
}
child("Europe") {
child("Poland")
child("Germany")
}
}
print(root.prettyString())

View 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
}

View 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)
}
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,8 @@
public final class com/github/adriankuta/datastructure/tree/coroutines/TreeNodeFlowExtKt {
public static final fun asFlow (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)Lkotlinx/coroutines/flow/Flow;
public static synthetic fun asFlow$default (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow;
public static final fun levelOrderFlow (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlinx/coroutines/flow/Flow;
public static final fun postOrderFlow (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlinx/coroutines/flow/Flow;
public static final fun preOrderFlow (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lkotlinx/coroutines/flow/Flow;
}

View File

@@ -0,0 +1,89 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
alias(libs.plugins.kotlinMultiplatform)
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-coroutines", version.toString())
pom {
name.set("Tree Data Structure — coroutines")
description.set("kotlinx.coroutines Flow traversal 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()
}
kotlin {
explicitApi()
jvmToolchain(21)
jvm()
js(IR) {
browser()
nodejs()
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
nodejs()
}
iosX64()
iosArm64()
iosSimulatorArm64()
val hostOs = System.getProperty("os.name")
val isMingwX64 = hostOs.startsWith("Windows")
when {
hostOs == "Mac OS X" -> macosX64("native")
hostOs == "Linux" -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
}
sourceSets {
commonMain.dependencies {
api(project(":"))
api(libs.kotlinx.coroutines.core)
}
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test)
}
}
}

View File

@@ -0,0 +1,24 @@
package com.github.adriankuta.datastructure.tree.coroutines
import com.github.adriankuta.datastructure.tree.TreeNode
import com.github.adriankuta.datastructure.tree.asSequence
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.asFlow
/**
* Emits this node and all of its descendants as a cold [Flow], traversed in the given [order].
* Useful for plugging tree traversal into coroutine/Flow pipelines (e.g. in a ViewModel).
*/
public fun <T> TreeNode<T>.asFlow(
order: TreeNodeIterators = TreeNodeIterators.PreOrder,
): Flow<TreeNode<T>> = asSequence(order).asFlow()
/** Pre-order traversal as a cold [Flow]. */
public fun <T> TreeNode<T>.preOrderFlow(): Flow<TreeNode<T>> = asFlow(TreeNodeIterators.PreOrder)
/** Post-order traversal as a cold [Flow]. */
public fun <T> TreeNode<T>.postOrderFlow(): Flow<TreeNode<T>> = asFlow(TreeNodeIterators.PostOrder)
/** Level-order (breadth-first) traversal as a cold [Flow]. */
public fun <T> TreeNode<T>.levelOrderFlow(): Flow<TreeNode<T>> = asFlow(TreeNodeIterators.LevelOrder)

View File

@@ -0,0 +1,35 @@
package com.github.adriankuta.datastructure.tree.coroutines
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import com.github.adriankuta.datastructure.tree.tree
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.test.assertEquals
class TreeNodeFlowTest {
private fun sample() = tree(1) {
child(2) {
child(4)
child(5)
}
child(3) {
child(6)
}
}
@Test
fun preOrderFlowEmitsInPreOrder() = runTest {
assertEquals(listOf(1, 2, 4, 5, 3, 6), sample().preOrderFlow().map { it.value }.toList())
}
@Test
fun levelOrderFlowEmitsInLevelOrder() = runTest {
assertEquals(
listOf(1, 2, 3, 4, 5, 6),
sample().asFlow(TreeNodeIterators.LevelOrder).map { it.value }.toList(),
)
}
}

View File

@@ -0,0 +1,35 @@
public final class com/github/adriankuta/datastructure/tree/serialization/TreeNodeDto {
public static final field Companion Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto$Companion;
public fun <init> (Ljava/lang/Object;Ljava/util/List;)V
public synthetic fun <init> (Ljava/lang/Object;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun component1 ()Ljava/lang/Object;
public final fun component2 ()Ljava/util/List;
public final fun copy (Ljava/lang/Object;Ljava/util/List;)Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;
public static synthetic fun copy$default (Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;Ljava/lang/Object;Ljava/util/List;ILjava/lang/Object;)Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;
public fun equals (Ljava/lang/Object;)Z
public final fun getChildren ()Ljava/util/List;
public final fun getValue ()Ljava/lang/Object;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public synthetic class com/github/adriankuta/datastructure/tree/serialization/TreeNodeDto$$serializer : kotlinx/serialization/internal/GeneratedSerializer {
public fun <init> (Lkotlinx/serialization/KSerializer;)V
public final fun childSerializers ()[Lkotlinx/serialization/KSerializer;
public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;
public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object;
public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor;
public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;)V
public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V
public final fun typeParametersSerializers ()[Lkotlinx/serialization/KSerializer;
}
public final class com/github/adriankuta/datastructure/tree/serialization/TreeNodeDto$Companion {
public final fun serializer (Lkotlinx/serialization/KSerializer;)Lkotlinx/serialization/KSerializer;
}
public final class com/github/adriankuta/datastructure/tree/serialization/TreeNodeDtoKt {
public static final fun toDto (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;
public static final fun toTreeNode (Lcom/github/adriankuta/datastructure/tree/serialization/TreeNodeDto;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
}

View File

@@ -0,0 +1,89 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.kotlinSerialization)
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-serialization", version.toString())
pom {
name.set("Tree Data Structure — kotlinx.serialization")
description.set("kotlinx.serialization support (TreeNodeDto round-trip) 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()
}
kotlin {
explicitApi()
jvmToolchain(21)
jvm()
js(IR) {
browser()
nodejs()
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
nodejs()
}
iosX64()
iosArm64()
iosSimulatorArm64()
val hostOs = System.getProperty("os.name")
val isMingwX64 = hostOs.startsWith("Windows")
when {
hostOs == "Mac OS X" -> macosX64("native")
hostOs == "Linux" -> linuxX64("native")
isMingwX64 -> mingwX64("native")
else -> throw GradleException("Host OS is not supported in Kotlin/Native.")
}
sourceSets {
commonMain.dependencies {
api(project(":"))
api(libs.kotlinx.serialization.json)
}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}

View File

@@ -0,0 +1,30 @@
package com.github.adriankuta.datastructure.tree.serialization
import com.github.adriankuta.datastructure.tree.TreeNode
import kotlinx.serialization.Serializable
/**
* A serializable, acyclic view of a [TreeNode] subtree. [TreeNode] itself holds a back-reference to
* its parent (a cycle), so it cannot be `@Serializable` directly — convert to/from this DTO instead.
*
* ```
* val json = Json.encodeToString(tree.toDto())
* val restored = Json.decodeFromString<TreeNodeDto<String>>(json).toTreeNode()
* ```
*/
@Serializable
public data class TreeNodeDto<T>(
public val value: T,
public val children: List<TreeNodeDto<T>> = emptyList(),
)
/** Converts this subtree into a serializable [TreeNodeDto], preserving values and shape. */
public fun <T> TreeNode<T>.toDto(): TreeNodeDto<T> =
TreeNodeDto(value, children.map { it.toDto() })
/** Rebuilds a mutable [TreeNode] tree from this DTO. */
public fun <T> TreeNodeDto<T>.toTreeNode(): TreeNode<T> {
val node = TreeNode(value)
children.forEach { node.addChild(it.toTreeNode()) }
return node
}

View File

@@ -0,0 +1,40 @@
package com.github.adriankuta.datastructure.tree.serialization
import com.github.adriankuta.datastructure.tree.structurallyEquals
import com.github.adriankuta.datastructure.tree.tree
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class TreeNodeSerializationTest {
@Test
fun roundTripsThroughJson() {
val original = tree("World") {
child("North America") { child("USA") }
child("Europe") {
child("Poland")
child("Germany")
}
}
val json = Json.encodeToString(original.toDto())
val restored = Json.decodeFromString<TreeNodeDto<String>>(json).toTreeNode()
assertTrue(original.structurallyEquals(restored))
}
@Test
fun dtoMirrorsTreeShape() {
val dto = tree(1) {
child(2)
child(3) { child(4) }
}.toDto()
assertEquals(1, dto.value)
assertEquals(2, dto.children.size)
assertEquals(4, dto.children[1].children[0].value)
}
}