67 Commits

Author SHA1 Message Date
Adrian Kuta
0fe6535258 Release 4.2.0
Some checks failed
Test / JVM / JS / Wasm / Native / Android + API check (push) Has been cancelled
Test / iOS (push) Has been cancelled
- PUBLISH_VERSION 4.1.1 -> 4.2.0
- CHANGELOG: promote [Unreleased] -> [4.2.0] (2026-06-08) + compare link
- README: bump install snippets to 4.2.0

Adds the Android target for tree-structure and tree-structure-compose,
the default TreeNodeRow composable + LazyTree(label=...) overload, and a
runnable Android sample. All additive, no breaking changes.
2026-06-08 21:58:23 +02:00
Adrian Kuta
28c1690f96 fix: raise Gradle daemon heap/Metaspace for the Android (AGP) build
The Android target added in #48 brought AGP onto the build's classpath
without setting org.gradle.jvmargs, so the Gradle daemon ran on its
defaults (512m heap / 384m Metaspace). AGP loads enough classes that,
combined with the KMP matrix, binary-compatibility-validator, Kover and
Dokka in a single build, the daemon exhausts Metaspace and is killed
mid-build (GC thrashing / out of Metaspace). The
'JVM / JS / Wasm / Native / Android + API check' job therefore failed
deterministically on master, and the publish job builds the same targets.

Set org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g. Verified with a
cold full-matrix build locally.
2026-06-08 21:58:23 +02:00
Adrian Kuta
dc59476b10 feat: add Android target and default TreeNodeRow to tree-structure-compose (#39) (#48)
Add the `android` target to the Compose Multiplatform library and a sensible
default renderer, so the largest Compose audience is a first-class consumer.

- Core `tree-structure` and `tree-structure-compose` now build and publish an
  `-android` variant (compileSdk 35, minSdk 21). The core needs it too: its
  inline `tree { }` DSL is built per target, and Android consumers (JVM 11/17)
  cannot inline a JVM-21 build.
- `TreeNodeRow`: a foundation-only default node row (clickable, indented, with a
  `▾`/`▸` marker, no Material dependency), plus a no-content `LazyTree(root,
  label = …)` overload that uses it. The existing lambda overload is unchanged.
- New `samples` Android app module demonstrating `LazyTree` + `@Preview`
  (not published; excluded from API validation).
- Toolchain: Gradle wrapper 8.5 -> 8.10.2, Android Gradle Plugin 8.7.2.
- binary-compatibility-validator now emits per-target dumps (api/jvm, api/android)
  for the two multi-JVM-target modules; CI assembles the Android outputs.
2026-06-08 19:43:05 +00:00
Adrian Kuta
1fce412815 feat: add runnable :samples module (#47)
Some checks failed
Test / JVM / JS / Wasm / Native + API check (push) Has been cancelled
Test / iOS (push) Has been cancelled
* build: scaffold :samples module (issue #37)

* feat: add runnable, verified examples to :samples (issue #37)

* ci,docs: run :samples in CI and document ./gradlew :samples:run (issue #37)
2026-06-08 13:59:16 +02:00
Adrian Kuta
2671c46f96 test: property-based tests for traversal & structural invariants (#38) (#46)
Add TreeNodePropertyTest: 23 properties checked over many seeded, randomly
generated trees instead of fixed examples. Covers the invariants from #38 —
all three orders visit the same node set with matching cardinality; pre/post
emit each subtree as a contiguous block; level-order is depth-monotonic;
child.depth == parent.depth + 1 (cross-checked against an independent BFS
level walk); nodeCount stays consistent across attach/detach, remove/insert
and clear; and every traversal terminates and is correctly ordered on deep
(5k-node chain) and wide (5k-child) trees. Also pins parent/child pointer
consistency, ancestor/leaf/sibling correctness, deepCopy/mapValues shape
preservation, and lca/distance/pathBetween.

The generator builds trees by uniform random attachment (iterative, so it is
stack-safe and free of left-heavy skew). Failures print the seed, so any case
reproduces. No new dependency — the suite is pure commonTest and runs on every
target (JVM/JS/Wasm/Native).
2026-06-08 13:29:30 +02:00
Adrian Kuta
f1e9a7bb54 chore: gitignore local superpowers working docs (docs/superpowers/) 2026-06-08 08:55:04 +02:00
Adrian Kuta
a7d5de1bba docs: remove design spec accidentally committed in v4.1.1 release 2026-06-08 08:53:12 +02:00
Adrian Kuta
160d7c8059 Release 4.1.1: publish on macOS to restore Apple/iOS artifacts 2026-06-08 08:37:35 +02:00
Adrian Kuta
f60a5c4a86 Release 4.1.0: structural mutations, query algorithms, customizable prettyString, immutable module
Some checks failed
Test / JVM / JS / Wasm / Native + API check (push) Has been cancelled
Test / iOS (push) Has been cancelled
2026-06-07 23:31:15 +02:00
Adrian Kuta
0b8429c859 ci: cache Gradle, harden permissions, fix test triggers (#45)
- Add gradle/actions/setup-gradle caching to all three workflows
- test.yml: trigger on pull_request + push to master (was branches-ignore: master), so PRs from forks are covered and master is verified after merge; add least-privilege permissions and PR-only concurrency
- publishRelease.yml: drop unused 'secrets: inherit' and the dead SNAPSHOT env var (Gradle reads the snapshot project property, not a plain env var); add contents: read permissions; fix the misleading Maven Central comment (upload only stages on Central Portal, the final Publish is manual)
- docs.yml: add Gradle caching
2026-06-07 23:25:55 +02:00
Adrian Kuta
f47fb091ec feat: add tree-structure-immutable module (persistent ImmutableTreeNode) (#33) (#44) 2026-06-07 22:40:41 +02:00
Adrian Kuta
6758a68522 feat: add customizable prettyString with renderer and connector styles (#36) (#43) 2026-06-07 22:38:30 +02:00
Adrian Kuta
30b2709803 feat: add tree query algorithms (lowestCommonAncestor/distance/pathBetween/contains) (#35) (#42) 2026-06-07 22:36:48 +02:00
Adrian Kuta
06eae4841e feat: add structural mutation helpers (insert/move/replace/sort children) (#34) (#41) 2026-06-07 22:35:04 +02:00
Adrian Kuta
1f60b854de Publish API reference (Dokka HTML) to GitHub Pages (#32) (#40)
* docs: design spec for Dokka HTML API reference on GitHub Pages (#32)

* docs: implementation plan for Dokka API reference on GitHub Pages (#32)

* build: migrate Dokka 1.9.20 -> 2.2.0 (DGP v2) (#32)

* docs: aggregate all modules into one Dokka HTML site with source links (#32)

* docs: add Dokka source links to serialization/coroutines/compose modules (#32)

* ci: add docs workflow to deploy Dokka HTML to GitHub Pages (#32)

* docs: link the published API reference from the README (#32)
2026-06-07 21:48:58 +02:00
Adrian Kuta
100054585f chore: ignore the Kotlin 2.x .kotlin/ build cache directory 2026-06-07 19:53:08 +02:00
Adrian Kuta
a7018b8c61 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.
2026-06-07 19:50:02 +02:00
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
Adrian Kuta
60c50f078d Bump version to 3.1.2 2025-10-14 18:47:18 +02:00
Adrian Kuta
54f5074483 26-add-ios-target (#28)
* Refactor: Modernize build and publishing workflow

- Upgraded Kotlin from 1.7.20 to 1.9.20.
- Upgraded JDK from 11 to 21 across all GitHub Actions workflows.
- Replaced the legacy `maven-publish` plugin with `com.vanniktech.maven.publish` for simplified publishing to Maven Central.
- Removed the `publishSnapshot.yml` workflow, as snapshot publishing is now managed by the new plugin.
- Updated `publishRelease.yml` to use the `publishToMavenCentral` task and new secrets (`MAVEN_CENTRAL_USERNAME`, `MAVEN_CENTRAL_PASSWORD`).
- Simplified `build.gradle.kts` by removing manual publishing and signing logic.
- Set the JVM target to "21".
- Bumped the project version to `3.1.1`.
- Removed `js` targets and related configurations.
- Added a new code style configuration file for the project (`.idea/codeStyles/Project.xml`).

* fix: Add JavaScript targets

- Added JS (IR) targets (`browser` and `nodejs`) to the Kotlin Multiplatform configuration for publishing.

* Improve README and document new publishing workflow

- Expanded the project description to highlight key features like Kotlin Multiplatform support, traversal iterators, DSL, and utility functions.
- Added a new "Installation" section with instructions for Gradle (Kotlin/Groovy) and Maven.
- Added a "Traversal and utilities" section to demonstrate usage of iterators and helper functions.
- Replaced the old publishing documentation with updated instructions for publishing to Maven Central via the Central Portal.
- Detailed the two publishing methods: automatically via GitHub Actions on release and manually via local Gradle commands.
- Clarified required secrets and environment variables for the new publishing process.
2025-10-14 18:46:05 +02:00
Adrian Kuta
772eeb0465 Migrate publishing to Sonatype s01 and Central Portal
Some checks failed
Publish Snapshot / test (push) Has been cancelled
Publish Snapshot / Publish Snapshot (push) Has been cancelled
- Updated build script to support publishing to Sonatype's s01.oss.sonatype.org, which is compatible with the new Maven Central Portal.
- Prioritized Central Portal credentials (CENTRAL_USERNAME, CENTRAL_PASSWORD) over legacy OSSRH credentials.
- Updated repository URLs for releases and snapshots to point to s01.oss.sonatype.org.
- Added documentation in README.md explaining the new publishing setup, environment variables, and Gradle tasks.
2025-09-09 09:12:20 +02:00
Adrian Kuta
456f889b9c Upgrade Gradle and add iOS targets (#27)
Some checks failed
Publish Snapshot / test (push) Has been cancelled
Publish Snapshot / Publish Snapshot (push) Has been cancelled
- Upgraded Gradle wrapper to version 8.5.
- Updated Gradle build scripts and configurations.
- Added iOS targets (iosX64, iosArm64, iosSimulatorArm64) to the Kotlin Multiplatform setup.
- Configured shared iOS source sets.
- Bumped project version to 3.1.0.
- Updated JDK version to 21 in project settings.
- Removed kotlinScripting.xml.
- Added runConfigurations.xml and AndroidProjectSystem.xml.

(cherry picked from commit 06dc507590)
2025-09-05 11:17:54 +02:00
Adrian Kuta
c3a4ca5925 Revert "Upgrade Gradle and add iOS targets"
This reverts commit 06dc507590.
2025-09-05 11:14:44 +02:00
Adrian Kuta
06dc507590 Upgrade Gradle and add iOS targets
- Upgraded Gradle wrapper to version 8.5.
- Updated Gradle build scripts and configurations.
- Added iOS targets (iosX64, iosArm64, iosSimulatorArm64) to the Kotlin Multiplatform setup.
- Configured shared iOS source sets.
- Bumped project version to 3.1.0.
- Updated JDK version to 21 in project settings.
- Removed kotlinScripting.xml.
- Added runConfigurations.xml and AndroidProjectSystem.xml.
2025-09-05 11:14:05 +02:00
Adrian Kuta
5eaf027dba Create dependabot.yml
Some checks failed
Publish Snapshot / test (push) Has been cancelled
Publish Snapshot / Publish Snapshot (push) Has been cancelled
2025-08-19 21:36:09 +02:00
Adrian Kuta
04c3728fcd 24: Add iterator param to TreeNode's constructor. (#25) 2023-07-24 18:50:46 +02:00
Adrian Kuta
d34050e9af Merge pull request #23 from AdrianKuta/develop
Relese 3.0.1
2023-02-22 14:32:58 +01:00
Adrian Kuta
2c8381deac Bump version to v3.0.1 2023-02-22 14:30:18 +01:00
Adrian Kuta
4aacbc9dbc 21: Added path function to TreeNode (#22)
Path refers to the sequence of nodes along the edges of a tree.
2023-02-22 14:29:22 +01:00
Adrian Kuta
8a4e677ebf Merge remote-tracking branch 'origin/master' into develop 2023-02-22 12:45:16 +01:00
Adrian Kuta
84e85f3240 Add javaDoc to release library (#20) 2022-12-16 22:26:14 +01:00
Adrian Kuta
e34a85ac39 Add javaDoc to release library 2022-12-16 22:22:29 +01:00
Adrian Kuta
c306088ea3 Merge pull request #19 from AdrianKuta/develop
Improvements
2022-12-16 19:26:50 +01:00
Adrian Kuta
308c37720d Fix snapshot CI 2022-12-16 19:24:35 +01:00
Adrian Kuta
90af315222 Fix release CI 2022-12-16 19:23:23 +01:00
Adrian Kuta
bd5e4f07ae Fix README.md 2022-12-16 19:22:52 +01:00
Adrian Kuta
ae4757fb9d Set the correct file structure to maintain backward compatibility. (#18) 2022-12-16 19:05:59 +01:00
Adrian Kuta
5dd586f9af 4: Implement Level-order traversal algorithm (#17) 2022-12-16 19:00:25 +01:00
Adrian Kuta
b69e838a8a 5: Implement post-order traversal algorithm (#16) 2022-12-16 18:57:34 +01:00
Adrian Kuta
8a2710339a 14 improve GitHub actions (#15)
* 14: Test CI

* 14: Test CI

* 14: Test CI

* 14: Test CI

* 14: Test CI

* 14: Test CI

* 14: Test CI
2022-12-15 16:28:33 +01:00
Adrian Kuta
dd773e3b16 10 Convert into multiplatform library (#13)
* 10: Convert library into Kotlin Multiplatform library

* 10: Setup CI

* 10: Setup CI

* 10: Test CI

* 10: Test CI

* 10: Test CI

* 10: Test CI

* 10: Test CI

* 10: Test CI

* 10: Test CI
2022-12-15 15:34:36 +01:00
Adrian Kuta
72c94f280d Merge pull request #12 from AdrianKuta/release/v.2.1.0
Release 2.1.0
2022-11-24 23:06:48 +01:00
Adrian Kuta
809914274a Update dependencies (#11) 2022-11-24 23:06:09 +01:00
Adrian Kuta
6f16f1cb85 Update README.md 2022-11-23 09:49:48 +01:00
Adrian Kuta
cad5b2dd95 Iterator class refactor 2021-09-03 13:35:18 +02:00
Adrian Kuta
74a6a65434 Merge pull request #9 from AdrianKuta/pr_test
Set up GitHub Actions
2021-09-03 13:23:25 +02:00
Adrian Kuta
b0a6c701ae Update README.md 2021-09-03 13:23:15 +02:00
Adrian Kuta
1bd47fcb21 Set up GitHub Actions 2021-09-03 13:19:09 +02:00
Adrian Kuta
20bd96918c Set up GitHub Actions 2021-09-03 13:17:27 +02:00
Adrian Kuta
60805b7187 Set up GitHub Actions 2021-09-03 13:14:25 +02:00
Adrian Kuta
12b1df764c Set up GitHub Actions 2021-09-03 13:12:09 +02:00
Adrian Kuta
43b8982b88 Update README.md 2021-09-03 13:02:32 +02:00
Adrian Kuta
7be868648b Set up GitHub Actions 2021-09-03 12:45:31 +02:00
Adrian Kuta
1702d8dfb5 Set up GitHub Actions 2021-09-03 12:39:38 +02:00
Adrian Kuta
8ac2901e23 Set up GitHub Actions 2021-09-03 12:37:28 +02:00
Adrian Kuta
e47c8593da Set up GitHub Actions 2021-09-03 12:31:31 +02:00
114 changed files with 7661 additions and 1486 deletions

View File

@@ -1,86 +0,0 @@
version: 2.1
jobs:
build:
working_directory: ~/code
docker:
- image: circleci/android:api-29
environment:
JVM_OPTS: -Xmx3200m
steps:
- checkout
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: Download Dependencies
command: ./gradlew androidDependencies
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
tests:
working_directory: ~/code
docker:
- image: circleci/android:api-29
environment:
JVM_OPTS: -Xmx3200m
steps:
- checkout
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- store_artifacts:
path: app/build/reports
destination: reports
- run:
name: Run Lint Test
command: ./gradlew lint
- run:
name: Run Tests
command: ./gradlew lint test
- store_artifacts:
path: app/build/reports
destination: reports
- store_test_results:
path: app/build/test-results
deploy:
working_directory: ~/code
docker:
- image: circleci/android:api-29
environment:
JVM_OPTS: -Xmx3200m
steps:
- checkout
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: Build Library
command: ./gradlew :treedatastructure:assembleRelease
- run:
name: Export key
command: |
mkdir treedatastructure/maven
echo ${GPG_KEY_CONTENTS} | base64 -d --ignore-garbage > treedatastructure/maven/secret.gpg
- run:
name: Staging
command: ./gradlew :treedatastructure:publishReleasePublicationToSonatypeRepository
- run:
name: Release Library
command: ./gradlew closeAndReleaseRepository
workflows:
version: 2.1
build-and-deploy:
jobs:
- build:
filters: # required since `deploy` has tag filters AND requires `build`
tags:
only: /.*/
- deploy:
requires:
- build
filters:
tags:
only: /v[0-9]{1,3}\.[0-9]{1,3}\.?[0-9]{0,3}/
branches:
ignore: /.*/

48
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,48 @@
name: Docs
on:
release:
types: [released]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
name: Build Dokka HTML
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Generate API docs
run: ./gradlew :dokkaGeneratePublicationHtml --console=plain
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: build/dokka/html
deploy:
name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -1,38 +0,0 @@
name: Publish
on:
release:
# We'll run this workflow when a new GitHub release is created
types: [released]
jobs:
publish:
name: Release build and publish
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
distribution: adopt
java-version: 11
# Builds the release artifacts of the library
- name: Release build
run: ./gradlew :treedatastructure:assembleRelease
# Generates other artifacts (javadocJar is optional)
- name: Source jar and dokka
run: ./gradlew androidSourcesJar javadocJar
# Runs upload, and then closes & releases the repository
- name: Publish to MavenCentral
run: ./gradlew publishReleasePublicationToSonatypeRepository --max-workers 1 closeAndReleaseSonatypeStagingRepository
env:
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }}

43
.github/workflows/publishRelease.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Publish
on:
release:
# We'll run this workflow when a new GitHub release is created
types: [released]
permissions:
contents: read
jobs:
test:
uses: ./.github/workflows/test.yml
publish:
needs: test
name: Publish Production
environment: production
# MUST be macOS: Kotlin/Native Apple targets (iosArm64/iosX64/iosSimulatorArm64) can only be
# built on a macOS host. On a Linux runner those tasks are silently skipped, so the iOS klibs
# never get uploaded (the build still goes green) — exactly what happened for 3.1.54.1.0.
runs-on: macos-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
# Uploads & stages the release on Central Portal. The final "Publish"
# step is manual there, because build.gradle.kts sets
# publishToMavenCentral(automaticRelease = false).
- name: Publish to MavenCentral
run: ./gradlew publishToMavenCentral
env:
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}

41
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: Test
on:
push:
branches: [master]
pull_request:
workflow_call:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: JVM / JS / Wasm / Native / Android + API check
os: ubuntu-latest
tasks: jvmTest jsNodeTest wasmJsNodeTest nativeTest :assembleRelease :tree-structure-compose:assembleRelease :samples-android:assembleDebug :samples:test :samples:run apiCheck
- name: iOS
os: macos-latest
tasks: iosSimulatorArm64Test
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Test
run: ./gradlew ${{ matrix.tasks }} --console=plain

6
.gitignore vendored
View File

@@ -1,5 +1,6 @@
*.iml
.gradle
/.kotlin/
/local.properties
/.idea/caches
/.idea/libraries
@@ -9,6 +10,7 @@
/.idea/assetWizardSettings.xml
.DS_Store
/build
build/
/captures
.externalNativeBuild
.cxx
@@ -16,3 +18,7 @@
/.idea/compiler.xml
/.idea/jarRepositories.xml
/.idea/deploymentTargetDropDown.xml
/.idea/
# Local superpowers working docs (design specs, plans) — not part of the repo
docs/superpowers/

View File

@@ -1,125 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<MarkdownNavigatorCodeStyleSettings>
<option name="RIGHT_MARGIN" value="72" />
</MarkdownNavigatorCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<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>

22
.idea/gradle.xml generated
View File

@@ -1,22 +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="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="11 (2)" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/treedatastructure" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

9
.idea/misc.xml generated
View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</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>

122
CHANGELOG.md Normal file
View File

@@ -0,0 +1,122 @@
# 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).
## [4.2.0] - 2026-06-08
### Added
- **Android target.** The core `tree-structure` module and `tree-structure-compose` now publish an
`-android` variant (`minSdk` 21), so both can be consumed directly in Android projects.
- `tree-structure-compose`: a foundation-only `TreeNodeRow` composable — a sensible default node row
(clickable, indented, with a `▾`/`▸` marker and no Material dependency) — plus a no-content
`LazyTree(root, label = …)` overload that renders each node with it. The existing lambda overload
is unchanged.
- A runnable Android sample app in the new `samples` module, with `@Preview` composables.
### Changed
- Upgraded the Gradle wrapper to 8.10.2 and introduced Android Gradle Plugin 8.7.2 (`compileSdk` 35).
No source or behavior changes to existing targets.
## [4.1.1] - 2026-06-08
### Fixed
- Restored the Apple/iOS artifacts (`iosArm64`, `iosX64`, `iosSimulatorArm64`) for the core and every
module. The release workflow published from a Linux runner, which cannot build Kotlin/Native Apple
targets, so they were silently omitted from 3.1.54.1.0; publishing now runs on macOS. No API or
behavior changes from 4.1.0 — this is a packaging fix only.
## [4.1.0] - 2026-06-07
### Added
- Structural mutation helpers on `TreeNode`: `insertChild`, `removeChildAt`, `replaceChild`,
`moveChild`, `addChildren`, and `sortChildren`.
- Tree query extensions: `lowestCommonAncestor`, `distance`, `pathBetween`, and `contains` for
finding common ancestors, edge distances, the path between two nodes, and value membership.
- Customizable `prettyString(connectors, render)` extension: choose connector glyphs via
`TreeConnectors` (`Default` box-drawing or `Ascii`) and supply a per-node renderer that receives
the value, its depth and whether it is its parent's last child. The all-defaults call is
byte-identical to the existing no-arg `prettyString()`.
- New `tree-structure-immutable` module: a persistent `ImmutableTreeNode` with structural sharing
(`addChild`/`removeChild`/`mapValues` return new roots; pre/post/level-order traversals,
`nodeCount`, and `height`).
### 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]
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.
[4.2.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v4.1.1...v4.2.0
[4.1.1]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v4.1.0...v4.1.1
[4.1.0]: https://github.com/AdrianKuta/Tree-Data-Structure/compare/v4.0.0...v4.1.0
[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

274
README.md
View File

@@ -1,79 +1,259 @@
# Tree (Data Structure)
[![maven](https://img.shields.io/maven-central/v/com.github.adriankuta/tree-structure?style=plastic)](https://mvnrepository.com/artifact/com.github.adriankuta/tree-structure)
[![License: MIT](https://img.shields.io/github/license/AdrianKuta/Tree-Data-Structure?style=plastic)](https://github.com/AdrianKuta/Design-Patterns-Kotlin/blob/master/LICENSE)
[![CircleCI](https://img.shields.io/circleci/build/github/AdrianKuta/Tree-Data-Structure/master?label=CircleCI&style=plastic&logo=circleci)](https://circleci.com/gh/AdrianKuta/Tree-Data-Structure)
[![License: MIT](https://img.shields.io/github/license/AdrianKuta/Tree-Data-Structure?style=plastic)](https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/LICENSE)
[![Publish](https://github.com/AdrianKuta/Tree-Data-Structure/actions/workflows/publishRelease.yml/badge.svg)](https://github.com/AdrianKuta/Tree-Data-Structure/actions/workflows/publishRelease.yml)
[![API docs](https://img.shields.io/badge/docs-API%20reference-blue?style=plastic)](https://adriankuta.github.io/Tree-Data-Structure/)
Simple implementation to store object in tree structure.
📖 **[API reference](https://adriankuta.github.io/Tree-Data-Structure/)** — full KDoc for the core and all modules.
## Usage
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**
It fits homogeneous trees of arbitrary depth: UI component hierarchies, file-system views, org
charts, and category menus. For fixed, typed hierarchies (like a compiler AST) a sealed class is
usually a better fit.
## Features
- Kotlin Multiplatform: JVM, Android, JS, Wasm, iOS, and a native host target.
- 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
Gradle (Kotlin DSL):
```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())
// commonMain for KMP projects, or any sourceSet/module where you need it
dependencies {
implementation("com.github.adriankuta:tree-structure:4.2.0") // latest version is on the badge above
}
```
**Pretty Kotlin**
Gradle (Groovy):
```groovy
dependencies {
implementation "com.github.adriankuta:tree-structure:4.2.0"
}
```
Maven:
```xml
<dependency>
<groupId>com.github.adriankuta</groupId>
<artifactId>tree-structure</artifactId>
<version>4.2.0</version>
</dependency>
```
## Building a tree
The DSL is the shortest way to build one:
```kotlin
val root =
tree("World") {
child("North America") {
child("USA")
}
child("Europe") {
child("Poland")
child("Germany")
}
val root = tree("World") {
child("North America") { child("USA") }
child("Europe") {
child("Poland")
child("Germany")
}
}
```
**Java**
The same node-by-node API works from Kotlin and Java:
```java
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");
root.addChild(northA);
root.addChild(europe);
TreeNode<String> usa = new TreeNode<>("USA");
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());
europe.addChild(new TreeNode<>("Poland"));
europe.addChild(new TreeNode<>("Germany"));
```
*Output:*
`prettyString()` renders the tree for logs and debugging:
```
World
├── North America
│ └── USA
└── Europe
├── Poland
└── France
└── Germany
```
## Traversal
## Download
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)
```
Traversal is also exposed as a lazy `Sequence`, so it composes with the standard library and stops
early instead of materializing the whole tree:
```kotlin
root.preOrderSequence().map { it.value }.toList() // [World, North America, USA, Europe, Poland, Germany]
root.levelOrderSequence().first { it.value == "USA" } // stops as soon as it is found
root.asSequence(TreeNodeIterators.PostOrder).count() // 6
```
## Navigation
```kotlin
val usa = root.findNode { it == "USA" }!!
usa.isLeaf // true
usa.depth() // 2
usa.root().value // "World"
usa.ancestors().map { it.value } // [North America, World]
root.leaves().map { it.value } // [USA, Poland, Germany]
```
## Functional operations
```kotlin
root.anyNode { it == "Poland" } // true
root.filterNodes { it.length > 5 } // nodes whose value is longer than 5 characters
root.countNodes { it.startsWith("U") } // 1
val lengths: TreeNode<Int> = root.mapValues { it.length } // a new tree; the original is untouched
val copy = root.deepCopy()
root.structurallyEquals(copy) // true: same values and shape, different nodes
```
## Utilities
```kotlin
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
The core artifact has no third-party dependencies. Each integration is a separate, opt-in artifact
that depends on the core.
### Serialization (`tree-structure-serialization`)
`kotlinx.serialization` support. A `TreeNode` keeps a reference back to its parent, so it cannot be
`@Serializable` directly. Convert to and from the acyclic `TreeNodeDto` instead.
```kotlin
implementation("com.github.adriankuta:tree-structure-serialization:4.2.0")
```
```kotlin
val json = Json.encodeToString(root.toDto())
val restored = Json.decodeFromString<TreeNodeDto<String>>(json).toTreeNode()
```
### Coroutines (`tree-structure-coroutines`)
Traverse a tree as a cold `Flow`, which is handy inside coroutine and `ViewModel` pipelines.
```kotlin
implementation("com.github.adriankuta:tree-structure-coroutines:4.2.0")
```
```kotlin
root.preOrderFlow().collect { println(it.value) }
root.asFlow(TreeNodeIterators.LevelOrder).map { it.value }
```
### Compose UI (`tree-structure-compose`)
A `LazyTree` composable for Compose Multiplatform (JVM/desktop, Android, iOS, Wasm). Only the visible
nodes are composed.
```kotlin
implementation("com.github.adriankuta:tree-structure-compose:4.2.0")
```
For the common case, the no-content overload renders each node with the built-in `TreeNodeRow`
(a clickable, indented row with a `▾`/`▸` marker — foundation-only, no Material dependency):
```kotlin
LazyTree(root) // sensible default
LazyTree(root, label = { it.name }) // map a node's value to its text
```
Or supply your own row for full control:
```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())
}
}
```
A runnable Android demo lives in the [`samples-android`](samples-android) module.
### Immutable (`tree-structure-immutable`)
A persistent `ImmutableTreeNode` with structural sharing. Every operation (`addChild`,
`removeChild`, `mapValues`) returns a **new** root and leaves the original untouched; unchanged
subtrees are reused, so updates are cheap and old roots stay valid. Backed by
`kotlinx.collections.immutable`.
```kotlin
implementation("com.github.adriankuta:tree-structure-immutable:4.2.0")
```
```kotlin
val root = ImmutableTreeNode("World").addChild(ImmutableTreeNode("Europe"))
val bigger = root.addChild(ImmutableTreeNode("Asia")) // root is unchanged; bigger is a new tree
bigger.preOrder().forEach { println(it.value) } // pre/post/level-order, nodeCount(), height()
```
## Examples
A runnable `:samples` module bundles compile-checked, assertion-verified examples of the core API
and the serialization, coroutines, and immutable modules. Run them with:
```
./gradlew :samples:run
```
## Notes
`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.
Coming from 3.x? See [CHANGELOG.md](CHANGELOG.md) for the 4.0 migration notes.
## Releasing (maintainers)
Releases go to Maven Central through the Sonatype Central Portal using the
`com.vanniktech.maven.publish` plugin. Creating a GitHub release runs
`.github/workflows/publishRelease.yml`, which signs and uploads every module; the deployment is then
published from central.sonatype.com. The published version comes from `PUBLISH_VERSION` in
`build.gradle.kts`. CI needs the `MAVEN_CENTRAL_USERNAME`, `MAVEN_CENTRAL_PASSWORD`, `SIGNING_KEY`,
and `SIGNING_PASSWORD` repository secrets. To publish from a local machine, set the matching
`ORG_GRADLE_PROJECT_*` properties and run `./gradlew publishToMavenCentral` (add `-Psnapshot=true`
for a snapshot build).
implementation "com.github.adriankuta:tree-structure:$latest_versions"
## License
MIT License

View File

@@ -1,3 +0,0 @@
theme: jekyll-theme-minimal
title: Tree Data Structure
logo: images/console_output.png

View File

@@ -0,0 +1,142 @@
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 final class com/github/adriankuta/datastructure/tree/TreeConnectors {
public static final field Companion Lcom/github/adriankuta/datastructure/tree/TreeConnectors$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/github/adriankuta/datastructure/tree/TreeConnectors;
public static synthetic fun copy$default (Lcom/github/adriankuta/datastructure/tree/TreeConnectors;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/github/adriankuta/datastructure/tree/TreeConnectors;
public fun equals (Ljava/lang/Object;)Z
public final fun getBranch ()Ljava/lang/String;
public final fun getEmpty ()Ljava/lang/String;
public final fun getLastBranch ()Ljava/lang/String;
public final fun getVertical ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class com/github/adriankuta/datastructure/tree/TreeConnectors$Companion {
public final fun getAscii ()Lcom/github/adriankuta/datastructure/tree/TreeConnectors;
public final fun getDefault ()Lcom/github/adriankuta/datastructure/tree/TreeConnectors;
}
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 final fun addChildren ([Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public synthetic fun child (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun clear ()V
public final fun depth ()I
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 insertChild (ILcom/github/adriankuta/datastructure/tree/TreeNode;)V
public final fun isRoot ()Z
public fun iterator ()Ljava/util/Iterator;
public final fun iterator (Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)Ljava/util/Iterator;
public final fun moveChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;I)Z
public final fun nodeCount ()I
public final fun path (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public final fun prettyString ()Ljava/lang/String;
public final fun removeChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z
public final fun removeChildAt (I)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun replaceChild (ILcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun sortChildren (Ljava/util/Comparator;)V
public fun toString ()Ljava/lang/String;
}
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/TreeNodePrettyPrintExtKt {
public static final fun prettyString (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeConnectors;Lkotlin/jvm/functions/Function3;)Ljava/lang/String;
public static synthetic fun prettyString$default (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeConnectors;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Ljava/lang/String;
}
public final class com/github/adriankuta/datastructure/tree/TreeNodeQueryExtKt {
public static final fun contains (Lcom/github/adriankuta/datastructure/tree/TreeNode;Ljava/lang/Object;)Z
public static final fun distance (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/lang/Integer;
public static final fun lowestCommonAncestor (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun pathBetween (Lcom/github/adriankuta/datastructure/tree/TreeNode;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;
}

142
api/jvm/tree-structure.api Normal file
View File

@@ -0,0 +1,142 @@
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 final class com/github/adriankuta/datastructure/tree/TreeConnectors {
public static final field Companion Lcom/github/adriankuta/datastructure/tree/TreeConnectors$Companion;
public fun <init> (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun component2 ()Ljava/lang/String;
public final fun component3 ()Ljava/lang/String;
public final fun component4 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lcom/github/adriankuta/datastructure/tree/TreeConnectors;
public static synthetic fun copy$default (Lcom/github/adriankuta/datastructure/tree/TreeConnectors;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/github/adriankuta/datastructure/tree/TreeConnectors;
public fun equals (Ljava/lang/Object;)Z
public final fun getBranch ()Ljava/lang/String;
public final fun getEmpty ()Ljava/lang/String;
public final fun getLastBranch ()Ljava/lang/String;
public final fun getVertical ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}
public final class com/github/adriankuta/datastructure/tree/TreeConnectors$Companion {
public final fun getAscii ()Lcom/github/adriankuta/datastructure/tree/TreeConnectors;
public final fun getDefault ()Lcom/github/adriankuta/datastructure/tree/TreeConnectors;
}
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 final fun addChildren ([Lcom/github/adriankuta/datastructure/tree/TreeNode;)V
public synthetic fun child (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun clear ()V
public final fun depth ()I
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 insertChild (ILcom/github/adriankuta/datastructure/tree/TreeNode;)V
public final fun isRoot ()Z
public fun iterator ()Ljava/util/Iterator;
public final fun iterator (Lcom/github/adriankuta/datastructure/tree/iterators/TreeNodeIterators;)Ljava/util/Iterator;
public final fun moveChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;I)Z
public final fun nodeCount ()I
public final fun path (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/util/List;
public final fun prettyString ()Ljava/lang/String;
public final fun removeChild (Lcom/github/adriankuta/datastructure/tree/TreeNode;)Z
public final fun removeChildAt (I)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun replaceChild (ILcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public final fun sortChildren (Ljava/util/Comparator;)V
public fun toString ()Ljava/lang/String;
}
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/TreeNodePrettyPrintExtKt {
public static final fun prettyString (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeConnectors;Lkotlin/jvm/functions/Function3;)Ljava/lang/String;
public static synthetic fun prettyString$default (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeConnectors;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Ljava/lang/String;
}
public final class com/github/adriankuta/datastructure/tree/TreeNodeQueryExtKt {
public static final fun contains (Lcom/github/adriankuta/datastructure/tree/TreeNode;Ljava/lang/Object;)Z
public static final fun distance (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeNode;)Ljava/lang/Integer;
public static final fun lowestCommonAncestor (Lcom/github/adriankuta/datastructure/tree/TreeNode;Lcom/github/adriankuta/datastructure/tree/TreeNode;)Lcom/github/adriankuta/datastructure/tree/TreeNode;
public static final fun pathBetween (Lcom/github/adriankuta/datastructure/tree/TreeNode;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;
}

1
app/.gitignore vendored
View File

@@ -1 +0,0 @@
/build

View File

@@ -1,33 +0,0 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion 31
buildToolsVersion "31.0.0"
defaultConfig {
applicationId "com.github.adriankuta.treedatastructure"
minSdkVersion 15
targetSdkVersion 31
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
implementation 'com.github.adriankuta:tree-structure:1.2.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

View File

@@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -1,24 +0,0 @@
package com.github.adriankuta.treedatastructure
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.github.adriankuta.treedatastructure", appContext.packageName)
}
}

View File

@@ -1,29 +0,0 @@
package com.github.adriankuta.treedatastructure
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.github.adriankuta.datastructure.tree.tree
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val root =
tree("World") {
child("North America") {
child("USA") {
child("LA")
child("New York")
}
}
child("Europe") {
child("Poland")
child("Germany")
}
}
Log.d("DEBUG_TAG", root.prettyString())
}
}

View File

@@ -1,34 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeWidth="1"
android:strokeColor="#00000000">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#008577"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#008577</color>
<color name="colorPrimaryDark">#00574B</color>
<color name="colorAccent">#D81B60</color>
</resources>

View File

@@ -1,3 +0,0 @@
<resources>
<string name="app_name">Tree Data Structure</string>
</resources>

View File

@@ -1,11 +0,0 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>

View File

@@ -1,17 +0,0 @@
package com.github.adriankuta.treedatastructure
import org.junit.Test
import org.junit.Assert.*
/**
* 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

@@ -1,32 +0,0 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.5.30'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
plugins {
id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
id("org.jetbrains.dokka") version "1.5.0"
}
allprojects {
repositories {
google()
mavenCentral()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
apply from: "${rootDir}/scripts/publish-root.gradle"

156
build.gradle.kts Normal file
View File

@@ -0,0 +1,156 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.dokka)
alias(libs.plugins.mavenPublish)
alias(libs.plugins.binaryCompatibilityValidator)
alias(libs.plugins.kover)
signing
alias(libs.plugins.androidLibrary)
// Loaded once here (with a known version) so the Android application/library variants can be
// applied across subprojects without the "already on the classpath with an unknown version" clash.
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.kotlinAndroid) apply false
}
val PUBLISH_GROUP_ID = "com.github.adriankuta"
val PUBLISH_ARTIFACT_ID = "tree-structure" // base artifact; KMP will add -jvm, -ios*, etc.
val PUBLISH_VERSION = "4.2.0"
val snapshot: String? by project
group = PUBLISH_GROUP_ID
version = if (snapshot.toBoolean()) "$PUBLISH_VERSION-SNAPSHOT" else PUBLISH_VERSION
mavenPublishing {
// Central Portal + auto release when we call publishAndReleaseToMavenCentral
publishToMavenCentral(automaticRelease = false)
signAllPublications()
coordinates(PUBLISH_GROUP_ID, PUBLISH_ARTIFACT_ID, version.toString())
pom {
name.set("Tree Data 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 {
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()
}
apiValidation {
// Neither sample module is a published artifact, so neither has a .api dump.
ignoredProjects.add("samples")
ignoredProjects.add("samples-android")
}
dependencies {
// Include this module's own docs in the aggregation — DGP v2 requires the
// aggregating project to list itself explicitly.
dokka(project(":"))
dokka(project(":tree-structure-serialization"))
dokka(project(":tree-structure-coroutines"))
dokka(project(":tree-structure-compose"))
dokka(project(":tree-structure-immutable"))
}
dokka {
moduleName.set("Tree Data Structure")
dokkaSourceSets.configureEach {
sourceLink {
localDirectory.set(projectDir.resolve("src"))
// For the root project projectDir == rootDir, so `module` is "" and links resolve to /blob/master/src.
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
kotlin {
explicitApi()
jvmToolchain(21)
jvm()
androidTarget {
publishLibraryVariants("release")
// Build the Android variant at JVM 17 so Android consumers (JVM 11/17) can inline the
// library's inline DSL (`tree { }`) — they cannot inline the default JVM-21 bytecode.
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
js(IR) {
browser()
nodejs()
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
nodejs()
}
// Apple targets
iosX64()
iosArm64()
iosSimulatorArm64()
// Native host target
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 {
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
android {
namespace = "com.github.adriankuta.datastructure.tree"
compileSdk = libs.versions.androidCompileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.androidMinSdk.get().toInt()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}

View File

@@ -0,0 +1,435 @@
# API Reference (Dokka HTML) on GitHub Pages — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Migrate Dokka to 2.x and publish an aggregated, source-linked multi-module API reference for all four modules to GitHub Pages on each release and on demand.
**Architecture:** Bump Dokka `1.9.20 → 2.2.0` and switch on the Dokka Gradle Plugin v2 (`V2Enabled`). The root project (the published core module) aggregates the three submodules via `dokka(project(...))` dependencies, producing one HTML site at `build/dokka/html`. A new `docs.yml` workflow builds that site and deploys it with the official GitHub Pages Actions. The vanniktech maven-publish plugin (0.34.0) keeps building the Maven Central `-javadoc.jar` — it already supports Dokka V2, so the release pipeline is unaffected.
**Tech Stack:** Kotlin Multiplatform 2.1.0, Gradle 8.5, Dokka Gradle Plugin 2.2.0, vanniktech maven-publish 0.34.0, GitHub Actions (`upload-pages-artifact@v3`, `deploy-pages@v4`).
**Spec:** `docs/superpowers/specs/2026-06-07-publish-api-reference-dokka-github-pages-design.md`
> **Note on "tests":** This is build/CI work, so each task's verification is a Gradle command or a YAML lint with a concrete expected result rather than a unit test. Treat the "verify" steps as the failing/passing check.
---
## File Structure
| File | Responsibility | Action |
| --- | --- | --- |
| `gradle/libs.versions.toml` | Centralized Dokka version | Modify (`dokka = "2.2.0"`) |
| `gradle.properties` | Enable DGP v2 plugin mode | Modify (add 2 flags) |
| `build.gradle.kts` (root) | Aggregate 3 submodules; root site title + source links | Modify (add `dependencies` + `dokka {}` blocks) |
| `tree-structure-serialization/build.gradle.kts` | Source links for this module | Modify (add `dokka {}` block) |
| `tree-structure-coroutines/build.gradle.kts` | Source links for this module | Modify (add `dokka {}` block) |
| `tree-structure-compose/build.gradle.kts` | Source links for this module | Modify (add `dokka {}` block) |
| `.github/workflows/docs.yml` | Build + deploy site to Pages | Create |
| `README.md` | Docs badge + link | Modify |
Work happens on branch `docs/api-reference-github-pages` (already created; spec already committed there).
---
## Task 1: Migrate Dokka to 2.2.0 and enable DGP v2
**Files:**
- Modify: `gradle/libs.versions.toml` (line `dokka = "1.9.20"`)
- Modify: `gradle.properties`
- [ ] **Step 1: Bump the Dokka version in the catalog**
In `gradle/libs.versions.toml`, change the `dokka` version under `[versions]`:
```toml
dokka = "2.2.0"
```
(Leave the `dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }` plugin line unchanged.)
- [ ] **Step 2: Enable the Dokka Gradle Plugin v2**
Append these two lines to `gradle.properties` (current content is only `kotlin.code.style=official`):
```properties
# Dokka Gradle Plugin v2 (https://kotl.in/dokka-gradle-migration)
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
```
- [ ] **Step 3: Verify the v2 task exists and there is no V1 warning**
Run: `./gradlew :dokkaGeneratePublicationHtml --dry-run --console=plain`
Expected: `BUILD SUCCESSFUL`, a list of `:dokkaGenerate*` tasks printed, and **no** message containing `Dokka Gradle plugin V1` or `migration guide`. (At this point only the root/core module is documented — aggregation is added in Task 2.)
- [ ] **Step 4: Verify the Maven Central javadoc jar still builds under Dokka 2.x**
Run: `./gradlew javadocJar --console=plain`
Expected: `BUILD SUCCESSFUL`. This is the vanniktech-generated jar that Maven Central requires; it must keep working. (If the task name isn't found, list it with `./gradlew tasks --all | grep -i javadoc` and run the reported task — it is the per-module `javadocJar`.)
- [ ] **Step 5: Commit**
```bash
git add gradle/libs.versions.toml gradle.properties
git commit -m "build: migrate Dokka 1.9.20 -> 2.2.0 (DGP v2) (#32)"
```
---
## Task 2: Aggregate all modules + root site title and source links
**Files:**
- Modify: `build.gradle.kts` (root)
- [ ] **Step 1: Add the Dokka aggregation dependencies**
Add this top-level block to the root `build.gradle.kts` (place it after the `repositories { mavenCentral() }` block, at the top level — not inside `kotlin {}`):
```kotlin
dependencies {
dokka(project(":tree-structure-serialization"))
dokka(project(":tree-structure-coroutines"))
dokka(project(":tree-structure-compose"))
}
```
- [ ] **Step 2: Add the root `dokka {}` configuration (site title + source links)**
Add this top-level block to the root `build.gradle.kts` (e.g. right after the `dependencies {}` block from Step 1):
```kotlin
dokka {
moduleName.set("Tree Data Structure")
dokkaSourceSets.configureEach {
sourceLink {
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
```
- [ ] **Step 3: Build the aggregated site**
Run: `./gradlew :dokkaGeneratePublicationHtml --console=plain`
Expected: `BUILD SUCCESSFUL` and the file `build/dokka/html/index.html` exists.
Run: `ls build/dokka/html`
Expected: per-module output directories including `tree-structure`, `tree-structure-serialization`, `tree-structure-coroutines`, and `tree-structure-compose` (plus `index.html`, `navigation.html`, assets).
- [ ] **Step 4: Verify the four modules and root source links are present**
Run: `grep -roh "tree-structure-serialization\|tree-structure-coroutines\|tree-structure-compose" build/dokka/html/index.html | sort -u`
Expected: all three submodule names listed (confirming aggregation).
Run: `grep -rl "github.com/AdrianKuta/Tree-Data-Structure/blob/master/src/" build/dokka/html/tree-structure | head -1`
Expected: at least one file path printed (confirming the root/core module's source links resolve to `.../blob/master/src/...`). Optionally open `build/dokka/html/index.html` in a browser and click a core class's "source" link to confirm it lands on the right GitHub file.
- [ ] **Step 5: Commit**
```bash
git add build.gradle.kts
git commit -m "docs: aggregate all modules into one Dokka HTML site with source links (#32)"
```
---
## Task 3: Source links for the three submodules
The submodules are documented by the aggregation but need their own `sourceLink` so their symbols point at the correct subdirectory on GitHub. The block below is **path-derived and identical** for every module — add the exact same block to all three files.
**Files:**
- Modify: `tree-structure-serialization/build.gradle.kts`
- Modify: `tree-structure-coroutines/build.gradle.kts`
- Modify: `tree-structure-compose/build.gradle.kts`
- [ ] **Step 1: Add the `dokka {}` block to `tree-structure-serialization/build.gradle.kts`**
Add this top-level block (after the `repositories {}` block, before `kotlin {}`):
```kotlin
dokka {
dokkaSourceSets.configureEach {
sourceLink {
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
```
- [ ] **Step 2: Add the same block to `tree-structure-coroutines/build.gradle.kts`**
Add the identical block (after `repositories {}`, before `kotlin {}`):
```kotlin
dokka {
dokkaSourceSets.configureEach {
sourceLink {
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
```
- [ ] **Step 3: Add the same block to `tree-structure-compose/build.gradle.kts`**
Add the identical block (after the `repositories { mavenCentral(); google() }` block, before `kotlin {}`):
```kotlin
dokka {
dokkaSourceSets.configureEach {
sourceLink {
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
```
- [ ] **Step 4: Rebuild the site**
Run: `./gradlew :dokkaGeneratePublicationHtml --console=plain`
Expected: `BUILD SUCCESSFUL`.
- [ ] **Step 5: Verify each submodule's source links resolve to its subdirectory**
Run:
```bash
for m in serialization coroutines compose; do
echo -n "tree-structure-$m: "
grep -rl "github.com/AdrianKuta/Tree-Data-Structure/blob/master/tree-structure-$m/src/" build/dokka/html/tree-structure-$m | head -1 || echo "MISSING"
done
```
Expected: a file path printed for each of the three modules (none "MISSING").
- [ ] **Step 6: Commit**
```bash
git add tree-structure-serialization/build.gradle.kts tree-structure-coroutines/build.gradle.kts tree-structure-compose/build.gradle.kts
git commit -m "docs: add Dokka source links to serialization/coroutines/compose modules (#32)"
```
---
## Task 4: GitHub Pages workflow
**Files:**
- Create: `.github/workflows/docs.yml`
- [ ] **Step 1: Create the workflow file**
Create `.github/workflows/docs.yml` with exactly:
```yaml
name: Docs
on:
release:
types: [released]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
name: Build Dokka HTML
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Generate API docs
run: ./gradlew :dokkaGeneratePublicationHtml --console=plain
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: build/dokka/html
deploy:
name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
```
- [ ] **Step 2: Validate the YAML is well-formed**
Run: `ruby -ryaml -e "YAML.load_file('.github/workflows/docs.yml'); puts 'OK'"`
Expected: prints `OK` with no error. (Ruby ships with macOS. Alternatively, if `actionlint` is installed: `actionlint .github/workflows/docs.yml` → no output.)
- [ ] **Step 3: Commit**
```bash
git add .github/workflows/docs.yml
git commit -m "ci: add docs workflow to deploy Dokka HTML to GitHub Pages (#32)"
```
---
## Task 5: Link the site from the README
**Files:**
- Modify: `README.md` (top badge block, lines 1-6)
- [ ] **Step 1: Add the API docs badge**
In `README.md`, immediately after the existing `Publish` badge line:
```markdown
[![Publish](https://github.com/AdrianKuta/Tree-Data-Structure/actions/workflows/publishRelease.yml/badge.svg)](https://github.com/AdrianKuta/Tree-Data-Structure/actions/workflows/publishRelease.yml)
```
add this new line:
```markdown
[![API docs](https://img.shields.io/badge/docs-API%20reference-blue?style=plastic)](https://adriankuta.github.io/Tree-Data-Structure/)
```
- [ ] **Step 2: Add a one-line pointer under the badges**
After the badge block and its following blank line, and **before** the intro paragraph that starts `A lightweight n-ary tree for Kotlin Multiplatform.`, insert:
```markdown
📖 **[API reference](https://adriankuta.github.io/Tree-Data-Structure/)** — full KDoc for the core and all modules.
```
- [ ] **Step 3: Verify the link is present**
Run: `grep -n "adriankuta.github.io/Tree-Data-Structure" README.md`
Expected: two matches (the badge and the 📖 line).
- [ ] **Step 4: Commit**
```bash
git add README.md
git commit -m "docs: link the published API reference from the README (#32)"
```
---
## Task 6: Full verification, push, and pull request
**Files:** none (verification + integration)
- [ ] **Step 1: Full local verification**
Run each and confirm `BUILD SUCCESSFUL`:
```bash
./gradlew :dokkaGeneratePublicationHtml --console=plain # site builds, all 4 modules
./gradlew javadocJar --console=plain # release javadoc jar intact
./gradlew apiCheck --console=plain # binary-compat baseline unchanged
```
Expected: all three succeed. `apiCheck` must pass with no diff — this change touches only build config and docs, not public API. Optionally open `build/dokka/html/index.html` in a browser for a final visual check (module nav + a couple of source links).
- [ ] **Step 2: Push the branch**
```bash
git push -u origin docs/api-reference-github-pages
```
- [ ] **Step 3: Open the pull request**
```bash
gh pr create --base master --head docs/api-reference-github-pages \
--title "Publish API reference (Dokka HTML) to GitHub Pages (#32)" \
--body "$(cat <<'EOF'
Closes #32.
Migrates Dokka 1.9.20 → 2.2.0 (DGP v2) and publishes an aggregated, source-linked
multi-module API reference to GitHub Pages.
## What changed
- **Dokka 2.2.0 / DGP v2** (`V2Enabled` in `gradle.properties`). Gradle 8.5 and Kotlin
2.1.0 already satisfy Dokka 2.2.0's minimums (7.6+ / 1.9+), so no wrapper/Kotlin bump.
- **Aggregation**: the root (core) module pulls in `-serialization`, `-coroutines`, and
`-compose` via `dokka(project(...))` → one site at `build/dokka/html`
(`:dokkaGeneratePublicationHtml`).
- **Source links** on every module, pointing each symbol at its source on `master`.
- **`.github/workflows/docs.yml`**: builds the site and deploys via the official Pages
Actions on each release and on manual `workflow_dispatch`.
- **README**: docs badge + link to https://adriankuta.github.io/Tree-Data-Structure/
## Release pipeline unaffected
vanniktech 0.34.0 already supports Dokka `V2Enabled`, so the Maven Central
`-javadoc.jar` keeps building (verified locally with `./gradlew javadocJar`).
## ⚠️ One-time manual step required before the site goes live
Enable Pages: **Settings → Pages → Source = "GitHub Actions"**. Until then the
`deploy` job will fail. After enabling, run the **Docs** workflow once via
*Actions → Docs → Run workflow* (`workflow_dispatch`) to publish without waiting for a
release.
EOF
)"
```
Expected: PR URL printed.
- [ ] **Step 4: Post-merge manual steps (call these out to the repo owner)**
1. **Settings → Pages → Source = "GitHub Actions"** (one-time; otherwise `deploy` fails).
2. Trigger **Actions → Docs → Run workflow** (`workflow_dispatch`) to publish immediately.
3. Confirm the site is live at https://adriankuta.github.io/Tree-Data-Structure/ and the module nav + source links work.
---
## Self-Review
**Spec coverage:**
- Dokka 1.9.20 → 2.2.0 + V2 mode → Task 1 ✓
- Multi-module aggregation in root → Task 2 ✓
- Source links (root + 3 submodules) + site title → Tasks 2 & 3 ✓
- `docs.yml` (release + workflow_dispatch, official Pages Actions) → Task 4 ✓
- README badge + link → Task 5 ✓
- Verify javadoc jar / apiCheck / local docs build → Tasks 1, 2, 6 ✓
- Manual Pages-enable prerequisite → Tasks 4/6 PR body + post-merge steps ✓
- Out-of-scope items (versioned docs, Module.md, vanniktech swap, CI cache) → correctly excluded ✓
**Placeholder scan:** No TBD/TODO/"handle edge cases"/"similar to Task N". The identical source-link block is repeated in full in each of Task 3's steps (not referenced) ✓
**Type/name consistency:** Task name `:dokkaGeneratePublicationHtml`, output dir `build/dokka/html`, config functions (`moduleName.set`, `dokkaSourceSets.configureEach`, `sourceLink { localDirectory.set / remoteUrl / remoteLineSuffix.set }`), and `dokka(project(...))` aggregation are used identically across Tasks 16 and the workflow ✓
**Open risk carried from spec:** if Dokka cannot resolve Apple source sets on `ubuntu-latest` in CI (Task 4), switch the `build` job's `runs-on` to `macos-latest` (mirrors the iOS test job). Local verification runs on macOS, which covers Apple source sets.

View File

@@ -0,0 +1,237 @@
# Design: Publish API reference (Dokka HTML) to GitHub Pages
- **Issue:** [#32](https://github.com/AdrianKuta/Tree-Data-Structure/issues/32) — *Publish API reference (Dokka HTML) to GitHub Pages*
- **Date:** 2026-06-07
- **Status:** Approved (design); pending spec review
## Summary
Generate a browsable, multi-module API reference with Dokka and host it on GitHub
Pages. The site aggregates the core (`tree-structure`) plus the `-serialization`,
`-coroutines`, and `-compose` modules, links every symbol back to its source on
GitHub, and is (re)deployed on each GitHub release or on demand. The README points
to it.
## Background / current state
- **Dokka 1.9.20** is applied to all four modules (root core + three submodules) via
`alias(libs.plugins.dokka)`. Today it runs only because the **vanniktech
maven-publish plugin (0.34.0)** uses it to build the `-javadoc.jar` that Maven
Central requires. There is no aggregation config, no docs site, no source links.
- Plugin/library versions are centralized in `gradle/libs.versions.toml`.
- The **root project is itself the published core module** and the natural Dokka
aggregation root.
- Existing workflows: `test.yml` (reusable matrix: JVM/JS/Wasm/Native + `apiCheck`,
plus iOS on macOS) and `publishRelease.yml` (on GitHub release → tests →
`./gradlew publishToMavenCentral`). Both use `actions/checkout@v4` +
`actions/setup-java@v4` (temurin, JDK 21) with no Gradle cache action.
- Gradle wrapper **8.5**; Kotlin **2.1.0**.
### Verified compatibility (de-risking)
- **Dokka 2.2.0** (latest stable) requires **Gradle 7.6+** and **Kotlin 1.9+** → our
8.5 / 2.1.0 satisfy both. **No wrapper bump, no Kotlin bump.**
- **vanniktech 0.34.0** already supports Dokka `V2Enabled` (added in 0.30.0), so the
Maven Central `-javadoc.jar` keeps building under Dokka 2.x. **No vanniktech bump.**
(For reference, 0.36.0 *removes* Dokka v1 support entirely — so V2 is the forward
direction regardless.)
## Goals
1. Migrate Dokka `1.9.20``2.2.0` (DGP v2 / `V2Enabled`) without breaking the
release pipeline's javadoc jar.
2. Produce one aggregated multi-module HTML site for all four modules.
3. Link every documented symbol to its source on GitHub.
4. Deploy the site to GitHub Pages on each release and on manual dispatch.
5. Link the site from the README.
## Non-goals (per the issue's "follow-up" note)
- Versioned / per-release docs (one published version at a time, tracking the latest
release / manual run).
- Long-form `Module.md` package descriptions.
- Changing the publishing tool (vanniktech stays; out of scope for #32).
- Adding a Gradle cache action to CI (keep parity with existing workflows).
## Detailed design
### 1. Dokka 2.x migration
**`gradle/libs.versions.toml`**
```toml
dokka = "2.2.0" # was 1.9.20
```
**`gradle.properties`** — enable DGP v2 and silence the migration notice:
```properties
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
```
The four `alias(libs.plugins.dokka)` plugin applications stay unchanged.
### 2. Multi-module aggregation (root `build.gradle.kts`)
Add a top-level `dependencies { }` block declaring the three submodules as Dokka
aggregation inputs. The root documents itself and pulls in the three:
```kotlin
dependencies {
dokka(project(":tree-structure-serialization"))
dokka(project(":tree-structure-coroutines"))
dokka(project(":tree-structure-compose"))
}
```
- Generating task: **`:dokkaGeneratePublicationHtml`** (root project).
- Output directory: **`build/dokka/html`** (the aggregated site, default location).
### 3. Source links + site title
A `dokka { }` block is added to **each of the four module build files**. The
`sourceLink` derives the per-module GitHub path from the project layout so each
module is correct without hardcoding paths:
```kotlin
dokka {
dokkaSourceSets.configureEach {
sourceLink {
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
```
- For the **root** module, `module` is empty → links resolve to `…/blob/master/src/…`.
- For a submodule (e.g. serialization) → `…/blob/master/tree-structure-serialization/src/…`.
- Links point at the **`master`** branch. Trade-off: a previously deployed page links
to current `master`, which may have drifted. Accepted for simplicity; tag-accurate
links are a possible follow-up.
The **root** module additionally sets a friendly site title:
```kotlin
dokka {
moduleName.set("Tree Data Structure")
// ...sourceLink block as above...
}
```
Submodules keep their default module names (`tree-structure-serialization`,
`tree-structure-coroutines`, `tree-structure-compose`).
### 4. Pages workflow — `.github/workflows/docs.yml`
```yaml
name: Docs
on:
release:
types: [released]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
name: Build Dokka HTML
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- name: Generate API docs
run: ./gradlew :dokkaGeneratePublicationHtml --console=plain
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: build/dokka/html
deploy:
name: Deploy to GitHub Pages
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
```
- Triggers: `release: [released]` (matches the issue) **+** `workflow_dispatch`
(manual rebuild without a release).
- Official GitHub Pages Actions (no `gh-pages` branch).
- Runs on `ubuntu-latest` to match `publishRelease.yml`. **Fallback:** if Dokka
cannot resolve the Apple source sets on Linux CI, switch the `build` job to
`macos-latest` (as the iOS test job already does).
### 5. README
In the badge row near the top, add a docs badge and a one-line pointer:
```markdown
[![API docs](https://img.shields.io/badge/docs-API%20reference-blue?style=plastic)](https://adriankuta.github.io/Tree-Data-Structure/)
```
Plus a short line under the badges:
```markdown
📖 **[API reference](https://adriankuta.github.io/Tree-Data-Structure/)** — full KDoc for all modules.
```
Site URL: `https://adriankuta.github.io/Tree-Data-Structure/`.
### 6. One-time manual step (repo owner, not code)
GitHub Pages must be enabled once: **Settings → Pages → Source = "GitHub Actions"**.
Until then the `deploy` job fails. This will be called out in the PR / final summary.
## Verification (before claiming done)
1. `./gradlew :dokkaGeneratePublicationHtml --console=plain` locally on **macOS**
(covers Apple source sets) → confirm `build/dokka/html/index.html` exists and the
site lists **all four** modules, with working source links.
2. `./gradlew javadocJar --console=plain` → confirm the vanniktech javadoc jar(s)
still build under Dokka 2.x (release pipeline unaffected). Optionally
`./gradlew publishToMavenLocal -Psnapshot=true` for an end-to-end check.
3. `./gradlew apiCheck` and the existing test tasks still pass (no API/source impact).
4. Validate `docs.yml` YAML (well-formed, correct action versions).
## Risks & mitigations
| Risk | Mitigation |
| --- | --- |
| Dokka 2.x breaks the javadoc jar | vanniktech 0.34.0 supports `V2Enabled`; verify with `javadocJar` step 2 above. |
| Dokka can't resolve Apple source sets on Linux CI | Verify locally on macOS; fallback `macos-latest` for the build job. |
| Source-link paths wrong for a module | Derived from `projectDir.relativeTo(rootDir)`; visually verify links in step 1. |
| Pages deploy fails on first run | Documented manual prerequisite (Settings → Pages → GitHub Actions). |
## Files touched
- `gradle/libs.versions.toml` — Dokka version bump.
- `gradle.properties``V2Enabled` flags.
- `build.gradle.kts` (root) — aggregation `dependencies`, `dokka { moduleName + sourceLink }`.
- `tree-structure-serialization/build.gradle.kts``dokka { sourceLink }`.
- `tree-structure-coroutines/build.gradle.kts``dokka { sourceLink }`.
- `tree-structure-compose/build.gradle.kts``dokka { sourceLink }`.
- `.github/workflows/docs.yml` — new Pages workflow.
- `README.md` — docs badge + link.

View File

@@ -1,21 +1,10 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Dokka Gradle Plugin v2 (https://kotl.in/dokka-gradle-migration)
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
# Android: Compose pulls AndroidX artifacts, which AGP requires this flag to consume.
android.useAndroidX=true
# AGP loads many classes into the Gradle daemon's Metaspace; combined with the KMP
# matrix, binary-compatibility-validator, Kover and Dokka in a single build, the
# default 512m heap / 384m Metaspace is exhausted (daemon OOM). Raise both.
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8

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

@@ -0,0 +1,34 @@
[versions]
kotlin = "2.1.0"
agp = "8.7.2"
androidCompileSdk = "35"
androidMinSdk = "21"
activityCompose = "1.9.3"
dokka = "2.2.0"
mavenPublish = "0.34.0"
binaryCompatibilityValidator = "0.16.3"
kover = "0.8.3"
coroutines = "1.9.0"
kotlinxSerialization = "1.7.3"
kotlinxCollectionsImmutable = "0.3.8"
composeMultiplatform = "1.7.3"
[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
androidApplication = { id = "com.android.application", version.ref = "agp" }
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" }
kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "kotlinxCollectionsImmutable" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }

Binary file not shown.

View File

@@ -1,6 +1,7 @@
#Wed Jan 08 12:28:15 CET 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip

309
gradlew vendored
View File

@@ -1,78 +1,127 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -81,92 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

56
gradlew.bat vendored
View File

@@ -1,4 +1,20 @@
@if "%DEBUG%" == "" @echo off
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -9,19 +25,23 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -35,7 +55,7 @@ goto fail
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
@@ -45,38 +65,26 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

1999
kotlin-js-store/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
repositories {
google()
mavenCentral()
}
kotlin {
jvmToolchain(21)
// Match the Kotlin bytecode target to the Java level in android.compileOptions below.
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
android {
namespace = "com.github.adriankuta.treestructure.sample"
compileSdk = libs.versions.androidCompileSdk.get().toInt()
defaultConfig {
applicationId = "com.github.adriankuta.treestructure.sample"
minSdk = libs.versions.androidMinSdk.get().toInt()
targetSdk = libs.versions.androidCompileSdk.get().toInt()
versionCode = 1
versionName = rootProject.version.toString()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
dependencies {
implementation(project(":"))
implementation(project(":tree-structure-compose"))
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.components.uiToolingPreview)
implementation(libs.androidx.activity.compose)
debugImplementation(compose.uiTooling)
}

View File

@@ -1,22 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.github.adriankuta.treedatastructure">
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity"
android:label="Tree Sample"
android:theme="@android:style/Theme.Material.Light.NoActionBar">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
</manifest>

View File

@@ -0,0 +1,73 @@
package com.github.adriankuta.treestructure.sample
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.github.adriankuta.datastructure.tree.TreeNode
import com.github.adriankuta.datastructure.tree.compose.LazyTree
import com.github.adriankuta.datastructure.tree.compose.TreeNodeRow
import com.github.adriankuta.datastructure.tree.tree
import org.jetbrains.compose.ui.tooling.preview.Preview
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
TreeSampleScreen()
}
}
}
}
}
/** Renders the [sampleTree] with the library's default [TreeNodeRow] via the one-line [LazyTree]. */
@Composable
fun TreeSampleScreen() {
LazyTree(sampleTree())
}
private fun sampleTree(): TreeNode<String> = tree("World") {
child("North America") {
child("USA")
child("Canada")
}
child("Europe") {
child("Poland")
child("Germany")
child("Spain")
}
child("Asia") {
child("Japan")
child("India")
}
}
@Preview
@Composable
private fun TreeSampleScreenPreview() {
MaterialTheme {
Surface {
TreeSampleScreen()
}
}
}
@Preview
@Composable
private fun TreeNodeRowPreview() {
MaterialTheme {
TreeNodeRow(
node = TreeNode("Europe").apply { addChild(TreeNode("Poland")) },
depth = 1,
expanded = true,
toggle = {},
)
}
}

35
samples/build.gradle.kts Normal file
View File

@@ -0,0 +1,35 @@
plugins {
// No version: the Kotlin Gradle plugin is already on the build classpath via the root
// project's kotlinMultiplatform plugin, so requesting a version here would clash.
kotlin("jvm")
application
}
kotlin {
jvmToolchain(21)
}
application {
mainClass.set("com.github.adriankuta.samples.SamplesKt")
}
repositories {
mavenCentral()
}
dependencies {
implementation(project(":"))
implementation(project(":tree-structure-serialization"))
implementation(project(":tree-structure-coroutines"))
implementation(project(":tree-structure-immutable"))
// ImmutableTreeNode.children returns a PersistentList, so consumers that touch it need
// kotlinx.collections.immutable on their own classpath (the module declares it as implementation).
implementation(libs.kotlinx.collections.immutable)
testImplementation(kotlin("test"))
}
// kotlin("test") auto-selects the JUnit 5 adapter when the test task uses the JUnit Platform.
tasks.test {
useJUnitPlatform()
}

View File

@@ -0,0 +1,121 @@
package com.github.adriankuta.samples
import com.github.adriankuta.datastructure.tree.TreeNode
import com.github.adriankuta.datastructure.tree.ancestors
import com.github.adriankuta.datastructure.tree.anyNode
import com.github.adriankuta.datastructure.tree.countNodes
import com.github.adriankuta.datastructure.tree.deepCopy
import com.github.adriankuta.datastructure.tree.distance
import com.github.adriankuta.datastructure.tree.filterNodes
import com.github.adriankuta.datastructure.tree.findNode
import com.github.adriankuta.datastructure.tree.isLeaf
import com.github.adriankuta.datastructure.tree.leaves
import com.github.adriankuta.datastructure.tree.levelOrderSequence
import com.github.adriankuta.datastructure.tree.lowestCommonAncestor
import com.github.adriankuta.datastructure.tree.mapValues
import com.github.adriankuta.datastructure.tree.pathBetween
import com.github.adriankuta.datastructure.tree.preOrderSequence
import com.github.adriankuta.datastructure.tree.structurallyEquals
import com.github.adriankuta.datastructure.tree.tree
import com.github.adriankuta.datastructure.tree.coroutines.asFlow
import com.github.adriankuta.datastructure.tree.coroutines.preOrderFlow
import com.github.adriankuta.datastructure.tree.immutable.ImmutableTreeNode
import com.github.adriankuta.datastructure.tree.immutable.preOrder
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import com.github.adriankuta.datastructure.tree.serialization.TreeNodeDto
import com.github.adriankuta.datastructure.tree.serialization.toDto
import com.github.adriankuta.datastructure.tree.serialization.toTreeNode
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
private fun sampleTree(): TreeNode<String> = tree("World") {
child("North America") {
child("USA")
}
child("Europe") {
child("Poland")
child("Germany")
}
}
/** Core API: DSL, pretty-print, traversal, navigation, functional, query, utilities, mutation. */
fun coreSample(): String = buildString {
val root = sampleTree()
appendLine("prettyString():")
append(root.prettyString())
appendLine()
appendLine("pre-order: " + root.preOrderSequence().map { it.value }.toList())
appendLine("level-order: " + root.levelOrderSequence().map { it.value }.toList())
val usa = root.findNode { it == "USA" }!!
val poland = root.findNode { it == "Poland" }!!
appendLine("usa.depth(): " + usa.depth())
appendLine("usa.ancestors(): " + usa.ancestors().map { it.value })
appendLine("root.leaves(): " + root.leaves().map { it.value })
appendLine("usa.isLeaf: " + usa.isLeaf)
appendLine("anyNode == Poland: " + root.anyNode { it == "Poland" })
appendLine("filterNodes len>5: " + root.filterNodes { it.length > 5 }.map { it.value })
appendLine("countNodes 'U*': " + root.countNodes { it.startsWith("U") })
appendLine("mapValues length: " + root.mapValues { it.length }.preOrderSequence().map { it.value }.toList())
appendLine("deepCopy equals: " + root.structurallyEquals(root.deepCopy()))
appendLine("lowestCommonAncestor(USA, Poland): " + usa.lowestCommonAncestor(poland)?.value)
appendLine("pathBetween(USA, Poland): " + usa.pathBetween(poland)?.map { it.value })
appendLine("distance(USA, Poland): " + usa.distance(poland))
appendLine("nodeCount(): " + root.nodeCount())
appendLine("height(): " + root.height())
appendLine("path(USA): " + root.path(usa)?.map { it.value })
// Mutation on a copy; the shared sampleTree() stays untouched.
val mutable = root.deepCopy()
mutable.addChild(TreeNode("Asia"))
mutable.findNode { it == "Germany" }?.detach()
appendLine("after addChild(Asia) + detach(Germany): " + mutable.preOrderSequence().map { it.value }.toList())
}
/** Serialization satellite: TreeNode -> TreeNodeDto -> JSON -> TreeNodeDto -> TreeNode round-trip. */
fun serializationSample(): String = buildString {
val root = sampleTree()
val json = Json.encodeToString(root.toDto())
appendLine("JSON: $json")
val restored = Json.decodeFromString<TreeNodeDto<String>>(json).toTreeNode()
appendLine("round-trips structurallyEquals: " + root.structurallyEquals(restored))
}
/** Coroutines satellite: traverse the tree as a cold Flow. */
fun coroutinesSample(): String = buildString {
val root = sampleTree()
val preOrder = runBlocking { root.preOrderFlow().map { it.value }.toList() }
val levelOrder = runBlocking { root.asFlow(TreeNodeIterators.LevelOrder).map { it.value }.toList() }
appendLine("preOrderFlow(): $preOrder")
appendLine("asFlow(LevelOrder): $levelOrder")
}
/** Immutable satellite: persistent tree; every op returns a new root, leaving the original intact. */
fun immutableSample(): String = buildString {
val root = ImmutableTreeNode("World").addChild(ImmutableTreeNode("Europe"))
val bigger = root.addChild(ImmutableTreeNode("Asia"))
appendLine("root.children: " + root.children.map { it.value })
appendLine("bigger.children: " + bigger.children.map { it.value })
appendLine("root unchanged: " + (root.children.size == 1))
appendLine("bigger.mapValues uppercase preOrder: " + bigger.mapValues { it.uppercase() }.preOrder().map { it.value })
}
fun main() {
println("== Core ==")
println(coreSample())
println("== Serialization ==")
println(serializationSample())
println("== Coroutines ==")
println(coroutinesSample())
println("== Immutable ==")
println(immutableSample())
}

View File

@@ -0,0 +1,46 @@
package com.github.adriankuta.samples
import kotlin.test.Test
import kotlin.test.assertContains
class SamplesTest {
@Test
fun coreSampleRendersTreeAndTraversals() {
val out = coreSample()
assertContains(
out,
"World\n" +
"├── North America\n" +
"│ └── USA\n" +
"└── Europe\n" +
" ├── Poland\n" +
" └── Germany\n",
)
assertContains(out, "[World, North America, USA, Europe, Poland, Germany]")
assertContains(out, "[North America, World]") // usa.ancestors()
assertContains(out, "[USA, Poland, Germany]") // root.leaves()
}
@Test
fun serializationSampleRoundTrips() {
val out = serializationSample()
assertContains(out, "\"World\"")
assertContains(out, "round-trips structurallyEquals: true")
}
@Test
fun coroutinesSampleCollectsFlows() {
val out = coroutinesSample()
assertContains(out, "preOrderFlow(): [World, North America, USA, Europe, Poland, Germany]")
assertContains(out, "asFlow(LevelOrder): [World, North America, Europe, USA, Poland, Germany]")
}
@Test
fun immutableSampleLeavesRootUnchanged() {
val out = immutableSample()
assertContains(out, "root.children: [Europe]")
assertContains(out, "bigger.children: [Europe, Asia]")
assertContains(out, "root unchanged: true")
}
}

View File

@@ -1,86 +0,0 @@
apply plugin: 'maven-publish'
apply plugin: 'signing'
apply plugin: 'org.jetbrains.dokka'
task androidSourcesJar(type: Jar) {
archiveClassifier.set('sources')
if (project.plugins.findPlugin("com.android.library")) {
from android.sourceSets.main.java.srcDirs
from android.sourceSets.main.kotlin.srcDirs
} else {
from sourceSets.main.java.srcDirs
from sourceSets.main.kotlin.srcDirs
}
}
tasks.withType(dokkaHtmlPartial.getClass()).configureEach {
pluginsMapConfiguration.set(
["org.jetbrains.dokka.base.DokkaBase": """{ "separateInheritedMembers": true}"""]
)
}
task javadocJar(type: Jar, dependsOn: dokkaJavadoc) {
archiveClassifier.set('javadoc')
from dokkaJavadoc.outputDirectory
}
artifacts {
archives androidSourcesJar
archives javadocJar
}
group = PUBLISH_GROUP_ID
version = PUBLISH_VERSION
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
groupId PUBLISH_GROUP_ID
artifactId PUBLISH_ARTIFACT_ID
version PUBLISH_VERSION
if (project.plugins.findPlugin("com.android.library")) {
from components.release
} else {
from components.java
}
artifact androidSourcesJar
artifact javadocJar
pom {
name = PUBLISH_ARTIFACT_ID
description = 'Simple implementation to store object in tree structure.'
url = 'https://github.com/AdrianKuta/Tree-Data-Structure'
licenses {
license {
name = 'MIT License'
url = 'https://www.mit.edu/~amini/LICENSE.md'
}
}
developers {
developer {
name = 'Adrian Kuta'
email = 'adrian.kuta93@gmail.com'
}
}
// Version control info, if you're using GitHub, follow the format as seen here
scm {
connection = 'scm:git:github.com/AdrianKuta/Tree-Data-Structure.git'
developerConnection = 'scm:git:ssh://github.com/AdrianKuta/Tree-Data-Structure.git'
url = 'https://github.com/AdrianKuta/Tree-Data-Structure/tree/master'
}
}
}
}
}
}
signing {
useInMemoryPgpKeys(
rootProject.ext["signing.keyId"],
rootProject.ext["signing.key"],
rootProject.ext["signing.password"],
)
sign publishing.publications
}

View File

@@ -1,43 +0,0 @@
// Create variables with empty default values
ext["ossrhUsername"] = ''
ext["ossrhPassword"] = ''
ext["sonatypeStagingProfileId"] = ''
ext["signing.keyId"] = ''
ext["signing.password"] = ''
ext["signing.key"] = ''
ext["snapshot"] = ''
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
// Read local.properties file first if it exists
Properties p = new Properties()
new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
p.each { name, value -> ext[name] = value }
} else {
// Use system environment variables
ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
ext["signing.key"] = System.getenv('SIGNING_KEY')
ext["snapshot"] = System.getenv('SNAPSHOT')
}
//if (snapshot) {
// ext["rootVersionName"] = Configuration.snapshotVersionName
//} else {
// ext["rootVersionName"] = Configuration.versionName
//}
// Set up Sonatype repository
nexusPublishing {
repositories {
sonatype {
stagingProfileId = sonatypeStagingProfileId
username = ossrhUsername
password = ossrhPassword
//version = rootVersionName
}
}
}

View File

@@ -1,2 +0,0 @@
include ':app', ':treedatastructure'
rootProject.name='Tree Data Structure'

16
settings.gradle.kts Normal file
View File

@@ -0,0 +1,16 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
rootProject.name = "tree-structure"
include(":tree-structure-serialization")
include(":tree-structure-coroutines")
include(":tree-structure-compose")
include(":tree-structure-immutable")
include(":samples")
include(":samples-android")

View File

@@ -1,6 +1,8 @@
package com.github.adriankuta.datastructure.tree
interface ChildDeclarationInterface<T> {
import kotlin.jvm.JvmSynthetic
public interface ChildDeclarationInterface<T> {
/**
* This method is used to easily create child in node.
@@ -18,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

@@ -0,0 +1,354 @@
package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.exceptions.TreeNodeException
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
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators.*
import kotlin.jvm.JvmSynthetic
/**
* 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.
*/
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.
*/
public val parent: TreeNode<T>?
get() = _parent
private val _children = mutableListOf<TreeNode<T>>()
/**
* A group of nodes with the same parent.
*/
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.
*/
public val isRoot: Boolean
get() = _parent == null
/**
* Adds [child] as a direct child of this 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).
*/
public fun addChild(child: TreeNode<T>) {
validateAttachable(child)
child._parent = this
_children.add(child)
}
/**
* Validates that [child] can be attached as a direct child of this node, throwing if it cannot.
*
* @param child the node about to be attached.
* @throws TreeNodeException if [child] already has a parent, or if attaching it here would create
* a cycle (i.e. [child] is this node or one of its ancestors).
*/
private fun validateAttachable(child: TreeNode<T>) {
if (child._parent != null) {
throw TreeNodeException("$child already has a parent; call detach() before re-attaching it.")
}
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
}
}
}
/**
* 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
public override fun child(value: T, childDeclaration: ChildDeclaration<T>?): TreeNode<T> {
val newChild = TreeNode(value)
newChild._parent = this
if (childDeclaration != null)
newChild.childDeclaration()
_children.add(newChild)
return newChild
}
/**
* Removes [child] from this node's direct [children], if present.
*
* 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.
*/
public fun removeChild(child: TreeNode<T>): Boolean {
val removed = _children.remove(child)
if (removed) child._parent = null
return removed
}
/**
* Inserts [child] as a direct child of this node at the given [index], shifting any existing
* children at and after [index] one position to the right.
*
* @param index the position at which to insert [child]; must be in `0..children.size`.
* @param child a node that is not already attached to a tree. To move a node that already has a
* parent, call [detach] on it first.
* @throws IndexOutOfBoundsException if [index] is out of range.
* @throws TreeNodeException if [child] already has a parent, or if attaching it here would create
* a cycle (i.e. [child] is this node or one of its ancestors).
*/
public fun insertChild(index: Int, child: TreeNode<T>) {
validateAttachable(child)
child._parent = this
_children.add(index, child)
}
/**
* Removes the direct child at the given [index], detaching it (its parent becomes `null`).
*
* @param index the position of the child to remove; must be in `0 until children.size`.
* @return the detached child that was at [index].
* @throws IndexOutOfBoundsException if [index] is out of range.
*/
public fun removeChildAt(index: Int): TreeNode<T> {
val removed = _children.removeAt(index)
removed._parent = null
return removed
}
/**
* Replaces the direct child at the given [index] with [child], detaching the previous child
* (its parent becomes `null`).
*
* @param index the position of the child to replace; must be in `0 until children.size`.
* @param child a node that is not already attached to a tree. To move a node that already has a
* parent, call [detach] on it first.
* @return the previous child that was at [index], now detached.
* @throws IndexOutOfBoundsException if [index] is out of range.
* @throws TreeNodeException if [child] already has a parent, or if attaching it here would create
* a cycle (i.e. [child] is this node or one of its ancestors).
*/
public fun replaceChild(index: Int, child: TreeNode<T>): TreeNode<T> {
validateAttachable(child)
val old = _children[index]
old._parent = null
child._parent = this
_children[index] = child
return old
}
/**
* Moves an existing direct [child] to a new position within this node's [children].
*
* [toIndex] is coerced into the valid range, so out-of-range targets clamp to the first or last
* position. Because [child] is already a direct child, no re-parenting or cycle check is needed.
*
* @param child the node to reorder; must already be a direct child of this node.
* @param toIndex the target position for [child] after removal, coerced into `0..children.size`.
* @return `true` if [child] was a direct child and has been moved; `false` otherwise.
*/
public fun moveChild(child: TreeNode<T>, toIndex: Int): Boolean {
val from = _children.indexOf(child)
if (from < 0) return false
_children.removeAt(from)
_children.add(toIndex.coerceIn(0, _children.size), child)
return true
}
/**
* Adds each of [children] as a direct child of this node, in order, validating each one the same
* way as [addChild].
*
* Validation is performed per node as it is added, so if one node fails the children added before
* it remain attached (the same partial-application behaviour as calling [addChild] in a loop).
*
* @param children nodes that are not already attached to a tree.
* @throws TreeNodeException if any node already has a parent, or if attaching it here would create
* a cycle (i.e. it is this node or one of its ancestors).
*/
public fun addChildren(vararg children: TreeNode<T>) {
for (child in children) {
addChild(child)
}
}
/**
* Sorts this node's direct [children] in place according to the given [comparator]. Only the
* immediate children are reordered; their subtrees are left untouched.
*
* @param comparator the comparator used to order the children.
*/
public fun sortChildren(comparator: Comparator<TreeNode<T>>) {
_children.sortWith(comparator)
}
/**
* This function go through tree and counts children. Root element is not counted.
* @return All child and nested child count.
*/
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.
*/
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.
*/
public fun depth(): Int {
var depth = 0
var tempParent = parent
while (tempParent != null) {
depth++
tempParent = tempParent.parent
}
return depth
}
/**
* 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 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.
*/
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
}
return null
}
/**
* 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.
*/
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()
}
public override fun toString(): String {
return value.toString()
}
public fun prettyString(): String {
val stringBuilder = StringBuilder()
print(stringBuilder, "", "")
return stringBuilder.toString()
}
private fun print(stringBuilder: StringBuilder, prefix: String, childrenPrefix: String) {
stringBuilder.append(prefix)
stringBuilder.append(value)
stringBuilder.append('\n')
val childIterator = _children.iterator()
while (childIterator.hasNext()) {
val node = childIterator.next()
if (childIterator.hasNext()) {
node.print(stringBuilder, "$childrenPrefix├── ", "$childrenPrefix")
} else {
node.print(stringBuilder, "$childrenPrefix└── ", "$childrenPrefix ")
}
}
}
/**
* 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.
*/
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

@@ -0,0 +1,25 @@
package com.github.adriankuta.datastructure.tree
import com.github.adriankuta.datastructure.tree.iterators.TreeNodeIterators
import kotlin.jvm.JvmSynthetic
public typealias ChildDeclaration<T> = ChildDeclarationInterface<T>.() -> Unit
/**
* This method can be used to initialize new tree.
* ```
* val root = tree("World") { ... }
* ```
* @param root Root element of new tree.
* @see [ChildDeclarationInterface.child]
*/
@JvmSynthetic
public inline fun <reified T> tree(
root: T,
defaultIterator: TreeNodeIterators = TreeNodeIterators.PreOrder,
childDeclaration: ChildDeclaration<T>
): TreeNode<T> {
val treeNode = TreeNode(root, defaultIterator)
treeNode.childDeclaration()
return treeNode
}

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,106 @@
package com.github.adriankuta.datastructure.tree
/**
* The four glyph strings used to draw the tree branches in [prettyString].
*
* Each value is the literal text emitted at the matching position:
* - [branch] precedes a child that is **not** its parent's last child.
* - [lastBranch] precedes a child that **is** its parent's last child.
* - [vertical] is accumulated into the prefix of the descendants of a non-last child (it keeps the
* vertical guide line going).
* - [empty] is accumulated into the prefix of the descendants of a last child (no guide line is
* needed past the last branch).
*
* Use [Default] for the box-drawing style or [Ascii] for a plain-ASCII style, or supply your own.
*
* @property branch drawn before a non-last child.
* @property vertical continuation prefix for descendants of a non-last child.
* @property lastBranch drawn before the last child.
* @property empty continuation prefix for descendants of a last child.
*/
public data class TreeConnectors(
public val branch: String,
public val vertical: String,
public val lastBranch: String,
public val empty: String,
) {
public companion object {
/** Box-drawing connectors, matching the output of the no-arg [TreeNode.prettyString]. */
public val Default: TreeConnectors = TreeConnectors(
branch = "├── ",
vertical = "",
lastBranch = "└── ",
empty = " ",
)
/** Plain-ASCII connectors for terminals or fonts that lack box-drawing glyphs. */
public val Ascii: TreeConnectors = TreeConnectors(
branch = "|-- ",
vertical = "| ",
lastBranch = "`-- ",
empty = " ",
)
}
}
/**
* Renders this subtree as a multi-line string, one node per line, with branch connectors.
*
* Calling this with all defaults produces output byte-identical to the no-arg member
* [TreeNode.prettyString]. Customise the drawing with [connectors] (e.g. [TreeConnectors.Ascii]) and
* the per-node text with [render].
*
* @param connectors the glyph set used to draw the branches. Defaults to [TreeConnectors.Default].
* @param render produces the text for each node from its `value`, its `depth` (distance from this
* receiver, which is `0`) and `isLast` (whether the node is its parent's last child; the root is
* considered `true`). Defaults to the value's string form (`"$value"`), which renders a `null`
* value as `"null"` to match the no-arg member.
* @return the rendered tree, each line terminated by `\n`.
*/
public fun <T> TreeNode<T>.prettyString(
connectors: TreeConnectors = TreeConnectors.Default,
render: (value: T, depth: Int, isLast: Boolean) -> String = { value, _, _ -> "$value" },
): String {
val stringBuilder = StringBuilder()
appendPretty(stringBuilder, "", "", 0, true, connectors, render)
return stringBuilder.toString()
}
private fun <T> TreeNode<T>.appendPretty(
stringBuilder: StringBuilder,
prefix: String,
childrenPrefix: String,
depth: Int,
isLast: Boolean,
connectors: TreeConnectors,
render: (value: T, depth: Int, isLast: Boolean) -> String,
) {
stringBuilder.append(prefix)
stringBuilder.append(render(value, depth, isLast))
stringBuilder.append('\n')
val childIterator = children.iterator()
while (childIterator.hasNext()) {
val node = childIterator.next()
if (childIterator.hasNext()) {
node.appendPretty(
stringBuilder,
childrenPrefix + connectors.branch,
childrenPrefix + connectors.vertical,
depth + 1,
false,
connectors,
render,
)
} else {
node.appendPretty(
stringBuilder,
childrenPrefix + connectors.lastBranch,
childrenPrefix + connectors.empty,
depth + 1,
true,
connectors,
render,
)
}
}
}

View File

@@ -0,0 +1,88 @@
package com.github.adriankuta.datastructure.tree
/**
* The lowest (deepest) node that is an ancestor of both this node and [other], where every node is
* considered an ancestor of itself.
*
* Nodes are compared by identity (`===`), so this only returns a node when both arguments live in
* the same tree.
*
* @param other the other node to find the common ancestor with.
* @return the lowest common ancestor, or `null` when the two nodes belong to different trees and
* therefore share no common ancestor.
*
* Runs in `O(da + db)` time and `O(da + db)` space, where `da`/`db` are the depths of the two nodes.
*/
public fun <T> TreeNode<T>.lowestCommonAncestor(other: TreeNode<T>): TreeNode<T>? {
// TreeNode has identity equality, so a HashSet gives O(1) identity membership and keeps the
// overall walk at O(da + db). Collect [other] and its ancestors, then climb from this node
// upward; the first node already on [other]'s chain is the deepest common ancestor.
val ancestorsOfOther = HashSet<TreeNode<T>>(other.ancestors())
ancestorsOfOther.add(other)
var node: TreeNode<T>? = this
while (node != null) {
if (node in ancestorsOfOther) return node
node = node.parent
}
return null
}
/**
* The number of edges on the shortest path between this node and [other].
*
* Computed as `depth() + other.depth() - 2 * lca.depth()`, where `lca` is their
* [lowestCommonAncestor]. The distance from a node to itself is `0`.
*
* @param other the other node to measure the distance to.
* @return the edge count, or `null` when the two nodes belong to different trees.
*
* Runs in `O(da + db)` time, where `da`/`db` are the depths of the two nodes.
*/
public fun <T> TreeNode<T>.distance(other: TreeNode<T>): Int? {
val lca = lowestCommonAncestor(other) ?: return null
return depth() + other.depth() - 2 * lca.depth()
}
/**
* The shortest path of nodes from this node to [other], inclusive of both endpoints.
*
* The path ascends from this node up to their [lowestCommonAncestor] and then descends to [other];
* the common ancestor appears exactly once. When `this === other` the result is `listOf(this)`. When
* one node is an ancestor of the other the path is simply the chain between them.
*
* @param other the node the path ends at.
* @return the path `[this, …, lca, …, other]`, or `null` when the two nodes belong to different
* trees.
*
* Runs in `O(da + db)` time and space, where `da`/`db` are the depths of the two nodes.
*/
public fun <T> TreeNode<T>.pathBetween(other: TreeNode<T>): List<TreeNode<T>>? {
val lca = lowestCommonAncestor(other) ?: return null
val up = mutableListOf<TreeNode<T>>()
var node: TreeNode<T> = this
up.add(node)
while (node !== lca) {
node = node.parent!!
up.add(node)
}
val down = mutableListOf<TreeNode<T>>()
node = other
down.add(node)
while (node !== lca) {
node = node.parent!!
down.add(node)
}
return up + down.dropLast(1).reversed()
}
/**
* Returns `true` when this subtree contains a node whose value equals [value], including the
* receiver itself. Values are compared with `==` ([equals]).
*
* @param value the value to search for.
* @return `true` if any node in the pre-order traversal of this subtree holds [value].
*
* Runs in `O(n)` time over the `n` nodes of this subtree and stops at the first match.
*/
public fun <T> TreeNode<T>.contains(value: T): Boolean =
preOrderSequence().any { it.value == value }

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

@@ -0,0 +1,6 @@
package com.github.adriankuta.datastructure.tree.exceptions
import kotlin.jvm.JvmOverloads
public class TreeNodeException @JvmOverloads constructor(message: String? = null, cause: Throwable? = null) :
RuntimeException(message, cause)

View File

@@ -0,0 +1,39 @@
package com.github.adriankuta.datastructure.tree.iterators
import com.github.adriankuta.datastructure.tree.TreeNode
/**
* Tree is iterated by using `Level-order Traversal Algorithm"
* In level-order traversal we iterating nodes level by level,
* starting from root, and going deeper and deeper in tree.
* ```
* E.g.
* 1
* / | \
* / | \
* 2 3 4
* / \ / | \
* 5 6 7 8 9
* / / | \
* 10 11 12 13
*
* Output: 1 2 3 4 5 6 7 8 9 10 11 12 13
* ```
*/
public class LevelOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
private val stack = ArrayDeque<TreeNode<T>>()
init {
stack.addLast(root)
}
public override fun hasNext(): Boolean = stack.isNotEmpty()
public override fun next(): TreeNode<T> {
val node = stack.removeFirst()
node.children
.forEach { stack.addLast(it) }
return node
}
}

View File

@@ -0,0 +1,43 @@
package com.github.adriankuta.datastructure.tree.iterators
import com.github.adriankuta.datastructure.tree.TreeNode
/**
* Tree is iterated by using `Post-order Traversal Algorithm"
* In post-order traversal, we starting from most left child.
* First visit all children of parent, then parent.
* ```
* E.g.
* 1
* / | \
* / | \
* 2 3 4
* / \ / | \
* 5 6 7 8 9
* / / | \
* 10 11 12 13
*
* Output: 10 5 11 12 13 6 2 3 7 8 9 4 1
* ```
*/
public class PostOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
private val result = ArrayDeque<TreeNode<T>>()
init {
// 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>>()
stack.addLast(root)
while (stack.isNotEmpty()) {
val node = stack.removeLast()
result.addFirst(node)
node.children.forEach { stack.addLast(it) }
}
}
public override fun hasNext(): Boolean = result.isNotEmpty()
public override fun next(): TreeNode<T> = result.removeFirst()
}

View File

@@ -0,0 +1,40 @@
package com.github.adriankuta.datastructure.tree.iterators
import com.github.adriankuta.datastructure.tree.TreeNode
/**
* Tree is iterated by using `Pre-order Traversal Algorithm"
* The pre-order traversal is a topologically sorted one,
* because a parent node is processed before any of its child nodes is done.
* ```
* E.g.
* 1
* / | \
* / | \
* 2 3 4
* / \ / | \
* 5 6 7 8 9
* / / | \
* 10 11 12 13
*
* Output: 1 2 5 10 6 11 12 13 3 4 7 8 9
* ```
*/
public class PreOrderTreeIterator<T>(root: TreeNode<T>) : Iterator<TreeNode<T>> {
private val stack = ArrayDeque<TreeNode<T>>()
init {
stack.addLast(root)
}
public override fun hasNext(): Boolean = stack.isNotEmpty()
public override fun next(): TreeNode<T> {
val node = stack.removeLast()
node.children
.asReversed()
.forEach { stack.addLast(it) }
return node
}
}

View File

@@ -0,0 +1,68 @@
package com.github.adriankuta.datastructure.tree.iterators
/**
* @see PreOrder
* @see PostOrder
* @see LevelOrder
*/
public enum class TreeNodeIterators {
/**
* Tree is iterated by using `Pre-order Traversal Algorithm"
* The pre-order traversal is a topologically sorted one,
* because a parent node is processed before any of its child nodes is done.
* ```
* E.g.
* 1
* / | \
* / | \
* 2 3 4
* / \ / | \
* 5 6 7 8 9
* / / | \
* 10 11 12 13
*
* Output: 1 2 5 10 6 11 12 13 3 4 7 8 9
* ```
*/
PreOrder,
/**
* Tree is iterated by using `Post-order Traversal Algorithm"
* In post-order traversal, we starting from most left child.
* First visit all children of parent, then parent.
* ```
* E.g.
* 1
* / | \
* / | \
* 2 3 4
* / \ / | \
* 5 6 7 8 9
* / / | \
* 10 11 12 13
*
* Output: 10 5 11 12 13 6 2 3 7 8 9 4 1
* ```
*/
PostOrder,
/**
* Tree is iterated by using `Level-order Traversal Algorithm"
* In level-order traversal we iterating nodes level by level,
* starting from root, and going deeper and deeper in tree.
* ```
* E.g.
* 1
* / | \
* / | \
* 2 3 4
* / \ / | \
* 5 6 7 8 9
* / / | \
* 10 11 12 13
*
* Output: 1 2 3 4 5 6 7 8 9 10 11 12 13
* ```
*/
LevelOrder
}

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

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,100 @@
package com.github.adriankuta.datastructure.tree
import kotlin.test.Test
import kotlin.test.assertEquals
class TreeNodePrettyPrintTest {
private fun sampleTree(): TreeNode<String> {
val root = TreeNode("Root")
val beverages = TreeNode("Beverages")
val curd = TreeNode("Curd")
root.addChild(beverages)
root.addChild(curd)
val tea = TreeNode("tea")
val coffee = TreeNode("coffee")
beverages.addChild(tea)
beverages.addChild(coffee)
tea.addChild(TreeNode("ginger tea"))
tea.addChild(TreeNode("normal tea"))
curd.addChild(TreeNode("yogurt"))
curd.addChild(TreeNode("lassi"))
return root
}
@Test
fun defaultConnectorsMatchMemberPrettyString() {
val root = sampleTree()
assertEquals(root.prettyString(), root.prettyString(connectors = TreeConnectors.Default))
}
@Test
fun defaultRenderMatchesMemberForNullValues() {
// The member prettyString() appends the value via StringBuilder, rendering null as "null".
// The all-defaults extension must stay byte-identical, including for null-valued nodes.
val root = TreeNode<String?>(null)
root.addChild(TreeNode("child"))
root.addChild(TreeNode<String?>(null))
assertEquals(root.prettyString(), root.prettyString(connectors = TreeConnectors.Default))
assertEquals(
"null\n" +
"├── child\n" +
"└── null\n",
root.prettyString(),
)
}
@Test
fun asciiConnectorsRenderPlainAscii() {
val root = sampleTree()
assertEquals(
"Root\n" +
"|-- Beverages\n" +
"| |-- tea\n" +
"| | |-- ginger tea\n" +
"| | `-- normal tea\n" +
"| `-- coffee\n" +
"`-- Curd\n" +
" |-- yogurt\n" +
" `-- lassi\n",
root.prettyString(connectors = TreeConnectors.Ascii),
)
}
@Test
fun customRenderIsApplied() {
val root = sampleTree()
assertEquals(
"ROOT\n" +
"├── BEVERAGES\n" +
"│ ├── TEA\n" +
"│ │ ├── GINGER TEA\n" +
"│ │ └── NORMAL TEA\n" +
"│ └── COFFEE\n" +
"└── CURD\n" +
" ├── YOGURT\n" +
" └── LASSI\n",
root.prettyString { value, _, _ -> value.uppercase() },
)
}
@Test
fun depthAndIsLastArePassedToRender() {
val root = sampleTree()
assertEquals(
"Root depth=0 last=true\n" +
"├── Beverages depth=1 last=false\n" +
"│ ├── tea depth=2 last=false\n" +
"│ │ ├── ginger tea depth=3 last=false\n" +
"│ │ └── normal tea depth=3 last=true\n" +
"│ └── coffee depth=2 last=true\n" +
"└── Curd depth=1 last=true\n" +
" ├── yogurt depth=2 last=false\n" +
" └── lassi depth=2 last=true\n",
root.prettyString { value, depth, isLast -> "$value depth=$depth last=$isLast" },
)
}
}

View File

@@ -0,0 +1,455 @@
package com.github.adriankuta.datastructure.tree
import kotlin.random.Random
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
/**
* Property-based tests for traversal and structural invariants (issue #38).
*
* Instead of a handful of hand-written example trees, each property is checked against many
* randomly generated trees. Generation is seeded ([BASE_SEED] + iteration index), so a failing case
* is fully reproducible: rerun [randomTree] with the seed printed in the failure message. No
* external dependency is used, so these run on every Kotlin target (JVM/JS/Wasm/Native).
*/
class TreeNodePropertyTest {
// -----------------------------------------------------------------------------------------
// Traversal node-set invariants
// -----------------------------------------------------------------------------------------
@Test
fun allThreeOrdersVisitTheSameSetOfNodes() = forEachRandomTree { tree, seed ->
val pre = tree.preOrderSequence().toList()
val post = tree.postOrderSequence().toList()
val level = tree.levelOrderSequence().toList()
assertEquals(pre.toSet(), post.toSet(), "pre vs post node set (seed=$seed)")
assertEquals(pre.toSet(), level.toSet(), "pre vs level node set (seed=$seed)")
}
@Test
fun allThreeOrdersHaveTheSameCardinalityAndNoDuplicates() = forEachRandomTree { tree, seed ->
val pre = tree.preOrderSequence().toList()
val post = tree.postOrderSequence().toList()
val level = tree.levelOrderSequence().toList()
val expectedSize = tree.nodeCount() + 1 // traversal includes the root; nodeCount excludes it
assertEquals(expectedSize, pre.size, "pre-order size (seed=$seed)")
assertEquals(expectedSize, post.size, "post-order size (seed=$seed)")
assertEquals(expectedSize, level.size, "level-order size (seed=$seed)")
assertEquals(pre.size, pre.toSet().size, "pre-order visits no node twice (seed=$seed)")
assertEquals(post.size, post.toSet().size, "post-order visits no node twice (seed=$seed)")
assertEquals(level.size, level.toSet().size, "level-order visits no node twice (seed=$seed)")
}
// -----------------------------------------------------------------------------------------
// Per-order ordering invariants
// -----------------------------------------------------------------------------------------
@Test
fun preOrderEmitsEverySubtreeAsAContiguousBlockAfterItsRoot() = forEachRandomTree { tree, seed ->
val pre = tree.preOrderSequence().toList()
val index = pre.indexMap()
for (node in pre) {
val start = index.getValue(node) + 1
val block = pre.subList(start, start + node.nodeCount())
assertEquals(node.descendants().toSet(), block.toSet(), "pre-order subtree of $node (seed=$seed)")
}
}
@Test
fun postOrderEmitsEverySubtreeAsAContiguousBlockBeforeItsRoot() = forEachRandomTree { tree, seed ->
val post = tree.postOrderSequence().toList()
val index = post.indexMap()
for (node in post) {
val end = index.getValue(node)
val block = post.subList(end - node.nodeCount(), end)
assertEquals(node.descendants().toSet(), block.toSet(), "post-order subtree of $node (seed=$seed)")
}
}
@Test
fun levelOrderVisitsNodesInNonDecreasingDepth() = forEachRandomTree { tree, seed ->
val depths = tree.levelOrderSequence().map { it.depth() }.toList()
for (i in 1 until depths.size) {
assertTrue(depths[i - 1] <= depths[i], "level-order depth not monotonic at $i (seed=$seed)")
}
}
@Test
fun preAndLevelOrderVisitEveryParentBeforeItsChildren() = forEachRandomTree { tree, seed ->
for (order in listOf(tree.preOrderSequence(), tree.levelOrderSequence())) {
val index = order.toList().indexMap()
for (node in index.keys) {
for (child in node.children) {
assertTrue(index.getValue(node) < index.getValue(child), "parent before child (seed=$seed)")
}
}
}
}
@Test
fun postOrderVisitsEveryChildBeforeItsParent() = forEachRandomTree { tree, seed ->
val index = tree.postOrderSequence().toList().indexMap()
for (node in index.keys) {
for (child in node.children) {
assertTrue(index.getValue(child) < index.getValue(node), "child before parent (seed=$seed)")
}
}
}
// -----------------------------------------------------------------------------------------
// depth / height invariants
// -----------------------------------------------------------------------------------------
@Test
fun depthMatchesAnIndependentBfsLevelAndEveryChildIsOneDeeper() = forEachRandomTree { tree, seed ->
assertEquals(0, tree.depth(), "root depth (seed=$seed)")
// Independent oracle: derive each node's level by walking DOWN through children (BFS), then
// cross-check against depth(), which walks UP through parent pointers. A bug in the parent
// walk cannot corrupt both derivations identically.
val levelByNode = HashMap<TreeNode<Int>, Int>()
levelByNode[tree] = 0
val queue = ArrayDeque<TreeNode<Int>>()
queue.add(tree)
while (queue.isNotEmpty()) {
val node = queue.removeFirst()
for (child in node.children) {
levelByNode[child] = levelByNode.getValue(node) + 1
queue.add(child)
}
}
for (node in tree) {
assertEquals(levelByNode.getValue(node), node.depth(), "depth matches BFS level (seed=$seed)")
for (child in node.children) {
assertEquals(node.depth() + 1, child.depth(), "child depth (seed=$seed)")
}
}
}
@Test
fun heightEqualsDeepestDescendantDistanceAndLeavesHaveHeightZero() = forEachRandomTree { tree, seed ->
for (node in tree) {
val expected = node.asSequence().maxOf { it.depth() } - node.depth()
assertEquals(expected, node.height(), "height of $node (seed=$seed)")
if (node.isLeaf) assertEquals(0, node.height(), "leaf height (seed=$seed)")
}
}
// -----------------------------------------------------------------------------------------
// nodeCount invariants
// -----------------------------------------------------------------------------------------
@Test
fun nodeCountEqualsDescendantCountForEveryNode() = forEachRandomTree { tree, seed ->
for (node in tree) {
// Independent oracle: nodeCount() walks an explicit stack; the pre-order sequence is a
// separate traversal, so agreeing pins the count without circularity.
assertEquals(node.asSequence().count() - 1, node.nodeCount(), "nodeCount vs traversal (seed=$seed)")
// Recursive self-consistency: a node's count is the sum of (each child + its subtree).
assertEquals(node.children.sumOf { it.nodeCount() + 1 }, node.nodeCount(), "nodeCount recursive sum (seed=$seed)")
}
}
@Test
fun nodeCountAndTraversalStayConsistentAcrossAttachAndDetach() = forEachRandomTree { tree, seed ->
// A second, independent tree we graft onto a random node of `tree`, then remove again.
val grafted = randomTree(Random(seed * 31 + 7), maxNodes = 20)
val host = tree.toList().random(Random(seed xor SELECT_SALT))
val countBefore = tree.nodeCount()
val graftSize = grafted.nodeCount() + 1 // the grafted root plus its descendants
host.addChild(grafted)
assertEquals(countBefore + graftSize, tree.nodeCount(), "nodeCount after addChild (seed=$seed)")
assertTrue(tree.toSet().containsAll(grafted.toList()), "grafted nodes now reachable (seed=$seed)")
assertSameNode(host, grafted.parent, "graft re-parented (seed=$seed)")
assertTrue(grafted.detach(), "detach returns true (seed=$seed)")
assertEquals(countBefore, tree.nodeCount(), "nodeCount restored after detach (seed=$seed)")
assertTrue(grafted.isRoot, "grafted is a root again (seed=$seed)")
assertFalse(tree.toSet().contains(grafted), "grafted no longer reachable (seed=$seed)")
}
@Test
fun removeAndReinsertKeepNodeCountAndPointersConsistent() = forEachRandomTree { tree, seed ->
val rnd = Random(seed xor 0x55AA_55AAL)
val parents = tree.toList().filter { it.children.isNotEmpty() }
if (parents.isEmpty()) return@forEachRandomTree // single-node tree: nothing to remove
val parent = parents.random(rnd)
val countBefore = tree.nodeCount()
val index = rnd.nextInt(parent.children.size)
val subtreeSize = parent.children[index].nodeCount() + 1
val removed = parent.removeChildAt(index)
assertTrue(removed.isRoot, "removeChildAt detaches the child (seed=$seed)")
assertEquals(countBefore - subtreeSize, tree.nodeCount(), "nodeCount drops by the subtree (seed=$seed)")
assertFalse(tree.toSet().contains(removed), "removed subtree no longer reachable (seed=$seed)")
val insertAt = rnd.nextInt(parent.children.size + 1)
parent.insertChild(insertAt, removed)
assertEquals(countBefore, tree.nodeCount(), "nodeCount restored after insertChild (seed=$seed)")
assertSameNode(parent, removed.parent, "re-inserted subtree is re-parented (seed=$seed)")
assertSameNode(removed, parent.children[insertAt], "re-inserted at the requested index (seed=$seed)")
for (node in tree) {
for (child in node.children) {
assertSameNode(node, child.parent, "parent pointers stay consistent (seed=$seed)")
}
}
}
@Test
fun clearRemovesEveryDescendantAndKeepsTheNodeAttached() = forEachRandomTree { tree, seed ->
val node = tree.toList().random(Random(seed xor SELECT_SALT))
val parentBefore = node.parent
node.clear()
assertEquals(0, node.nodeCount(), "nodeCount after clear (seed=$seed)")
assertTrue(node.children.isEmpty(), "children empty after clear (seed=$seed)")
assertSameNode(parentBefore, node.parent, "node keeps its own parent after clear (seed=$seed)")
}
// -----------------------------------------------------------------------------------------
// Structural / parent-pointer invariants
// -----------------------------------------------------------------------------------------
@Test
fun parentAndChildPointersAreConsistentForEveryNode() = forEachRandomTree { tree, seed ->
assertTrue(tree.isRoot, "generated root is a root (seed=$seed)")
for (node in tree) {
assertEquals(node.parent == null, node.isRoot, "isRoot matches null parent (seed=$seed)")
assertSameNode(tree, node.root(), "root() returns the tree root (seed=$seed)")
for (child in node.children) {
assertSameNode(node, child.parent, "child points back to parent (seed=$seed)")
}
val parent = node.parent
if (parent != null) {
assertTrue(parent.children.any { it === node }, "node listed in its parent (seed=$seed)")
// Note: TreeNode is Iterable, so `list + node` would flatten the node's subtree —
// compare siblings against the parent's other children directly instead.
assertEquals(
parent.children.filter { it !== node }.toSet(),
node.siblings().toSet(),
"siblings are exactly the parent's other children (seed=$seed)",
)
assertFalse(node.siblings().any { it === node }, "siblings exclude self (seed=$seed)")
}
}
}
@Test
fun ancestorChainOfEveryNodeTerminatesAtTheRoot() = forEachRandomTree { tree, seed ->
for (node in tree) {
val ancestors = node.ancestors()
assertEquals(node.depth(), ancestors.size, "ancestor count equals depth (seed=$seed)")
if (ancestors.isNotEmpty()) {
assertSameNode(tree, ancestors.last(), "topmost ancestor is the root (seed=$seed)")
}
ancestors.forEach { assertTrue(it.depth() < node.depth(), "ancestors are shallower (seed=$seed)") }
}
}
@Test
fun leavesAreExactlyTheChildlessNodes() = forEachRandomTree { tree, seed ->
assertEquals(tree.asSequence().filter { it.isLeaf }.toSet(), tree.leaves().toSet(), "leaves (seed=$seed)")
assertTrue(tree.leaves().all { it.isLeaf }, "every leaf is childless (seed=$seed)")
assertEquals(tree.toList().size, tree.descendants().size + 1, "descendants + self (seed=$seed)")
}
// -----------------------------------------------------------------------------------------
// Transform / structural-equality invariants
// -----------------------------------------------------------------------------------------
@Test
fun deepCopyAndIdentityMapPreserveShapeWithFreshNodes() = forEachRandomTree { tree, seed ->
assertTrue(tree.structurallyEquals(tree), "structurallyEquals is reflexive (seed=$seed)")
val copy = tree.deepCopy()
assertTrue(copy.structurallyEquals(tree), "deepCopy is structurally equal (seed=$seed)")
assertEquals(tree.nodeCount(), copy.nodeCount(), "deepCopy node count (seed=$seed)")
assertEquals(tree.height(), copy.height(), "deepCopy height (seed=$seed)")
assertTrue(copy.toSet().intersect(tree.toSet()).isEmpty(), "deepCopy shares no node object (seed=$seed)")
val mapped = tree.mapValues { it }
assertTrue(mapped.structurallyEquals(tree), "identity mapValues preserves structure (seed=$seed)")
assertTrue(mapped.toSet().intersect(tree.toSet()).isEmpty(), "mapValues yields fresh nodes (seed=$seed)")
}
// -----------------------------------------------------------------------------------------
// Functional / value-query invariants
// -----------------------------------------------------------------------------------------
@Test
fun valueQueriesAgreeWithTraversalOverUniqueValues() = forEachRandomTree { tree, seed ->
val values = tree.asSequence().map { it.value }.toList()
assertEquals(values.size, values.toSet().size, "generated values are unique (seed=$seed)")
assertEquals(values.size, tree.countNodes { true }, "countNodes(true) == size (seed=$seed)")
for (value in values) {
assertTrue(tree.contains(value), "contains present value $value (seed=$seed)")
assertNotNull(tree.findNode { it == value }, "findNode present value $value (seed=$seed)")
}
val absent = values.max() + 1 // values are unique and dense from 0, so this one is absent
assertFalse(tree.contains(absent), "absent value not contained (seed=$seed)")
}
// -----------------------------------------------------------------------------------------
// Query algorithms (lca / distance / pathBetween)
// -----------------------------------------------------------------------------------------
@Test
fun lowestCommonAncestorIsTheDeepestSharedAncestor() = forEachRandomTree { tree, seed ->
val nodes = tree.toList()
val rnd = Random(seed xor 0x1234_5678L)
repeat(PAIRS_PER_TREE) {
val a = nodes.random(rnd)
val b = nodes.random(rnd)
val lca = a.lowestCommonAncestor(b)
assertNotNull(lca, "lca within one tree is non-null (seed=$seed)")
assertSameNode(lca, b.lowestCommonAncestor(a), "lca is symmetric (seed=$seed)")
val ancestorsAndSelfA = (listOf(a) + a.ancestors()).toSet()
val ancestorsAndSelfB = (listOf(b) + b.ancestors()).toSet()
assertTrue(lca in ancestorsAndSelfA, "lca is an ancestor-or-self of a (seed=$seed)")
assertTrue(lca in ancestorsAndSelfB, "lca is an ancestor-or-self of b (seed=$seed)")
// Common ancestors form a chain, so the deepest one is unique; it must be the lca itself.
val deepestShared = ancestorsAndSelfA.intersect(ancestorsAndSelfB).maxByOrNull { it.depth() }
assertSameNode(deepestShared, lca, "lca is the deepest shared ancestor (seed=$seed)")
}
}
@Test
fun distanceAndPathBetweenAreConsistent() = forEachRandomTree { tree, seed ->
val nodes = tree.toList()
val rnd = Random(seed xor 0x0F0F_0F0FL)
repeat(PAIRS_PER_TREE) {
val a = nodes.random(rnd)
val b = nodes.random(rnd)
val distance = a.distance(b)
assertNotNull(distance, "distance within one tree is non-null (seed=$seed)")
assertTrue(distance >= 0, "distance is non-negative (seed=$seed)")
assertEquals(distance, b.distance(a), "distance is symmetric (seed=$seed)")
val path = a.pathBetween(b)
assertNotNull(path, "path within one tree is non-null (seed=$seed)")
assertSameNode(a, path.first(), "path starts at a (seed=$seed)")
assertSameNode(b, path.last(), "path ends at b (seed=$seed)")
assertEquals(distance, path.size - 1, "distance == path edges (seed=$seed)")
assertEquals(path.size, path.toSet().size, "path has no repeated node (seed=$seed)")
for (i in 1 until path.size) {
val (p, q) = path[i - 1] to path[i]
assertTrue(p.parent === q || q.parent === p, "consecutive path nodes are an edge (seed=$seed)")
}
}
}
@Test
fun distanceAndPathToSelfAreTrivial() = forEachRandomTree { tree, seed ->
val node = tree.toList().random(Random(seed xor SELECT_SALT))
assertEquals(0, node.distance(node), "distance to self is 0 (seed=$seed)")
assertSameNode(node, node.lowestCommonAncestor(node), "lca with self is self (seed=$seed)")
assertEquals(listOf(node), node.pathBetween(node), "path to self is the singleton (seed=$seed)")
}
// -----------------------------------------------------------------------------------------
// Termination and ordering on degenerate (deep / wide) trees
// -----------------------------------------------------------------------------------------
@Test
fun everyTraversalTerminatesAndIsCorrectlyOrderedOnADeepChain() {
val depth = 5_000
val root = TreeNode(0)
var current = root
for (i in 1..depth) {
val child = TreeNode(i)
current.addChild(child)
current = child
}
// On a chain pre- and level-order descend the chain; post-order returns it leaf-first. These
// pin the actual ordering, not merely that every order visits the same number of nodes.
assertContentEquals((0..depth).toList(), root.preOrderSequence().map { it.value }.toList(), "pre-order of a chain")
assertContentEquals((0..depth).toList(), root.levelOrderSequence().map { it.value }.toList(), "level-order of a chain")
assertContentEquals((depth downTo 0).toList(), root.postOrderSequence().map { it.value }.toList(), "post-order of a chain")
assertEquals(depth, root.height(), "height on deep chain")
assertEquals(depth, root.nodeCount(), "nodeCount on deep chain")
assertEquals(depth, current.depth(), "depth of the deepest node")
}
@Test
fun everyTraversalTerminatesAndIsCorrectlyOrderedOnAWideTree() {
val width = 5_000
val root = TreeNode(0)
for (i in 1..width) root.addChild(TreeNode(i))
// pre- and level-order list the root then its children in order; post-order lists the
// children in order then the root.
assertContentEquals(listOf(0) + (1..width), root.preOrderSequence().map { it.value }.toList(), "pre-order of a star")
assertContentEquals(listOf(0) + (1..width), root.levelOrderSequence().map { it.value }.toList(), "level-order of a star")
assertContentEquals((1..width).toList() + 0, root.postOrderSequence().map { it.value }.toList(), "post-order of a star")
assertEquals(1, root.height(), "height on wide tree")
assertEquals(width, root.nodeCount(), "nodeCount on wide tree")
assertTrue(root.children.all { it.depth() == 1 }, "every child of a wide root is at depth 1")
}
}
// ---------------------------------------------------------------------------------------------
// Random tree generation + property harness
// ---------------------------------------------------------------------------------------------
private const val ITERATIONS = 200
private const val BASE_SEED = 0x5EEDL
/** How many random node pairs each query property samples per generated tree. */
private const val PAIRS_PER_TREE = 8
/** Decorrelates node-selection RNGs from the tree-construction RNG that shares the same seed. */
private const val SELECT_SALT = 0x2545_F491_4F6C_DD1DL
/** Runs [property] against [iterations] freshly generated random trees, one per derived seed. */
private fun forEachRandomTree(
iterations: Int = ITERATIONS,
maxNodes: Int = 80,
property: (tree: TreeNode<Int>, seed: Long) -> Unit,
) {
for (i in 0 until iterations) {
val seed = BASE_SEED + i
property(randomTree(Random(seed), maxNodes), seed)
}
}
/**
* Builds a random tree of `1..[maxNodes]` nodes by uniform random attachment: each new node (value
* `1, 2, …`) is attached under a uniformly chosen existing node. This samples a broad spread of
* shapes — chains, bushy, and lopsided trees, plus single-node trees — without the left-heavy skew
* of a depth-first node budget, and is iterative so it never risks the call stack. Values are unique
* and dense from `0` (the root). Deterministic for a given [random], so the seed reproduces it.
*/
private fun randomTree(random: Random, maxNodes: Int): TreeNode<Int> {
val size = random.nextInt(1, maxNodes + 1)
val root = TreeNode(0)
val nodes = ArrayList<TreeNode<Int>>(size)
nodes.add(root)
for (value in 1 until size) {
val parent = nodes[random.nextInt(nodes.size)]
val child = TreeNode(value)
parent.addChild(child)
nodes.add(child)
}
return root
}
/** Maps each node to its position in this traversal. Keys compare by identity (TreeNode equality). */
private fun List<TreeNode<Int>>.indexMap(): Map<TreeNode<Int>, Int> =
withIndex().associate { (i, node) -> node to i }
/** Asserts two references point at the same node object (TreeNode uses identity equality). */
private fun assertSameNode(expected: TreeNode<*>?, actual: TreeNode<*>?, message: String) {
assertTrue(expected === actual, "$message — expected same node as $expected but was $actual")
}

View File

@@ -0,0 +1,123 @@
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.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue
class TreeNodeQueryTest {
// root(1)
// ├── n2(2)
// │ ├── n4(4)
// │ └── n5(5)
// └── n3(3)
// └── n6(6)
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)
// A completely separate tree.
private val otherRoot = TreeNode(10)
private val o11 = TreeNode(11)
init {
root.addChild(n2)
root.addChild(n3)
n2.addChild(n4)
n2.addChild(n5)
n3.addChild(n6)
otherRoot.addChild(o11)
}
@Test
fun lowestCommonAncestorOfTwoLeaves() {
assertSame(n2, n4.lowestCommonAncestor(n5))
assertSame(root, n4.lowestCommonAncestor(n6))
}
@Test
fun lowestCommonAncestorOfSameNode() {
assertSame(n4, n4.lowestCommonAncestor(n4))
}
@Test
fun lowestCommonAncestorOfAncestorAndDescendant() {
assertSame(n2, n2.lowestCommonAncestor(n4))
assertSame(n2, n4.lowestCommonAncestor(n2))
assertSame(root, root.lowestCommonAncestor(n6))
}
@Test
fun lowestCommonAncestorOfNodesInDifferentTreesIsNull() {
assertNull(n4.lowestCommonAncestor(o11))
assertNull(o11.lowestCommonAncestor(n4))
}
@Test
fun distanceValues() {
assertEquals(0, n4.distance(n4))
assertEquals(2, n4.distance(n5))
assertEquals(1, n2.distance(n4))
assertEquals(4, n4.distance(n6))
assertEquals(2, root.distance(n4))
}
@Test
fun distanceOfNodesInDifferentTreesIsNull() {
assertNull(n4.distance(o11))
}
@Test
fun pathBetweenSameNode() {
assertContentEquals(listOf(n4), n4.pathBetween(n4))
}
@Test
fun pathBetweenTwoLeaves() {
// n4 -> n2 -> n5 (lca = n2 appears once, endpoints are n4 and n5)
assertContentEquals(listOf(n4, n2, n5), n4.pathBetween(n5))
// n4 -> n2 -> root -> n3 -> n6 (lca = root appears once)
assertContentEquals(listOf(n4, n2, root, n3, n6), n4.pathBetween(n6))
}
@Test
fun pathBetweenWithUnequalDepthLegs() {
// Neither is an ancestor of the other and the legs differ in length: n4 is at depth 2, n3 at
// depth 1, lca = root. Exercises the asymmetric up/down assembly.
assertContentEquals(listOf(n4, n2, root, n3), n4.pathBetween(n3))
assertContentEquals(listOf(n3, root, n2, n4), n3.pathBetween(n4))
}
@Test
fun pathBetweenAncestorAndDescendant() {
assertContentEquals(listOf(n2, n4), n2.pathBetween(n4))
assertContentEquals(listOf(n4, n2), n4.pathBetween(n2))
assertContentEquals(listOf(root, n3, n6), root.pathBetween(n6))
}
@Test
fun pathBetweenOfNodesInDifferentTreesIsNull() {
assertNull(n4.pathBetween(o11))
}
@Test
fun containsTrueForValuesInSubtree() {
assertTrue(root.contains(1)) // the receiver itself
assertTrue(root.contains(6))
assertTrue(n2.contains(5))
}
@Test
fun containsFalseForValuesNotInSubtree() {
assertFalse(n2.contains(6)) // n6 lives under n3, not n2
assertFalse(root.contains(99))
}
}

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

@@ -0,0 +1,242 @@
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
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
class TreeNodeTest {
@Test
fun removeNodeTest() {
val root = TreeNode("Root")
val beveragesNode = TreeNode("Beverages")
val curdNode = TreeNode("Curd")
root.addChild(beveragesNode)
root.addChild(curdNode)
val teaNode = TreeNode("tea")
val coffeeNode = TreeNode("coffee")
val milkShakeNode = TreeNode("Milk Shake")
beveragesNode.addChild(teaNode)
beveragesNode.addChild(coffeeNode)
beveragesNode.addChild(milkShakeNode)
val gingerTeaNode = TreeNode("ginger tea")
val normalTeaNode = TreeNode("normal tea")
teaNode.addChild(gingerTeaNode)
teaNode.addChild(normalTeaNode)
val yogurtNode = TreeNode("yogurt")
val lassiNode = TreeNode("lassi")
curdNode.addChild(yogurtNode)
curdNode.addChild(lassiNode)
assertEquals(
"Root\n" +
"├── Beverages\n" +
"│ ├── tea\n" +
"│ │ ├── ginger tea\n" +
"│ │ └── normal tea\n" +
"│ ├── coffee\n" +
"│ └── Milk Shake\n" +
"└── Curd\n" +
" ├── yogurt\n" +
" └── lassi\n",
root.prettyString(),
"Pretty print test"
)
root.removeChild(curdNode)
gingerTeaNode.detach()
assertEquals(
"Root\n" +
"└── Beverages\n" +
" ├── tea\n" +
" │ └── normal tea\n" +
" ├── coffee\n" +
" └── Milk Shake\n",
root.prettyString(),
"Remove node test"
)
}
@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")
val beveragesNode = TreeNode("Beverages")
val curdNode = TreeNode("Curd")
root.addChild(beveragesNode)
root.addChild(curdNode)
val teaNode = TreeNode("tea")
val coffeeNode = TreeNode("coffee")
val milkShakeNode = TreeNode("Milk Shake")
beveragesNode.addChild(teaNode)
beveragesNode.addChild(coffeeNode)
beveragesNode.addChild(milkShakeNode)
val gingerTeaNode = TreeNode("ginger tea")
val normalTeaNode = TreeNode("normal tea")
teaNode.addChild(gingerTeaNode)
teaNode.addChild(normalTeaNode)
val yogurtNode = TreeNode("yogurt")
val lassiNode = TreeNode("lassi")
curdNode.addChild(yogurtNode)
curdNode.addChild(lassiNode)
root.clear()
assertEquals(root.children, emptyList())
assertEquals(beveragesNode.children, emptyList())
assertEquals(curdNode.children, emptyList())
assertEquals(teaNode.children, emptyList())
assertEquals(coffeeNode.children, emptyList())
assertEquals(milkShakeNode.children, emptyList())
assertEquals(gingerTeaNode.children, emptyList())
assertEquals(normalTeaNode.children, emptyList())
assertEquals(yogurtNode.children, emptyList())
assertEquals(lassiNode.children, emptyList())
assertNull(root.parent)
assertNull(beveragesNode.parent)
assertNull(curdNode.parent)
assertNull(teaNode.parent)
assertNull(coffeeNode.parent)
assertNull(milkShakeNode.parent)
assertNull(gingerTeaNode.parent)
assertNull(normalTeaNode.parent)
assertNull(yogurtNode.parent)
assertNull(lassiNode.parent)
}
@Test
fun kotlinExtTest() {
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)
val rootExt = tree("World") {
child("North America") {
child("USA")
}
child("Europe") {
child("Poland")
child("France")
}
}
assertEquals(root.prettyString(), rootExt.prettyString())
}
@Test
fun preOrderIteratorTest() {
val tree = tree("F") {
child("B") {
child("A")
child("D") {
child("C")
child("E")
}
}
child("G") {
child("I") {
child("H")
}
}
}
val expectedPreOrder = listOf("F", "B", "A", "D", "C", "E", "G", "I", "H")
assertContentEquals(expectedPreOrder, tree.toList().map { it.toString() })
}
@Test
fun postOrderIteratorTest() {
val tree = tree("A", TreeNodeIterators.PostOrder) {
child("B") {
child("E")
}
child("C")
child("D") {
child("F")
child("G")
child("H")
child("I")
child("J")
}
}
val expectedPreOrder = listOf("E", "B", "C", "F", "G", "H", "I", "J", "D", "A")
assertContentEquals(expectedPreOrder, tree.toList().map { it.toString() })
}
@Test
fun secondPostOrderIteratorTest() {
val tree = tree(1, TreeNodeIterators.PostOrder) {
child(2) {
child(5) {
child(10)
}
child(6) {
child(11)
child(12)
child(13)
}
}
child(3)
child(4) {
child(7)
child(8)
child(9)
}
}
val expectedOrder = listOf(10, 5, 11, 12, 13, 6, 2, 3, 7, 8, 9, 4, 1)
assertContentEquals(expectedOrder, tree.toList().map { it.value })
}
@Test
fun levelOrderIteratorTest() {
val tree = tree(1, TreeNodeIterators.LevelOrder) {
child(2) {
child(5) {
child(10)
}
child(6) {
child(11)
child(12)
child(13)
}
}
child(3)
child(4) {
child(7)
child(8)
child(9)
}
}
val expectedOrder = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)
assertContentEquals(expectedOrder, tree.toList().map { it.value })
}
}

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

@@ -0,0 +1,9 @@
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
public static final fun LazyTree-hGBTI10 (Lcom/github/adriankuta/datastructure/tree/TreeNode;Landroidx/compose/ui/Modifier;ZFLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
}
public final class com/github/adriankuta/datastructure/tree/compose/TreeNodeRowKt {
public static final fun TreeNodeRow-PfoAEA0 (Lcom/github/adriankuta/datastructure/tree/TreeNode;IZLkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;FLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
}

View File

@@ -0,0 +1,9 @@
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
public static final fun LazyTree-hGBTI10 (Lcom/github/adriankuta/datastructure/tree/TreeNode;Landroidx/compose/ui/Modifier;ZFLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
}
public final class com/github/adriankuta/datastructure/tree/compose/TreeNodeRowKt {
public static final fun TreeNodeRow-PfoAEA0 (Lcom/github/adriankuta/datastructure/tree/TreeNode;IZLkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;FLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V
}

View File

@@ -0,0 +1,108 @@
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
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()
}
dokka {
dokkaSourceSets.configureEach {
sourceLink {
// Resolve this module's GitHub source path relative to the repo root.
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
kotlin {
explicitApi()
jvmToolchain(21)
jvm()
androidTarget {
publishLibraryVariants("release")
// Match the Android variant's Kotlin bytecode to the Java level set in compileOptions.
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
}
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
commonMain.dependencies {
api(project(":"))
implementation(compose.runtime)
implementation(compose.foundation)
}
}
}
android {
namespace = "com.github.adriankuta.datastructure.tree.compose"
compileSdk = libs.versions.androidCompileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.androidMinSdk.get().toInt()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}

View File

@@ -0,0 +1,102 @@
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 androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
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)
}
}
}
}
/**
* Convenience overload of [LazyTree] that renders each node with the built-in [TreeNodeRow], so the
* common case is a single call:
*
* ```
* LazyTree(root)
* ```
*
* Use the overload that takes a `nodeContent` lambda when you need full control over a node's look.
*
* @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 indent the horizontal indentation applied per depth level.
* @param label maps a node's value to the text shown. Defaults to `toString()`.
*/
@Composable
public fun <T> LazyTree(
root: TreeNode<T>,
modifier: Modifier = Modifier,
initiallyExpanded: Boolean = true,
indent: Dp = 16.dp,
label: (T) -> String = { it.toString() },
) {
LazyTree(root, modifier, initiallyExpanded) { node, depth, expanded, toggle ->
TreeNodeRow(node, depth, expanded, toggle, indent = indent, label = label)
}
}
/**
* 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,59 @@
package com.github.adriankuta.datastructure.tree.compose
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.github.adriankuta.datastructure.tree.TreeNode
/**
* A sensible default row for a single node in a [LazyTree]. The whole row is clickable to expand or
* collapse, indentation reflects [depth], and a `▾`/`▸` marker precedes non-leaf nodes.
*
* It is intentionally foundation-only (no Material), so using it does not pull a theming dependency
* into your app. For full control over a node's appearance, use the `LazyTree` overload that takes a
* `nodeContent` lambda instead.
*
* ```
* LazyTree(root) { node, depth, expanded, toggle ->
* TreeNodeRow(node, depth, expanded, toggle)
* }
* ```
*
* @param node the node to render.
* @param depth the node's depth in the tree (root = 0), used for indentation.
* @param expanded whether the node is currently expanded.
* @param toggle flips this node's expansion state; invoked when the row is clicked.
* @param modifier the [Modifier] applied to the row.
* @param indent the horizontal indentation applied per depth level.
* @param label maps the node's value to the text shown. Defaults to `toString()`.
*/
@Composable
public fun <T> TreeNodeRow(
node: TreeNode<T>,
depth: Int,
expanded: Boolean,
toggle: () -> Unit,
modifier: Modifier = Modifier,
indent: Dp = 16.dp,
label: (T) -> String = { it.toString() },
) {
val marker = when {
node.children.isEmpty() -> ""
expanded -> ""
else -> ""
}
Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = toggle)
.padding(start = indent * depth, top = 8.dp, bottom = 8.dp, end = 8.dp),
) {
BasicText(text = marker + label(node.value))
}
}

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,102 @@
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()
}
dokka {
dokkaSourceSets.configureEach {
sourceLink {
// Resolve this module's GitHub source path relative to the repo root.
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
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,25 @@
public final class com/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode {
public fun <init> (Ljava/lang/Object;Lkotlinx/collections/immutable/PersistentList;)V
public synthetic fun <init> (Ljava/lang/Object;Lkotlinx/collections/immutable/PersistentList;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun addChild (Lcom/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode;)Lcom/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode;
public final fun component1 ()Ljava/lang/Object;
public final fun component2 ()Lkotlinx/collections/immutable/PersistentList;
public final fun copy (Ljava/lang/Object;Lkotlinx/collections/immutable/PersistentList;)Lcom/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode;
public static synthetic fun copy$default (Lcom/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode;Ljava/lang/Object;Lkotlinx/collections/immutable/PersistentList;ILjava/lang/Object;)Lcom/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode;
public fun equals (Ljava/lang/Object;)Z
public final fun getChildren ()Lkotlinx/collections/immutable/PersistentList;
public final fun getValue ()Ljava/lang/Object;
public fun hashCode ()I
public final fun mapValues (Lkotlin/jvm/functions/Function1;)Lcom/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode;
public final fun removeChild (Lcom/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode;)Lcom/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode;
public fun toString ()Ljava/lang/String;
}
public final class com/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNodeKt {
public static final fun height (Lcom/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode;)I
public static final fun levelOrder (Lcom/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode;)Ljava/util/List;
public static final fun nodeCount (Lcom/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode;)I
public static final fun postOrder (Lcom/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode;)Ljava/util/List;
public static final fun preOrder (Lcom/github/adriankuta/datastructure/tree/immutable/ImmutableTreeNode;)Ljava/util/List;
}

View File

@@ -0,0 +1,101 @@
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-immutable", version.toString())
pom {
name.set("Tree Data Structure — immutable")
description.set("Immutable, persistent tree variant (ImmutableTreeNode with structural sharing) 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()
}
dokka {
dokkaSourceSets.configureEach {
sourceLink {
// Resolve this module's GitHub source path relative to the repo root.
localDirectory.set(projectDir.resolve("src"))
val module = projectDir.relativeTo(rootDir).invariantSeparatorsPath
val prefix = if (module.isEmpty()) "" else "$module/"
remoteUrl("https://github.com/AdrianKuta/Tree-Data-Structure/blob/master/${prefix}src")
remoteLineSuffix.set("#L")
}
}
}
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(":"))
implementation(libs.kotlinx.collections.immutable)
}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}

View File

@@ -0,0 +1,147 @@
package com.github.adriankuta.datastructure.tree.immutable
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
/**
* A node in an immutable, persistent n-ary tree. Each node holds a [value] and an ordered
* [PersistentList] of [children]; nodes never carry a parent back-reference, so a subtree is a
* self-contained, acyclic value.
*
* Every mutating operation ([addChild], [removeChild], [mapValues]) returns a **new** root and
* leaves the receiver untouched. Subtrees that are not on the path of the change are reused as the
* same instances (structural sharing), so updates are cheap and old roots stay valid.
*
* Equality is value-based: two nodes are equal when their [value]s and [children] are equal
* (a `data class`), independent of identity.
*
* @param value the value stored in this node.
* @param children the ordered, persistent list of child subtrees.
*/
public data class ImmutableTreeNode<T>(
public val value: T,
public val children: PersistentList<ImmutableTreeNode<T>> = persistentListOf(),
) {
/**
* Returns a new node with [child] appended to this node's [children]. The receiver and every
* existing child subtree are reused unchanged (structural sharing).
*
* @param child the subtree to append.
* @return a new [ImmutableTreeNode] with [child] added; the receiver is not modified.
*/
public fun addChild(child: ImmutableTreeNode<T>): ImmutableTreeNode<T> =
copy(children = children.add(child))
/**
* Returns a new node with the first occurrence of [child] removed from this node's direct
* [children], compared by value-based equality. If [child] is not a direct child, a structurally
* equal new node is returned. The receiver is never modified.
*
* @param child the direct child subtree to remove.
* @return a new [ImmutableTreeNode] without [child]; the receiver is not modified.
*/
public fun removeChild(child: ImmutableTreeNode<T>): ImmutableTreeNode<T> =
copy(children = children.remove(child))
/**
* Returns a new tree of the same shape with every node's value transformed by [transform].
* The receiver is not modified.
*
* @param transform maps each node's value of type [T] to a value of type [R].
* @return a new [ImmutableTreeNode] of type [R] mirroring this tree's structure.
*/
public fun <R> mapValues(transform: (T) -> R): ImmutableTreeNode<R> =
ImmutableTreeNode(transform(value), children.map { it.mapValues(transform) }.toPersistentList())
}
/**
* Returns this subtree's nodes in pre-order (the receiver first, then each child subtree in order).
* Implemented iteratively, so it is safe on arbitrarily deep trees.
*
* @return the nodes of this subtree in pre-order, starting with the receiver.
*/
public fun <T> ImmutableTreeNode<T>.preOrder(): List<ImmutableTreeNode<T>> {
val result = mutableListOf<ImmutableTreeNode<T>>()
val stack = ArrayDeque<ImmutableTreeNode<T>>()
stack.addLast(this)
while (stack.isNotEmpty()) {
val node = stack.removeLast()
result.add(node)
node.children.asReversed().forEach { stack.addLast(it) }
}
return result
}
/**
* Returns this subtree's nodes in post-order (each child subtree in order, then the receiver last).
* Implemented iteratively, so it is safe on arbitrarily deep trees.
*
* @return the nodes of this subtree in post-order, ending with the receiver.
*/
public fun <T> ImmutableTreeNode<T>.postOrder(): List<ImmutableTreeNode<T>> {
val result = ArrayDeque<ImmutableTreeNode<T>>()
val stack = ArrayDeque<ImmutableTreeNode<T>>()
stack.addLast(this)
while (stack.isNotEmpty()) {
val node = stack.removeLast()
result.addFirst(node)
node.children.forEach { stack.addLast(it) }
}
return result.toList()
}
/**
* Returns this subtree's nodes in level-order (breadth-first: the receiver, then its children, then
* their children, and so on). Implemented iteratively, so it is safe on arbitrarily deep trees.
*
* @return the nodes of this subtree in breadth-first order, starting with the receiver.
*/
public fun <T> ImmutableTreeNode<T>.levelOrder(): List<ImmutableTreeNode<T>> {
val result = mutableListOf<ImmutableTreeNode<T>>()
val queue = ArrayDeque<ImmutableTreeNode<T>>()
queue.addLast(this)
while (queue.isNotEmpty()) {
val node = queue.removeFirst()
result.add(node)
node.children.forEach { queue.addLast(it) }
}
return result
}
/**
* Counts all descendants of this node; the receiver itself is not counted (matching the core
* `TreeNode.nodeCount`). Implemented iteratively, so it is safe on arbitrarily deep trees.
*
* @return the number of descendant nodes (children and nested children) of this node.
*/
public fun <T> ImmutableTreeNode<T>.nodeCount(): Int {
var count = 0
val stack = ArrayDeque<ImmutableTreeNode<T>>()
stack.addAll(children)
while (stack.isNotEmpty()) {
val node = stack.removeLast()
count++
stack.addAll(node.children)
}
return count
}
/**
* Returns the number of edges on the longest path between this node and a descendant leaf (0 for a
* leaf). Implemented iteratively, so it is safe on arbitrarily deep trees.
*
* @return the height of this subtree, measured in edges.
*/
public fun <T> ImmutableTreeNode<T>.height(): Int {
var maxDepth = 0
val stack = ArrayDeque<Pair<ImmutableTreeNode<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
}

View File

@@ -0,0 +1,135 @@
package com.github.adriankuta.datastructure.tree.immutable
import kotlinx.collections.immutable.persistentListOf
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertSame
import kotlin.test.assertTrue
class ImmutableTreeNodeTest {
// World
// ├── North America
// │ └── USA
// └── Europe
// ├── Poland
// └── Germany
private val usa = ImmutableTreeNode("USA")
private val northAmerica = ImmutableTreeNode("North America", persistentListOf(usa))
private val poland = ImmutableTreeNode("Poland")
private val germany = ImmutableTreeNode("Germany")
private val europe = ImmutableTreeNode("Europe", persistentListOf(poland, germany))
private val world = ImmutableTreeNode("World", persistentListOf(northAmerica, europe))
@Test
fun addChildReturnsNewInstanceAndLeavesOriginalUnchanged() {
val asia = ImmutableTreeNode("Asia")
val updated = world.addChild(asia)
assertEquals(3, updated.children.size)
assertEquals("Asia", updated.children[2].value)
// Original is untouched.
assertEquals(2, world.children.size)
assertFalse(updated === world)
}
@Test
fun removeChildReturnsNewInstanceAndLeavesOriginalUnchanged() {
val updated = world.removeChild(europe)
assertEquals(1, updated.children.size)
assertEquals("North America", updated.children[0].value)
// Original is untouched.
assertEquals(2, world.children.size)
assertFalse(updated === world)
}
@Test
fun addChildSharesUnmodifiedSiblingSubtrees() {
val asia = ImmutableTreeNode("Asia")
val updated = world.addChild(asia)
// The siblings that are not on the modified path are the SAME instances.
assertSame(northAmerica, updated.children[0])
assertSame(europe, updated.children[1])
}
@Test
fun rebuildingOnlyOnePathSharesTheOtherSubtree() {
// Add a child under Europe; North America's subtree should be reused untouched.
val spain = ImmutableTreeNode("Spain")
val newEurope = europe.addChild(spain)
val updated = world.copy(children = world.children.set(1, newEurope))
assertSame(northAmerica, updated.children[0])
assertFalse(updated.children[1] === europe)
assertSame(usa, updated.children[0].children[0])
}
@Test
fun mapValuesTransformsEveryValueAndKeepsShape() {
val lengths = world.mapValues { it.length }
assertEquals("World".length, lengths.value)
assertEquals(2, lengths.children.size)
assertEquals("North America".length, lengths.children[0].value)
assertEquals("USA".length, lengths.children[0].children[0].value)
assertEquals("Germany".length, lengths.children[1].children[1].value)
}
@Test
fun preOrderVisitsParentBeforeChildren() {
assertEquals(
listOf("World", "North America", "USA", "Europe", "Poland", "Germany"),
world.preOrder().map { it.value },
)
}
@Test
fun postOrderVisitsChildrenBeforeParent() {
assertEquals(
listOf("USA", "North America", "Poland", "Germany", "Europe", "World"),
world.postOrder().map { it.value },
)
}
@Test
fun levelOrderVisitsBreadthFirst() {
assertEquals(
listOf("World", "North America", "Europe", "USA", "Poland", "Germany"),
world.levelOrder().map { it.value },
)
}
@Test
fun nodeCountExcludesReceiver() {
assertEquals(5, world.nodeCount())
assertEquals(1, northAmerica.nodeCount())
assertEquals(0, usa.nodeCount())
}
@Test
fun heightCountsEdgesOnLongestPath() {
assertEquals(2, world.height())
assertEquals(1, europe.height())
assertEquals(0, usa.height())
}
@Test
fun equalityIsValueBased() {
val sameWorld = ImmutableTreeNode(
"World",
persistentListOf(
ImmutableTreeNode("North America", persistentListOf(ImmutableTreeNode("USA"))),
ImmutableTreeNode("Europe", persistentListOf(ImmutableTreeNode("Poland"), ImmutableTreeNode("Germany"))),
),
)
assertEquals(world, sameWorld)
assertTrue(world == sameWorld)
assertFalse(world === sameWorld)
}
}

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

Some files were not shown because too many files have changed in this diff Show More