diff --git a/.github/workflows/gradle build.yml b/.github/workflows/gradle build.yml
new file mode 100644
index 0000000..89b75d8
--- /dev/null
+++ b/.github/workflows/gradle build.yml
@@ -0,0 +1,32 @@
+name: test
+
+on:
+ push:
+ pull_request:
+ branches: [work]
+
+jobs:
+ build:
+ strategy:
+ matrix:
+ os: [ ubuntu-latest, windows-latest, macos-latest ]
+ runs-on: ${{ matrix.os }}
+
+ steps:
+ - name: actions checkout
+ uses: actions/checkout@v3
+ - name: Set up JDK
+ uses: actions/setup-java@v3
+ with:
+ java-version: 18
+ distribution: 'temurin'
+
+ - name: Cache Gradle packages
+ uses: actions/cache@v3
+ with:
+ path: ~/.gradle/caches
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
+ restore-keys: ${{ runner.os }}-gradle
+
+ - name: Build with Gradle
+ run: ./gradlew build
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9deb9f5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,108 @@
+/.idea/
+
+/.gradle/
+
+build-logic/.gradle/
+
+**/build
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+### Kotlin ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+replay_pid*
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### VisualStudioCode ###
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+
+# Local History for Visual Studio Code
+.history/
+
+# Built Visual Studio Code Extensions
+*.vsix
+
+### VisualStudioCode Patch ###
+# Ignore all local history of files
+.history
+.ionide
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7a4a3ea
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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
+
+ http://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.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c835003
--- /dev/null
+++ b/README.md
@@ -0,0 +1,65 @@
+
+
+# Tree Structure
+
+Trees are a data structures that link nodes in a parent/child relationship, in the sense that there're nodes that depend on or come off other nodes. Each node contains a key and a value.
+
+
+[](https://www.apache.org/licenses/LICENSE-2.0)
+
+
+
+
+## Build tool
+
+The Gradle build tool is used to manage the project.
+You only need to write one line to build a project:
+```bash
+ ./gradlew build
+```
+## How to use app
+
+To run app write one line to build a project:
+```
+./gradlew run
+```
+
+After launching the app, you will see three buttons:
+ - `New` Creates a new tree, you just have to type in a name and choose one of three tree types: Binary search Tree, AVL Tree, Red-Black Tree.
+- `Open` Invites you to open a tree of three possible databases: Json, SQLite, Neo4j. Point and click on the database you want, and you will see a list of trees that already exist. Then choose the file you want.
+- `Exit` Close the app.
+
+After creating or opening a tree, a window will appear where you can:
+- Insert key and value to your tree. If a node with this name already exists, its value will be overwritten with the new one.
+- Remove the node with the entered key.
+- Find the value of a node using a key.
+- Save the tree in three possible databases: Json, SQLite, Neo4j. If you want to save the tree with an existing name, the app will overwrite the old file.
+
+
+
+
+## How to use library
+- `insert` - Inserts a node into the tree. It takes `Key` and `Value` and uses them to add. If a node with this name already exists, its value will be overwritten with the new one.
+- `remove` - Removes a node from the tree. It accepts the `Key` and uses it to delete the node.
+- `get` - Retrieves a given node. Use the `Key` to get the `Value`. If there is no such key in the tree the program will return null.
+
+
+## How to use Data Bases
+
+- ## Json
+If you want to change the save folder of your tree, change `json_save_dir` value in the `trees-11/app/src/main/resources/Json.properties` file.
+
+- ## SQLite
+If you want to change the saving path of your tree, change `sqlite_path` value in the `trees-11/app/src/main/resources/SQLite.properties` file.
+- ## Neo4j
+You should download Neo4j Desktop [here](https://neo4j.com/).
+
+Open app.
+
+

+
+
+
+
+If you want to change uri, user name or password of your data Base, change relevant fields in the `trees-11/app/src/main/resources/Neo4j.properties` file.
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
new file mode 100644
index 0000000..8211d3e
--- /dev/null
+++ b/app/build.gradle.kts
@@ -0,0 +1,28 @@
+plugins {
+ id("trees.kotlin-application-conventions")
+ id("org.jetbrains.compose") version "1.4.0"
+}
+
+group = "org.example"
+version = "1.0-SNAPSHOT"
+
+val sqliteJdbcVersion: String by project
+
+dependencies {
+ implementation(project(":lib"))
+ implementation("org.neo4j.driver:neo4j-java-driver:5.7.0")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.2")
+ implementation("org.xerial", "sqlite-jdbc", sqliteJdbcVersion)
+
+ implementation(compose.desktop.currentOs)
+ implementation("org.jetbrains.compose.material3:material3-desktop:1.2.1")
+}
+
+application {
+ mainClass.set("app.MainKt")
+}
+
+repositories {
+ maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
+ google()
+}
diff --git a/app/src/main/kotlin/app/Main.kt b/app/src/main/kotlin/app/Main.kt
new file mode 100644
index 0000000..9ad5217
--- /dev/null
+++ b/app/src/main/kotlin/app/Main.kt
@@ -0,0 +1,23 @@
+package app
+
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.application
+import androidx.compose.ui.window.rememberWindowState
+import app.theme.AppTheme
+import app.ui.Main
+
+
+fun main() = application {
+ Window(
+ onCloseRequest = ::exitApplication,
+ title = "Trees",
+ state = rememberWindowState(width = 900.dp, height = 700.dp),
+ icon = painterResource("icon.png")
+ ) {
+ AppTheme {
+ Main(window)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/app/controller/Controller.kt b/app/src/main/kotlin/app/controller/Controller.kt
new file mode 100644
index 0000000..41030f9
--- /dev/null
+++ b/app/src/main/kotlin/app/controller/Controller.kt
@@ -0,0 +1,198 @@
+package app.controller
+
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import dataBase.*
+import trees.*
+import java.io.IOException
+import kotlin.math.pow
+
+object Controller {
+ init {
+ try {
+ System.getProperties().load(ClassLoader.getSystemResourceAsStream("App.properties"))
+ System.getProperties().load(ClassLoader.getSystemResourceAsStream("Json.properties"))
+ System.getProperties().load(ClassLoader.getSystemResourceAsStream("Neo4j.properties"))
+ System.getProperties().load(ClassLoader.getSystemResourceAsStream("SQLite.properties"))
+ } catch (ex: Exception) {
+ throw IOException("Cannot get properties file\nCheck that all properties file exist in the src/main/kotlin/app/resources\n$ex")
+ }
+ }
+
+ enum class TreeType {
+ RBTree,
+ AVLTree,
+ BSTree
+ }
+
+ enum class KeysType {
+ Int,
+ Float,
+ String
+ }
+
+ enum class DatabaseType {
+ Json,
+ Neo4j,
+ SQLite
+ }
+
+ fun validKey(key: String) = run {
+ try {
+ key.toInt()
+ true
+ } catch (ex: Exception) {
+ false
+ }
+ }
+
+ fun validateName(name: String) {
+ for (i in name)
+ if (i !in 'a'..'z' && i !in 'A'..'Z' && i !in '0'..'9')
+ throw IllegalArgumentException("Please use only ascii letters or digits")
+ if (name.isNotEmpty() && name[0] in '0'..'9')
+ throw IllegalArgumentException("Please don't use a digit as the first char")
+ if (name.length !in 1..System.getProperty("max_string_len")
+ .toInt()
+ ) throw IllegalArgumentException("The name must be less than ${System.getProperty("max_string_len")} and greater than 0")
+ }
+
+ fun getTree(treeType: TreeType, keysType: KeysType) = when(keysType) {
+ KeysType.Int -> getTree(treeType)
+ else -> throw IllegalArgumentException("Only Int support now")
+ }
+
+ private fun > getTree(treeType: TreeType) = when (treeType) {
+ TreeType.BSTree -> BSTree>>()
+ TreeType.AVLTree -> AVLTree>>()
+ TreeType.RBTree -> RBTree>>()
+ }
+
+ fun getDatabase(databaseType: DatabaseType) = when (databaseType) {
+ DatabaseType.Json -> Json(System.getProperty("json_save_dir"))
+ DatabaseType.Neo4j -> Neo4j(
+ System.getProperty("neo4j_uri"),
+ System.getProperty("neo4j_user"),
+ System.getProperty("neo4j_password")
+ )
+
+ DatabaseType.SQLite -> SQLite(System.getProperty("sqlite_path"), System.getProperty("max_string_len").toUInt())
+ }
+
+ class Database(databaseType: DatabaseType) {
+ private val database = getDatabase(databaseType)
+
+ fun getAllTrees() = database.getAllTrees()
+ fun removeTree(treeName: String) = database.removeTree(treeName)
+ fun clean() = database.clean()
+ fun close() = database.close()
+ }
+
+ open class DrawNode(
+ var key: String,
+ var value: String,
+ var x: MutableState,
+ var y: MutableState,
+ var parent: DrawNode?
+ )
+
+ class DrawTree {
+ private var tree: BinTree>>
+ private var treeName: String
+ private var keysType: KeysType
+
+ var viewCoordinates = Pair(0F, 0F)
+
+ var startCoordinate = Pair(0F, 0F) //coordinates of the root node
+
+ var xMinInterval = 100F //interval between nodes
+ var yInterval = -100F //interval between nodes
+ var content = mutableStateOf(listOf())
+
+ constructor(treeName: String, databaseType: DatabaseType) {
+ this.treeName = treeName
+ val treeData = getDatabase(databaseType).readTree(treeName)
+ tree = treeData.first
+ viewCoordinates = treeData.second
+ keysType = KeysType.String
+ }
+
+ constructor(treeName: String, treeType: TreeType, keysType: KeysType) {
+ this.treeName = treeName
+ this.keysType = keysType
+ tree = getTree(treeType, keysType)
+ }
+
+ fun getAllDrawNodes(): MutableList {
+ val listOfDrawNodes = mutableListOf()
+ val mapOfKeysNodes = mutableMapOf>()
+ for (i in tree.getNodesDataWithParentKeys().reversed()) {
+ val node = DrawNode(
+ i.first.toString(),
+ i.second.first,
+ mutableStateOf(i.second.second.first),
+ mutableStateOf(i.second.second.second),
+ parent = null
+ )
+ listOfDrawNodes.add(node)
+ if (mapOfKeysNodes[i.third] == null)
+ mapOfKeysNodes[i.third] = mutableListOf(node)
+ else mapOfKeysNodes[i.third]?.add(node)
+ mapOfKeysNodes[i.first]?.forEach { it.parent = node }
+ }
+
+ return listOfDrawNodes
+ }
+
+ fun reInitAllDrawNodes() {
+ content.value = getAllDrawNodes()
+ }
+
+ private fun rewriteAllCoordinates() {
+ fun offsetOnLevel(level: Int, height: Int) = if (height == 2 && level != 0) xMinInterval / 2 else
+ ((0.5.pow(level) - 1) * (height - 2) * xMinInterval * (-2)).toFloat() //the sum of the terms of the geometric progression
+
+ var lastLevel = -1
+ var curX = startCoordinate.first
+ var curY = startCoordinate.second + yInterval
+ var levelInterval = 0F
+ tree.rewriteAllValue(true) { value, level, height ->
+ if (level != lastLevel) {
+ curY -= yInterval
+ curX = startCoordinate.first - offsetOnLevel(level, height)
+ levelInterval = xMinInterval * 2F.pow(height - level - 1)
+ } else curX += levelInterval
+ lastLevel = level
+ if (value != null)
+ Pair(value.first, Pair(curX, curY))
+ else null
+ }
+ }
+
+ fun drawInsert(key: String, value: String) {
+ tree.insert(key.toInt(), Pair(value, Pair(0F, 0F)))
+
+ rewriteAllCoordinates()
+ }
+
+ fun drawRemove(key: String) {
+ tree.remove(key.toInt())
+
+ rewriteAllCoordinates()
+ }
+
+ fun drawFind(key: String) = tree.get(key.toInt())?.first
+
+ fun updateCoordinate(node: DrawNode) {
+ tree.insert(node.key.toInt(), Pair(node.value, Pair(node.x.value, node.y.value)))
+ }
+
+ fun saveToDB(databaseType: DatabaseType) {
+ getDatabase(databaseType).saveTree(treeName, tree, viewCoordinates)
+ }
+
+ fun clean() {
+ tree.clean()
+ }
+ }
+}
diff --git a/app/src/main/kotlin/app/theme/Color.kt b/app/src/main/kotlin/app/theme/Color.kt
new file mode 100644
index 0000000..2b2e0bb
--- /dev/null
+++ b/app/src/main/kotlin/app/theme/Color.kt
@@ -0,0 +1,71 @@
+package app.theme
+
+import androidx.compose.ui.graphics.Color
+
+val md_theme_light_primary = Color(0xFF00687C)
+val md_theme_light_onPrimary = Color(0xFFFFFFFF)
+val md_theme_light_negative_primary = Color(0xFFA44F45)
+val md_theme_light_border = Color(0xFFf1f7ff)
+val md_theme_light_primaryContainer = Color(0xFFB0ECFF)
+val md_theme_light_onPrimaryContainer = Color(0xFF001F27)
+val md_theme_light_secondary = Color(0xFF4B6269)
+val md_theme_light_onSecondary = Color(0xFFFFFFFF)
+val md_theme_light_secondaryContainer = Color(0xFFCEE7EF)
+val md_theme_light_onSecondaryContainer = Color(0xFF061F25)
+val md_theme_light_tertiary = Color(0xFF585C7E)
+val md_theme_light_onTertiary = Color(0xFFFFFFFF)
+val md_theme_light_tertiaryContainer = Color(0xFFDEE0FF)
+val md_theme_light_onTertiaryContainer = Color(0xFF141937)
+val md_theme_light_error = Color(0xFFBA1A1A)
+val md_theme_light_errorContainer = Color(0xFFFFDAD6)
+val md_theme_light_onError = Color(0xFFFFFFFF)
+val md_theme_light_onErrorContainer = Color(0xFF410002)
+val md_theme_light_background = Color(0xFFFBFCFE)
+val md_theme_light_onBackground = Color(0xFF191C1D)
+val md_theme_light_surface = Color(0xFFFBFCFE)
+val md_theme_light_onSurface = Color(0xFF191C1D)
+val md_theme_light_surfaceVariant = Color(0xFFDBE4E7)
+val md_theme_light_onSurfaceVariant = Color(0xFF40484B)
+val md_theme_light_outline = Color(0xFF70787C)
+val md_theme_light_inverseOnSurface = Color(0xFFEFF1F2)
+val md_theme_light_inverseSurface = Color(0xFF2E3132)
+val md_theme_light_inversePrimary = Color(0xFF57D6F6)
+val md_theme_light_shadow = Color(0xFF000000)
+val md_theme_light_surfaceTint = Color(0xFF00687C)
+val md_theme_light_outlineVariant = Color(0xFFBFC8CB)
+val md_theme_light_scrim = Color(0xFF000000)
+
+val md_theme_dark_primary = Color(0xFF57D6F6)
+val md_theme_dark_onPrimary = Color(0xFF003641)
+val md_theme_dark_negative_primary = Color(0xFF8E2417)
+val md_theme_dark_primaryContainer = Color(0xFF004E5E)
+val md_theme_dark_onPrimaryContainer = Color(0xFFB0ECFF)
+val md_theme_dark_secondary = Color(0xFFB2CBD3)
+val md_theme_dark_onSecondary = Color(0xFF1D343A)
+val md_theme_dark_secondaryContainer = Color(0xFF344A51)
+val md_theme_dark_onSecondaryContainer = Color(0xFFCEE7EF)
+val md_theme_dark_tertiary = Color(0xFFC0C4EB)
+val md_theme_dark_onTertiary = Color(0xFF292E4D)
+val md_theme_dark_tertiaryContainer = Color(0xFF404565)
+val md_theme_dark_onTertiaryContainer = Color(0xFFDEE0FF)
+val md_theme_dark_error = Color(0xFFFFB4AB)
+val md_theme_dark_errorContainer = Color(0xFF93000A)
+val md_theme_dark_onError = Color(0xFF690005)
+val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
+val md_theme_dark_background = Color(0xFF191C1D)
+val md_theme_dark_onBackground = Color(0xFFE1E3E4)
+val md_theme_dark_surface = Color(0xFF191C1D)
+val md_theme_dark_onSurface = Color(0xFFE1E3E4)
+val md_theme_dark_surfaceVariant = Color(0xFF40484B)
+val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CB)
+val md_theme_dark_outline = Color(0xFF899295)
+val md_theme_dark_inverseOnSurface = Color(0xFF191C1D)
+val md_theme_dark_inverseSurface = Color(0xFFE1E3E4)
+val md_theme_dark_inversePrimary = Color(0xFF00687C)
+val md_theme_dark_shadow = Color(0xFF000000)
+val md_theme_dark_surfaceTint = Color(0xFF57D6F6)
+val md_theme_dark_outlineVariant = Color(0xFF40484B)
+val md_theme_dark_scrim = Color(0xFF000000)
+
+
+val seed = Color(0xFF004452)
diff --git a/app/src/main/kotlin/app/theme/Theme.kt b/app/src/main/kotlin/app/theme/Theme.kt
new file mode 100644
index 0000000..b1319ac
--- /dev/null
+++ b/app/src/main/kotlin/app/theme/Theme.kt
@@ -0,0 +1,86 @@
+package app.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.darkColorScheme
+import androidx.compose.material3.lightColorScheme
+import androidx.compose.runtime.Composable
+
+
+private val LightColors = lightColorScheme(
+ primary = md_theme_light_primary,
+ onPrimary = md_theme_light_onPrimary,
+ primaryContainer = md_theme_light_primaryContainer,
+ onPrimaryContainer = md_theme_light_onPrimaryContainer,
+ secondary = md_theme_light_secondary,
+ onSecondary = md_theme_light_onSecondary,
+ secondaryContainer = md_theme_light_secondaryContainer,
+ onSecondaryContainer = md_theme_light_onSecondaryContainer,
+ tertiary = md_theme_light_tertiary,
+ onTertiary = md_theme_light_onTertiary,
+ tertiaryContainer = md_theme_light_tertiaryContainer,
+ onTertiaryContainer = md_theme_light_onTertiaryContainer,
+ error = md_theme_light_error,
+ errorContainer = md_theme_light_errorContainer,
+ onError = md_theme_light_onError,
+ onErrorContainer = md_theme_light_onErrorContainer,
+ background = md_theme_light_background,
+ onBackground = md_theme_light_onBackground,
+ surface = md_theme_light_surface,
+ onSurface = md_theme_light_onSurface,
+ surfaceVariant = md_theme_light_surfaceVariant,
+ onSurfaceVariant = md_theme_light_onSurfaceVariant,
+ outline = md_theme_light_outline,
+ inverseOnSurface = md_theme_light_inverseOnSurface,
+ inverseSurface = md_theme_light_inverseSurface,
+ inversePrimary = md_theme_light_inversePrimary,
+ surfaceTint = md_theme_light_surfaceTint,
+)
+
+
+private val DarkColors = darkColorScheme(
+ primary = md_theme_dark_primary,
+ onPrimary = md_theme_dark_onPrimary,
+ primaryContainer = md_theme_dark_primaryContainer,
+ onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+ secondary = md_theme_dark_secondary,
+ onSecondary = md_theme_dark_onSecondary,
+ secondaryContainer = md_theme_dark_secondaryContainer,
+ onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+ tertiary = md_theme_dark_tertiary,
+ onTertiary = md_theme_dark_onTertiary,
+ tertiaryContainer = md_theme_dark_tertiaryContainer,
+ onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+ error = md_theme_dark_error,
+ errorContainer = md_theme_dark_errorContainer,
+ onError = md_theme_dark_onError,
+ onErrorContainer = md_theme_dark_onErrorContainer,
+ background = md_theme_dark_background,
+ onBackground = md_theme_dark_onBackground,
+ surface = md_theme_dark_surface,
+ onSurface = md_theme_dark_onSurface,
+ surfaceVariant = md_theme_dark_surfaceVariant,
+ onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+ outline = md_theme_dark_outline,
+ inverseOnSurface = md_theme_dark_inverseOnSurface,
+ inverseSurface = md_theme_dark_inverseSurface,
+ inversePrimary = md_theme_dark_inversePrimary,
+ surfaceTint = md_theme_dark_surfaceTint,
+)
+
+@Composable
+fun AppTheme(
+ useDarkTheme: Boolean = isSystemInDarkTheme(),
+ content: @Composable() () -> Unit
+) {
+ val colors = if (!useDarkTheme) {
+ LightColors
+ } else {
+ DarkColors
+ }
+
+ MaterialTheme(
+ colorScheme = colors,
+ content = content
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/app/ui/CreatNewWindow.kt b/app/src/main/kotlin/app/ui/CreatNewWindow.kt
new file mode 100644
index 0000000..b37bb31
--- /dev/null
+++ b/app/src/main/kotlin/app/ui/CreatNewWindow.kt
@@ -0,0 +1,175 @@
+package app.ui
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import app.controller.Controller
+
+@Composable
+fun CreatNewTree(onBack: () -> Unit, onClick: (Controller.DrawTree) -> Unit) {
+ var name by remember { mutableStateOf("") }
+ val error: MutableState = remember { mutableStateOf(null) }
+ val treeType = remember { mutableStateOf(Controller.TreeType.BSTree) }
+ val keysType = remember { mutableStateOf(Controller.KeysType.Int) }
+
+ fun isNameValid() {
+ try {
+ Controller.validateName(name)
+ } catch (ex: Exception) {
+ error.value = ex.message
+ return
+ }
+ error.value = null
+ }
+ Column(
+ modifier = Modifier.fillMaxSize().padding(start = 120.dp, end = 120.dp),
+ verticalArrangement = Arrangement.aligned(Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ OutlinedTextField(
+ value = name,
+ onValueChange = { name = it; isNameValid(); },
+ label = { Text(text = "name") },
+ isError = error.value != null,
+ singleLine = true,
+ modifier = Modifier.weight(0.70f),
+ shape = MaterialTheme.shapes.extraLarge,
+ )
+ }
+ Spacer(modifier = Modifier.height(15.dp))
+
+ if (error.value != null)
+ Text(
+ text = error.value.toString()
+ )
+
+
+ Spacer(modifier = Modifier.height(15.dp))
+
+ Row(verticalAlignment = Alignment.CenterVertically) {
+
+ Button(
+ onClick = onBack,
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.3f).height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text(
+ text = "Exit",
+ )
+ }
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Box(modifier = Modifier.weight(0.3f)) {
+ val expanded = remember { mutableStateOf(false) }
+
+ Button(
+ onClick = {
+ expanded.value = true
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.fillMaxWidth().height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text(
+ text = treeType.value.toString(),
+ )
+ }
+
+ DropdownMenu(
+ expanded = expanded.value,
+ onDismissRequest = { expanded.value = false }
+ ) {
+ DropdownMenuItem(onClick = {
+ treeType.value = Controller.TreeType.BSTree; expanded.value = false
+ }) {
+ Text("Binary search tree")
+ }
+ Divider()
+ DropdownMenuItem(onClick = {
+ treeType.value = Controller.TreeType.AVLTree; expanded.value = false
+ }) {
+ Text("AVL tree")
+ }
+ Divider()
+ DropdownMenuItem(onClick = {
+ treeType.value = Controller.TreeType.RBTree; expanded.value = false
+ }) {
+ Text("Red-black tree")
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Box(modifier = Modifier.weight(0.3f)) {
+ val expanded = remember { mutableStateOf(false) }
+
+ Button(
+ enabled = false,
+ onClick = {
+ expanded.value = true
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.fillMaxWidth().height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text(
+ text = keysType.value.toString(),
+ )
+ }
+
+ DropdownMenu(
+ expanded = expanded.value,
+ onDismissRequest = { expanded.value = false }
+ ) {
+ DropdownMenuItem(onClick = { keysType.value = Controller.KeysType.Int; expanded.value = false }) {
+ Text("Int keys")
+ }
+ Divider()
+ DropdownMenuItem(onClick = { keysType.value = Controller.KeysType.Float; expanded.value = false }) {
+ Text("Float keys")
+ }
+ Divider()
+ DropdownMenuItem(onClick = {
+ keysType.value = Controller.KeysType.String; expanded.value = false
+ }) {
+ Text("String keys")
+ }
+ }
+ }
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Button(
+ enabled = error.value == null && name.isNotEmpty(),
+ onClick = {
+ val tree = Controller.DrawTree(name, treeType.value, keysType.value)
+ name = ""
+ onClick(tree)
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.3f).height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text(
+ text = "Create",
+ )
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/app/ui/MainWindow.kt b/app/src/main/kotlin/app/ui/MainWindow.kt
new file mode 100644
index 0000000..de4e236
--- /dev/null
+++ b/app/src/main/kotlin/app/ui/MainWindow.kt
@@ -0,0 +1,65 @@
+package app.ui
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import kotlin.system.exitProcess
+
+@Composable
+fun MainWindow(onClickNew: () -> Unit, onClickOpen: () -> Unit) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.aligned(Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Button(
+ onClick = onClickNew,
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.fillMaxWidth(0.6f).height(70.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text(
+ text = "New",
+ style = MaterialTheme.typography.headlineSmall
+ )
+ }
+ Spacer(modifier = Modifier.height(15.dp))
+ Button(
+ onClick = onClickOpen,
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.fillMaxWidth(0.6f).height(70.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text(
+ text = "Open",
+ style = MaterialTheme.typography.headlineSmall
+ )
+ }
+ Spacer(modifier = Modifier.height(15.dp))
+
+ Button(
+ onClick = { exitProcess(1) },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.fillMaxWidth(0.6f).height(70.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text(
+ text = "Exit",
+ style = MaterialTheme.typography.headlineSmall
+ )
+ }
+ }
+
+}
diff --git a/app/src/main/kotlin/app/ui/OpenTree.kt b/app/src/main/kotlin/app/ui/OpenTree.kt
new file mode 100644
index 0000000..3d448a2
--- /dev/null
+++ b/app/src/main/kotlin/app/ui/OpenTree.kt
@@ -0,0 +1,175 @@
+package app.ui
+
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import app.controller.Controller
+
+
+@Composable
+fun OpenTree(onBack: () -> Unit, onClick: (Controller.DrawTree) -> Unit) {
+ val files = remember { mutableStateOf(mutableStateListOf>>()) }
+ val dataBaseType = remember { mutableStateOf(Controller.DatabaseType.Json) }
+ Column(
+ modifier = Modifier.fillMaxSize().padding(start = 120.dp, end = 120.dp),
+ verticalArrangement = Arrangement.aligned(Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Button(
+ onClick = onBack,
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.3f).height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text(
+ text = "Exit",
+ )
+ }
+ Spacer(modifier = Modifier.width(16.dp))
+ Button(
+ onClick = {
+ files.value =
+ Controller.Database(Controller.DatabaseType.Json).getAllTrees().toMutableStateList()
+ dataBaseType.value = Controller.DatabaseType.Json
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.3f).height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text(
+ text = "Json",
+ )
+ }
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Button(
+ onClick = {
+ files.value =
+ Controller.Database(Controller.DatabaseType.SQLite).getAllTrees().toMutableStateList()
+ dataBaseType.value = Controller.DatabaseType.SQLite
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.3f).height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text(
+ text = "SQLite",
+ )
+ }
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Button(
+ onClick = {
+ files.value =
+ Controller.Database(Controller.DatabaseType.Neo4j).getAllTrees().toMutableStateList()
+ dataBaseType.value = Controller.DatabaseType.Neo4j
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.3f).height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text(
+ text = "Neo4j",
+ )
+ }
+ }
+
+ Spacer(modifier = Modifier.height(20.dp))
+
+ if (files.value.isNotEmpty()) {
+
+ Button(
+ onClick = {
+ Controller.Database(dataBaseType.value).clean()
+ files.value = mutableStateListOf()
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.height(50.dp).fillMaxWidth(0.95f),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.onErrorContainer
+ )
+ ) {
+ Text("delete all trees saved in ${dataBaseType.value}")
+ }
+ Spacer(modifier = Modifier.height(10.dp))
+ }
+
+ LazyColumn {
+ items(files.value) { file ->
+ Spacer(modifier = Modifier.width(20.dp))
+ Box(
+ modifier = Modifier.fillMaxWidth(0.95f)
+ .zIndex(0f)
+ .border(4.dp, MaterialTheme.colorScheme.background, RoundedCornerShape(20.dp))
+ ) {
+ Row(
+ modifier = Modifier.fillMaxSize()
+ .zIndex(1f),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Button(
+ onClick = {
+ onClick(Controller.DrawTree(file.first, dataBaseType.value))
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(3f).width(30.dp).fillMaxHeight(),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text("Open")
+ }
+
+ Text(
+ modifier = Modifier.weight(6f),
+ textAlign = TextAlign.Center,
+ text = "name: \"${file.first}\""
+ )
+ Text(
+ modifier = Modifier.weight(3f),
+ textAlign = TextAlign.Center,
+ text = file.second
+ )
+
+ Button(
+ onClick = {
+ Controller.Database(dataBaseType.value).removeTree(file.first)
+ files.value.remove(file)
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(3f).width(30.dp).fillMaxHeight(),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.onErrorContainer
+ )
+ ) {
+ Text("Delete")
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/app/ui/TreeWindow.kt b/app/src/main/kotlin/app/ui/TreeWindow.kt
new file mode 100644
index 0000000..46d792b
--- /dev/null
+++ b/app/src/main/kotlin/app/ui/TreeWindow.kt
@@ -0,0 +1,334 @@
+package app.ui
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.AlertDialog
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import app.controller.Controller
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun Tree(onBack: () -> Unit, tree: Controller.DrawTree) {
+ var textForUser by remember { mutableStateOf("") }
+ val openDialog = remember { mutableStateOf(false) }
+
+ val offSetX = remember { mutableStateOf(tree.viewCoordinates.first) }
+ val offSetY = remember { mutableStateOf(tree.viewCoordinates.second) }
+
+ tree.reInitAllDrawNodes()
+ tree.viewCoordinates = Pair(offSetX.value, offSetY.value)
+
+ Row(modifier = Modifier.fillMaxSize().background(Color.White).padding(6.dp)) {
+ Column(
+ modifier = Modifier.padding(start = 32.dp, top = 16.dp).width(400.dp)
+ ) {
+ Insert(
+ onClick = { key, value ->
+ if (!Controller.validKey(key)) {
+ textForUser = "Oops.. it's not Int, bro"
+ } else if (key != "") {
+ tree.drawInsert(key, value)
+ tree.reInitAllDrawNodes()
+ textForUser = "I insert node with key: $key and value: $value :)"
+ } else {
+ textForUser = "Give me key pls :("
+ }
+ }
+ )
+ Remove(
+ onClick = { key ->
+ if (key != "") {
+ tree.drawRemove(key)
+ tree.reInitAllDrawNodes()
+ textForUser = "I remove node :)"
+ } else {
+ textForUser = "Give me key pls :("
+ }
+ }
+ )
+ Find(
+ onClick = { key ->
+ val value = tree.drawFind(key)
+ if (value != null) {
+ textForUser = "Result: $value"
+ } else {
+ textForUser = "Ooops... I can't find node :("
+ }
+ }
+ )
+ Spacer(modifier = Modifier.height(15.dp))
+ Text(
+ text = textForUser,
+ modifier = Modifier.padding(start = 32.dp, top = 16.dp),
+ style = MaterialTheme.typography.headlineSmall,
+ )
+ Spacer(modifier = Modifier.height(40.dp))
+ Row(
+ modifier = Modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.aligned(Alignment.End)
+ ) {
+ Button(
+ onClick = onBack,
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.3f),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.onErrorContainer
+ )
+ ) {
+ Text(
+ text = "Exit",
+ )
+ }
+ Spacer(modifier = Modifier.width(16.dp))
+ Button(
+ onClick = {
+ openDialog.value = true
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.3f),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text(
+ text = "Save",
+ )
+ }
+ }
+ Spacer(modifier = Modifier.height(15.dp))
+ Button(
+ onClick = {
+ offSetX.value = 0f
+ offSetY.value = 0f
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.width(400.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text("go to tree Root!")
+ }
+ Button(
+ onClick = {
+ tree.yInterval = -tree.yInterval
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.width(400.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = Color.White
+ )
+ ) {
+ Text("secret", color = Color.White)
+ }
+ }
+ ViewTree().drawTree(tree, offSetX, offSetY)
+ }
+ if (openDialog.value) {
+ AlertDialog(
+ onDismissRequest = { openDialog.value = false },
+ title = {
+ Text(text = "How do you want to save this tree?")
+ },
+ text = {
+ Text("Select the database to save:")
+ },
+ buttons = {
+ Row(
+ modifier = Modifier.padding(12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.aligned(Alignment.CenterHorizontally)
+ ) {
+ Button(
+ onClick = { openDialog.value = false },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.3f).height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text("Exit")
+ }
+ Spacer(modifier = Modifier.width(16.dp))
+ Button(
+ onClick = {
+ tree.saveToDB(Controller.DatabaseType.Json)
+ openDialog.value = false
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.3f).height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text("Json")
+ }
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Button(
+ onClick = {
+ tree.saveToDB(Controller.DatabaseType.SQLite)
+ openDialog.value = false
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.3f).height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text("SQLite")
+ }
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Button(
+ onClick = {
+ tree.saveToDB(Controller.DatabaseType.Neo4j)
+ openDialog.value = false
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.3f).height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text("Neo4j")
+ }
+ }
+ }
+ )
+ }
+}
+
+
+@Composable
+fun Insert(onClick: (key: String, value: String) -> Unit) {
+ Column(modifier = Modifier.padding(start = 32.dp, top = 16.dp).width(300.dp)) {
+ Text(text = "Insert:", style = MaterialTheme.typography.headlineSmall)
+ Spacer(modifier = Modifier.height(7.dp))
+
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ var textKey by remember { mutableStateOf("") }
+ var textValue by remember { mutableStateOf("") }
+
+
+ OutlinedTextField(
+ value = textKey,
+ onValueChange = { textKey = it },
+ label = { Text(text = "key") },
+ singleLine = true,
+ modifier = Modifier.weight(0.30f),
+ shape = MaterialTheme.shapes.extraLarge,
+ )
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ OutlinedTextField(
+ value = textValue,
+ onValueChange = { textValue = it },
+ label = { Text(text = "value") },
+ singleLine = true,
+ modifier = Modifier.weight(0.30f),
+ shape = MaterialTheme.shapes.extraLarge,
+ )
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Button(
+ onClick = {
+ onClick(textKey, textValue)
+ textKey = ""
+ textValue = ""
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.30f).height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text("go!")
+ }
+ }
+ }
+}
+
+@Composable
+fun Remove(onClick: (key: String) -> Unit) {
+ Column(modifier = Modifier.padding(start = 32.dp, top = 16.dp).width(300.dp)) {
+ Text(text = "Remove:", style = MaterialTheme.typography.headlineSmall)
+ Spacer(modifier = Modifier.height(7.dp))
+
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ var textKey by remember { mutableStateOf("") }
+
+ OutlinedTextField(
+ value = textKey,
+ onValueChange = { textKey = it },
+ label = { Text(text = "key") },
+ singleLine = true,
+ modifier = Modifier.weight(0.70f),
+ shape = MaterialTheme.shapes.extraLarge,
+ )
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+
+ Button(
+ onClick = {
+ onClick(textKey)
+ textKey = ""
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.30f).height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text("go!")
+ }
+ }
+ }
+}
+
+@Composable
+fun Find(onClick: (key: String) -> Unit) {
+ Column(modifier = Modifier.padding(start = 32.dp, top = 16.dp).width(300.dp)) {
+ Text(text = "Find:", style = MaterialTheme.typography.headlineSmall)
+ Spacer(modifier = Modifier.height(7.dp))
+
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ var textKey by remember { mutableStateOf("") }
+
+ OutlinedTextField(
+ value = textKey,
+ onValueChange = { textKey = it },
+ label = { Text(text = "key") },
+ singleLine = true,
+ modifier = Modifier.weight(0.70f),
+ shape = MaterialTheme.shapes.extraLarge,
+ )
+
+ Spacer(modifier = Modifier.width(16.dp))
+
+ Button(
+ onClick = {
+ onClick(textKey)
+ textKey = ""
+ },
+ shape = MaterialTheme.shapes.extraLarge,
+ modifier = Modifier.weight(0.30f).height(57.dp),
+ colors = ButtonDefaults.buttonColors(
+ containerColor = MaterialTheme.colorScheme.primary
+ )
+ ) {
+ Text("go!")
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/app/ui/ViewTree.kt b/app/src/main/kotlin/app/ui/ViewTree.kt
new file mode 100644
index 0000000..3e4aaf2
--- /dev/null
+++ b/app/src/main/kotlin/app/ui/ViewTree.kt
@@ -0,0 +1,130 @@
+package app.ui
+
+import androidx.compose.foundation.*
+import androidx.compose.foundation.gestures.detectDragGestures
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.Surface
+import androidx.compose.material.Text
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.zIndex
+import app.controller.Controller
+import kotlin.math.roundToInt
+
+open class ViewTree {
+
+ @Composable
+ open fun drawTree(
+ tree: Controller.DrawTree,
+ offSetX: MutableState,
+ offSetY: MutableState
+ ) {
+ Box(contentAlignment = Alignment.TopCenter,
+ modifier = Modifier
+ .background(MaterialTheme.colorScheme.background)
+ .fillMaxSize()
+ .clipToBounds()
+ .padding(top = 15.dp)
+ .pointerInput(offSetX, offSetY) {
+ detectDragGestures { _, dragAmount ->
+ offSetX.value += dragAmount.x
+ offSetY.value += dragAmount.y
+ }
+ }
+
+ ) {
+ Box(modifier = Modifier.size(50.dp)) {
+ tree.content.value.forEach() { node ->
+ drawNode(node, 50, offSetX, offSetY)
+ node.parent?.let {
+ drawLine(node, it, 50, offSetX, offSetY)
+ }
+ tree.updateCoordinate(node)
+ }
+ }
+ }
+ }
+
+
+ @OptIn(ExperimentalFoundationApi::class)
+ @Composable
+ fun drawNode(
+ node: Controller.DrawNode,
+ size: Int,
+ offSetX: MutableState,
+ offSetY: MutableState
+ ) {
+
+ Box(modifier = Modifier.fillMaxSize()) {
+ TooltipArea(
+ tooltip = {
+ Surface {
+ Text(
+ text = "value: ${node.value}"
+ )
+ }
+ },
+ modifier = Modifier
+ .offset {
+ IntOffset(
+ x = (node.x.value + offSetX.value).roundToInt(),
+ y = (node.y.value + offSetY.value).roundToInt(),
+ )
+ }
+ .pointerInput(node.x, node.y) {
+ detectDragGestures { _, dragAmount ->
+ node.x.value += dragAmount.x
+ node.y.value += dragAmount.y
+ }
+ }
+
+ ) {
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .size(size.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primary)
+ ) {
+ Text(
+ text = node.key,
+ fontSize = 10.sp,
+ color = Color.White,
+ fontWeight = FontWeight.Bold
+ )
+ }
+
+ }
+ }
+ }
+
+ @Composable
+ fun drawLine(
+ node: Controller.DrawNode,
+ parent : Controller.DrawNode,
+ size: Int = 50,
+ offSetX: MutableState,
+ offSetY: MutableState
+ ) {
+ Canvas(modifier = Modifier.fillMaxSize().zIndex(-1f)) {
+ drawLine(
+ color = Color.DarkGray,
+ start = Offset(node.x.value + size/2 + offSetX.value, node.y.value + size/2 + offSetY.value),
+ end = Offset(parent.x.value + size/2 + offSetX.value, parent.y.value + size/2 + offSetY.value),
+ strokeWidth = 3f
+ )
+ }
+ }
+}
diff --git a/app/src/main/kotlin/app/ui/Windows.kt b/app/src/main/kotlin/app/ui/Windows.kt
new file mode 100644
index 0000000..56269f3
--- /dev/null
+++ b/app/src/main/kotlin/app/ui/Windows.kt
@@ -0,0 +1,46 @@
+package app.ui
+
+import androidx.compose.runtime.*
+import androidx.compose.ui.awt.ComposeWindow
+import app.controller.Controller
+
+
+sealed class Screen {
+ object MainWindow: Screen()
+
+ object CreatNewWindow: Screen()
+
+ object OpenTree: Screen()
+
+ data class TreeWindow(val tree: Controller.DrawTree): Screen()
+}
+@Composable
+fun Main(window: ComposeWindow) {
+ var screenState by remember { mutableStateOf(Screen.MainWindow) }
+
+ when (val screen = screenState) {
+ is Screen.MainWindow ->
+ MainWindow(
+ onClickNew = { screenState = Screen.CreatNewWindow },
+ onClickOpen = { screenState = Screen.OpenTree }
+ )
+
+ is Screen.CreatNewWindow ->
+ CreatNewTree(
+ onBack = {screenState = Screen.MainWindow },
+ onClick = { screenState = Screen.TreeWindow(tree = it) }
+ )
+
+ is Screen.OpenTree ->
+ OpenTree(
+ onBack = {screenState = Screen.MainWindow },
+ onClick = { screenState = Screen.TreeWindow(tree = it) }
+ )
+
+ is Screen.TreeWindow ->
+ Tree(
+ onBack = {screenState = Screen.MainWindow },
+ tree = screen.tree
+ )
+ }
+}
diff --git a/app/src/main/kotlin/dataBase/DataBase.kt b/app/src/main/kotlin/dataBase/DataBase.kt
new file mode 100644
index 0000000..3c4666f
--- /dev/null
+++ b/app/src/main/kotlin/dataBase/DataBase.kt
@@ -0,0 +1,44 @@
+package dataBase
+
+import trees.*
+import java.io.IOException
+
+interface DataBase {
+ fun isSupportTreeType(tree: BinTree<*, *>) = when (tree) {
+ is BSTree,
+ is RBTree,
+ is AVLTree -> true
+ else -> false
+ }
+
+ fun typeToTree(type: String): BinTree>> = when (type) {
+ BSTree::class.simpleName -> BSTree()
+ RBTree::class.simpleName -> RBTree()
+ AVLTree::class.simpleName -> AVLTree()
+ else -> throw IOException("invalid type of tree")
+ }
+
+ fun validateName(name: String) {
+ for (i in name)
+ if (i !in 'a'..'z' && i !in 'A'..'Z' && i !in '0'..'9')
+ throw IllegalArgumentException("Unsupported tree name, please use only ascii letters or digits")
+ if (name[0] in '0'..'9')
+ throw IllegalArgumentException("Unsupported tree name, please don't use a digit as the first char")
+ if (name.isEmpty()) throw IllegalArgumentException("Incorrect tree name")
+ }
+
+ fun saveTree(
+ treeName: String,
+ tree: BinTree>>,
+ viewCoordinates: Pair
+ )
+ fun readTree(treeName: String): Pair>>, Pair>
+ fun removeTree(treeName: String)
+ fun getAllTrees(): List>>
+ fun clean()
+ fun close()
+ fun cleanAndClose() {
+ clean()
+ close()
+ }
+}
diff --git a/app/src/main/kotlin/dataBase/Json.kt b/app/src/main/kotlin/dataBase/Json.kt
new file mode 100644
index 0000000..01a8348
--- /dev/null
+++ b/app/src/main/kotlin/dataBase/Json.kt
@@ -0,0 +1,96 @@
+package dataBase
+
+import trees.BinTree
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import java.io.File
+import java.io.IOException
+import java.nio.file.Files
+import java.nio.file.Paths
+import kotlin.io.path.extension
+
+class Json(private val saveDirPath: String) : DataBase {
+ private val mapper = jacksonObjectMapper()
+
+ init {
+ if (saveDirPath.last() == '\\' || saveDirPath.last() == '/') throw IllegalArgumentException("Please, don't use '/' or '\\' in the end of dir path")
+ File(saveDirPath).mkdirs()
+ }
+
+ private fun getFile(treeName: String) = try {
+ File("${saveDirPath}/${treeName}.json")
+ } catch (ex: Exception) {
+ throw IOException("cannot get file with name: ${saveDirPath}/${treeName}.json\n$ex")
+ }
+
+ override fun saveTree(
+ treeName: String,
+ tree: BinTree>>,
+ viewCoordinates: Pair
+ ) {
+ if (!isSupportTreeType(tree)) throw IllegalArgumentException("Unsupported tree type")
+ validateName(treeName)
+
+ removeTree(treeName)
+
+
+ val jsonFile = getFile(treeName)
+ jsonFile.createNewFile()
+
+ jsonFile.appendText(
+ mapper.writeValueAsString(
+ Pair(
+ Triple(treeName, tree::class.simpleName, viewCoordinates),
+ tree.getKeyValueList()
+ )
+ )
+ )
+ }
+
+ override fun readTree(treeName: String): Pair>>, Pair> {
+ validateName(treeName)
+
+ val jsonFile = getFile(treeName)
+
+ val readTree =
+ mapper.readValue>, Array>>>>>(
+ jsonFile
+ )
+ val tree = typeToTree(readTree.first.second)
+ tree.insert(*readTree.second)
+ return Pair(tree, readTree.first.third)
+ }
+
+ override fun removeTree(treeName: String) {
+ validateName(treeName)
+
+ getFile(treeName).delete()
+ }
+
+ private fun forAllJsonFile(function: (File) -> Unit) {
+ Files.walk(Paths.get(saveDirPath)).use { path ->
+ path.filter { Files.isRegularFile(it) && Files.isWritable(it) && (it.extension == "json") }
+ .forEach { function(it.toFile()) }
+ }
+ }
+
+ override fun getAllTrees(): MutableList>> {
+ val list = mutableListOf>>()
+ forAllJsonFile {
+ list.add(
+ mapper.readValue>, Array>>>>>(
+ it
+ ).first
+ )
+ }
+
+ return list
+ }
+
+ override fun clean() {
+ forAllJsonFile { it.delete() }
+ }
+
+ override fun close() {
+ }
+}
diff --git a/app/src/main/kotlin/dataBase/Neo4j.kt b/app/src/main/kotlin/dataBase/Neo4j.kt
new file mode 100644
index 0000000..47982ac
--- /dev/null
+++ b/app/src/main/kotlin/dataBase/Neo4j.kt
@@ -0,0 +1,172 @@
+package dataBase
+
+import trees.BinTree
+import org.neo4j.driver.AuthTokens
+import org.neo4j.driver.Driver
+import org.neo4j.driver.GraphDatabase
+import org.neo4j.driver.Session
+import org.neo4j.driver.exceptions.ServiceUnavailableException
+import org.neo4j.driver.exceptions.value.Uncoercible
+import java.io.IOException
+
+class Neo4j(uri: String, user: String, password: String) : DataBase {
+ private var driver: Driver
+ private var session: Session
+
+ init {
+ try {
+ driver = GraphDatabase.driver(uri, AuthTokens.basic(user, password))
+ driver.verifyConnectivity()
+ session = driver.session()
+ } catch (ex: Exception) {
+ throw IOException("can't start session, try to change uri, user name or password\n$ex")
+ }
+ }
+
+ private fun executeQuery(query: String) {
+ try {
+ session.run(query)
+ } catch (ex: ServiceUnavailableException) {
+ throw IOException(
+ "Cannot connect to Neo4j database\n" +
+ "Check that Neo4j is running and that all the data in the app/src/main/resources/Neo4j.properties file is correct\n" +
+ "$ex"
+ )
+ }
+ }
+
+ override fun saveTree(
+ treeName: String,
+ tree: BinTree>>,
+ viewCoordinates: Pair
+ ) {
+ if (!isSupportTreeType(tree)) throw IllegalArgumentException("Unsupported tree type")
+ validateName(treeName)
+
+ removeTree(treeName)
+ addTreeNode(treeName, tree, viewCoordinates)
+ var prevKey: Int? = null
+ tree.getKeyValueList()
+ .forEach { saveNode(it.first, it.second.first, it.second.second, prevKey, treeName); prevKey = it.first }
+ }
+
+ private fun addTreeNode(
+ treeName: String,
+ tree: BinTree>>,
+ coordinates: Pair
+ ) {
+ session.executeWrite { tx ->
+ tx.run(
+ "CREATE (:Tree {name: \$name, type: \$type, " +
+ "viewX: \$x, viewY: \$y})",
+ mutableMapOf(
+ "name" to treeName,
+ "type" to tree::class.simpleName,
+ "x" to coordinates.first,
+ "y" to coordinates.second
+ ) as Map
+ )
+ }
+ }
+
+ private fun saveNode(
+ key: Int,
+ value: String,
+ coordinate: Pair,
+ prevKey: Int?,
+ treeName: String
+ ) {
+ session.executeWrite { tx ->
+ tx.run(
+ "OPTIONAL MATCH (prevNode:${if (prevKey == null) "Tree WHERE prevNode.name = '$treeName'" else "${treeName}Node WHERE prevNode.key = $prevKey"}) " +
+ "CREATE (prevNode)-[:next]->(b:${treeName}Node {key:\$key, value:\$value, x:\$x, y:\$y})",
+ mutableMapOf(
+ "key" to key,
+ "value" to value,
+ "x" to coordinate.first,
+ "y" to coordinate.second
+ ) as Map
+ )
+ }
+ }
+
+ override fun readTree(treeName: String): Pair>>, Pair> {
+ validateName(treeName)
+
+ var type = ""
+ var viewCoordinates = Pair(0F, 0F)
+ session.executeRead { tx ->
+ val result = tx.run("OPTIONAL MATCH (tree: Tree WHERE tree.name = '$treeName') RETURN tree.type AS type, tree.viewX AS x, tree.viewY AS y").single()
+ try {
+ type = result["type"].asString()
+ viewCoordinates = Pair(result["x"].asFloat(), result["y"].asFloat())
+ } catch (ex: Uncoercible) {
+ throw IOException("Corrupted data in the database.\nPossible solution: Clear the data.\n$ex")
+ } catch (ex: Exception) {
+ throw IOException("Cannot get or recognise data\n$ex")
+ }
+ }
+
+ val tree = typeToTree(type)
+
+ session.executeRead { tx ->
+ val result = tx.run(
+ "OPTIONAL MATCH (tree: Tree WHERE tree.name = '$treeName')-[n:next*]->(node) RETURN node.key AS key, node.value AS value, node.x AS x, node.y AS y ORDER BY n"
+ )
+
+ result.stream().forEach {
+ try {
+ tree.insert(
+ it["key"].asInt(),
+ Pair(
+ it["value"].asString(),
+ Pair(it["x"].asFloat(), it["y"].asFloat())
+ )
+ )
+ } catch (ex: Uncoercible) {
+ throw IOException("Corrupted data in the database.\nPossible solution: Clear the data.\n$ex")
+ } catch (ex: Exception) {
+ throw IOException("Cannot get or recognise data\n$ex")
+ }
+ }
+ }
+ return Pair(tree, viewCoordinates)
+ }
+
+ override fun removeTree(treeName: String) {
+ validateName(treeName)
+
+ executeQuery("OPTIONAL MATCH (tree: Tree WHERE tree.name = '$treeName')-[:next*]->(node) DETACH DELETE node, tree")
+ }
+
+ override fun getAllTrees(): List>> {
+ val list = mutableListOf>>()
+ try {
+ session.executeRead { tx ->
+ val result =
+ tx.run("OPTIONAL MATCH (tree: Tree) RETURN tree.name AS name, tree.type AS type, tree.viewX AS x, tree.viewY AS y")
+ result.stream().forEach {
+ list.add(
+ Triple(
+ it["name"].asString(),
+ it["type"].asString(),
+ Pair(it["x"].asFloat(), it["y"].asFloat())
+ )
+ )
+ }
+ }
+ } catch (ex: Exception) {
+ throw IOException("Cannot get trees from Neo4j\nCheck that the database is active and all data is entered correctly\n$ex")
+ }
+ return list
+ }
+
+ override fun close() {
+ session.close()
+ driver.close()
+ }
+
+ override fun clean() {
+ executeQuery("MATCH (n) DETACH DELETE n")
+ }
+}
diff --git a/app/src/main/kotlin/dataBase/SQLite.kt b/app/src/main/kotlin/dataBase/SQLite.kt
new file mode 100644
index 0000000..a496495
--- /dev/null
+++ b/app/src/main/kotlin/dataBase/SQLite.kt
@@ -0,0 +1,202 @@
+package dataBase
+
+import trees.BinTree
+import java.io.File
+import java.io.IOException
+import java.sql.DriverManager
+import java.sql.PreparedStatement
+import java.sql.SQLException
+
+class SQLite(dbPath: String, val maxStringLen: UInt) : DataBase {
+ companion object {
+ private const val DB_DRIVER = "jdbc:sqlite"
+ }
+
+ private val connection = try {
+ DriverManager.getConnection("$DB_DRIVER:$dbPath")
+ } catch (ex: Exception) {
+ throw SQLException("Cannot connect to database\nCheck that it is running and that there is no error in the path to it\n$ex")
+ }
+ ?: throw SQLException("Cannot connect to database\nCheck that it is running and that there is no error in the path to it")
+ private val addTreeStatement by lazy { connection.prepareStatement("INSERT INTO trees (name, type, viewX, viewY) VALUES (?, ?, ?, ?);") }
+ private val getAllTreesStatement by lazy { connection.prepareStatement("SELECT trees.name as name, trees.type as type, trees.viewX as x, trees.viewY as y FROM trees;") }
+
+ init {
+ File(dbPath).mkdirs()
+ createTreesTable()
+ }
+
+ override fun saveTree(
+ treeName: String,
+ tree: BinTree>>,
+ viewCoordinates: Pair
+ ) {
+ if (!isSupportTreeType(tree)) throw IllegalArgumentException("Unsupported tree type")
+ validateName(treeName)
+
+ removeTree(treeName)
+ createTableForTree(treeName)
+ addTree(
+ treeName,
+ tree::class.simpleName ?: throw IllegalArgumentException("Cannot get tree type"),
+ viewCoordinates
+ )
+
+ val addNodeStatement by lazy { connection.prepareStatement("INSERT INTO ${treeName}Nodes (key, value, x, y) VALUES (?, ?, ?, ?);") }
+ tree.getKeyValueList()
+ .forEach { saveNode(it.first, it.second.first, it.second.second, treeName, addNodeStatement) }
+ addNodeStatement.close()
+ }
+
+ private fun saveNode(
+ key: Int,
+ value: String,
+ coordinate: Pair,
+ treeName: String,
+ addNodeStatement: PreparedStatement
+ ) {
+ try {
+ addNodeStatement.setInt(1, key)
+ addNodeStatement.setString(2, value)
+ addNodeStatement.setFloat(3, coordinate.first)
+ addNodeStatement.setFloat(4, coordinate.second)
+
+ addNodeStatement.execute()
+ } catch (ex: Exception) {
+ throw SQLException("Cannot add node with key: \"${key}\" in tree: $treeName")
+ }
+ }
+
+ private fun createTreesTable() {
+ try {
+ executeQuery(
+ "CREATE TABLE if not exists trees (treeId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
+ "name varchar($maxStringLen), " +
+ "type varchar($maxStringLen), " +
+ "viewX FLOAT, " +
+ "viewY FLOAT);"
+ )
+ } catch (ex: Exception) {
+ throw SQLException("Cannot create table in database\n$ex")
+ }
+ }
+
+ private fun createTableForTree(treeName: String) {
+ validateName(treeName)
+
+ try {
+ executeQuery(
+ "CREATE TABLE if not exists ${treeName}Nodes (nodeId INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
+ "key INTEGER, " +
+ "value varchar($maxStringLen), " +
+ "x FLOAT, " +
+ "y FLOAT);"
+ )
+ } catch (ex: Exception) {
+ throw SQLException("Cannot create table in database\n$ex")
+ }
+ }
+
+ private fun executeQuery(query: String) {
+ connection.createStatement().also { stmt ->
+ try {
+ stmt.execute(query)
+ } catch (ex: Exception) {
+ throw SQLException("Cannot execute query: \"$query\"\n$ex")
+ } finally {
+ stmt.close()
+ }
+ }
+ }
+
+ private fun addTree(treeName: String, treeType: String, viewCoordinates: Pair) {
+ try {
+ addTreeStatement.setString(1, treeName)
+ addTreeStatement.setString(2, treeType)
+ addTreeStatement.setFloat(3, viewCoordinates.first)
+ addTreeStatement.setFloat(4, viewCoordinates.second)
+
+ addTreeStatement.execute()
+ } catch (ex: Exception) {
+ throw SQLException("Cannot add tree: $treeName\n$ex")
+ }
+ }
+
+ private fun getTreeData(treeName: String): Pair> {
+ val getTreeTypeStatement by lazy { connection.prepareStatement("SELECT trees.type as type, trees.viewX as x, trees.viewY as y FROM trees WHERE name = ?") }
+ getTreeTypeStatement.setString(1, treeName)
+ try {
+ val data = getTreeTypeStatement.executeQuery()
+ return Pair(data.getString("type"), Pair(data.getFloat("x"), data.getFloat("y")))
+ } catch (ex: Exception) {
+ throw SQLException("Cannot get tree type from database\n$ex")
+ } finally {
+ getTreeTypeStatement.close()
+ }
+
+ }
+
+ override fun readTree(treeName: String): Pair>>, Pair> {
+ validateName(treeName)
+
+ val nodes = "${treeName}Nodes"
+ val getAllNodesStatement by lazy { connection.prepareStatement("SELECT $nodes.key as key, $nodes.value as value, $nodes.x as x, $nodes.y as y FROM $nodes;") }
+
+ val treeData = getTreeData(treeName)
+ val tree = typeToTree(treeData.first)
+
+ try {
+ val nodesSet = getAllNodesStatement.executeQuery()
+ while (nodesSet.next()) {
+ tree.insert(
+ nodesSet.getInt("key"),
+ Pair(
+ nodesSet.getString("value"),
+ Pair(nodesSet.getFloat("x"), nodesSet.getFloat("y"))
+ )
+ )
+ }
+ } catch (ex: Exception) {
+ throw IOException("Cannot get nodes from database\n$ex")
+ } finally {
+ getAllNodesStatement.close()
+ }
+
+ return Pair(tree, treeData.second)
+ }
+
+ override fun removeTree(treeName: String) {
+ validateName(treeName)
+
+ executeQuery("DROP TABLE IF EXISTS ${treeName}Nodes;")
+
+ executeQuery("DELETE FROM trees WHERE name = '$treeName';")
+ }
+
+ override fun getAllTrees(): List>> {
+ val list = mutableListOf>>()
+ val treesSet = getAllTreesStatement.executeQuery()
+ while (treesSet.next()) {
+ list.add(
+ Triple(
+ treesSet.getString("name"),
+ treesSet.getString("type"),
+ Pair(treesSet.getFloat("x"), treesSet.getFloat("y"))
+ )
+ )
+ }
+
+ return list
+ }
+
+ override fun close() {
+ addTreeStatement.close()
+ getAllTreesStatement.close()
+ connection.close()
+ }
+
+ override fun clean() {
+ for (i in getAllTrees())
+ removeTree(i.first)
+ }
+}
diff --git a/app/src/main/resources/App.properties b/app/src/main/resources/App.properties
new file mode 100644
index 0000000..1e392f5
--- /dev/null
+++ b/app/src/main/resources/App.properties
@@ -0,0 +1 @@
+max_string_len = 255
diff --git a/app/src/main/resources/Json.properties b/app/src/main/resources/Json.properties
new file mode 100644
index 0000000..971fe70
--- /dev/null
+++ b/app/src/main/resources/Json.properties
@@ -0,0 +1 @@
+json_save_dir = JsonSave
diff --git a/app/src/main/resources/Neo4j.properties b/app/src/main/resources/Neo4j.properties
new file mode 100644
index 0000000..cd28511
--- /dev/null
+++ b/app/src/main/resources/Neo4j.properties
@@ -0,0 +1,3 @@
+neo4j_uri = bolt://localhost:7687
+neo4j_user = neo4j
+neo4j_password = 12345678
diff --git a/app/src/main/resources/SQLite.properties b/app/src/main/resources/SQLite.properties
new file mode 100644
index 0000000..2a938c2
--- /dev/null
+++ b/app/src/main/resources/SQLite.properties
@@ -0,0 +1 @@
+sqlite_path = SQLDB/sqliteTreeStorage.db
diff --git a/app/src/main/resources/icon.png b/app/src/main/resources/icon.png
new file mode 100644
index 0000000..6b4147a
Binary files /dev/null and b/app/src/main/resources/icon.png differ
diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
new file mode 100644
index 0000000..8bfe7be
--- /dev/null
+++ b/build-logic/build.gradle.kts
@@ -0,0 +1,11 @@
+plugins {
+ `kotlin-dsl`
+}
+
+repositories {
+ gradlePluginPortal()
+}
+
+dependencies {
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10")
+}
diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts
new file mode 100644
index 0000000..d1a9718
--- /dev/null
+++ b/build-logic/settings.gradle.kts
@@ -0,0 +1 @@
+rootProject.name = "trees-build-logic"
diff --git a/build-logic/src/main/kotlin/trees.kotlin-application-conventions.gradle.kts b/build-logic/src/main/kotlin/trees.kotlin-application-conventions.gradle.kts
new file mode 100644
index 0000000..d0520c8
--- /dev/null
+++ b/build-logic/src/main/kotlin/trees.kotlin-application-conventions.gradle.kts
@@ -0,0 +1,5 @@
+plugins {
+ id("trees.kotlin-common-conventions")
+
+ application
+}
diff --git a/build-logic/src/main/kotlin/trees.kotlin-common-conventions.gradle.kts b/build-logic/src/main/kotlin/trees.kotlin-common-conventions.gradle.kts
new file mode 100644
index 0000000..06c11bd
--- /dev/null
+++ b/build-logic/src/main/kotlin/trees.kotlin-common-conventions.gradle.kts
@@ -0,0 +1,17 @@
+plugins {
+ id("org.jetbrains.kotlin.jvm")
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ testImplementation("org.jetbrains.kotlin:kotlin-test:1.8.10")
+ testImplementation(platform("org.junit:junit-bom:5.9.2"))
+ testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
+}
+
+tasks.test {
+ useJUnitPlatform()
+}
diff --git a/build-logic/src/main/kotlin/trees.kotlin-library-conventions.gradle.kts b/build-logic/src/main/kotlin/trees.kotlin-library-conventions.gradle.kts
new file mode 100644
index 0000000..a29f85b
--- /dev/null
+++ b/build-logic/src/main/kotlin/trees.kotlin-library-conventions.gradle.kts
@@ -0,0 +1,5 @@
+plugins {
+ id("trees.kotlin-common-conventions")
+
+ `java-library`
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..1319bf0
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,2 @@
+kotlin.code.style=official
+sqliteJdbcVersion=3.41.2.1
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..249e583
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..ae04661
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..a69d9cb
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,240 @@
+#!/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 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/master/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
+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
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${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='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+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 ;; #(
+ 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
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+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.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# 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.
+
+# 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" )
+
+ 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
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+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" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100755
index 0000000..f127cfd
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,91 @@
+@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
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+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="-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% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+: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 %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+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!
+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
+
+:omega
diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts
new file mode 100644
index 0000000..b583248
--- /dev/null
+++ b/lib/build.gradle.kts
@@ -0,0 +1,3 @@
+plugins {
+ id("trees.kotlin-library-conventions")
+}
diff --git a/lib/src/main/kotlin/trees/AVLTree.kt b/lib/src/main/kotlin/trees/AVLTree.kt
new file mode 100644
index 0000000..77ef68e
--- /dev/null
+++ b/lib/src/main/kotlin/trees/AVLTree.kt
@@ -0,0 +1,95 @@
+package trees
+
+open class AVLTree, Value> : BalanceTree {
+ protected class AVLNode, Value>(
+ key: Key,
+ value: Value,
+ var height: UByte = 0U
+ ) : BinNode(key, value)
+
+ constructor() : super()
+ constructor(key: Key, value: Value) : super(key, value)
+ constructor(vararg pairs: Pair) : super(pairs)
+
+ override fun insert(key: Key, value: Value) {
+ //if the tree is too big, we can't insert something
+ rootNode?.let { if ((rootNode as AVLNode).height == 255.toUByte()) return }
+
+ val node = insertService(AVLNode(key, value)) as AVLNode?
+ balancing(node ?: return)
+ }
+
+ override fun remove(key: Key) {
+ val removeNode = getNode(key) ?: return
+
+ //special case when the node has two children
+ if ((removeNode.right != null) && (removeNode.left != null)) {
+ val node = nextElement(removeNode) as AVLNode
+
+ if (node.parent == removeNode) {
+ removeService(removeNode)
+ balancing(node)
+ } else {
+ val nextNodeParent = node.parent as AVLNode
+ removeService(removeNode)
+ balancing(nextNodeParent)
+ }
+ }
+
+ //when the node has zero or one child, just remove it and balance the tree
+ else {
+ removeService(removeNode)
+ if (removeNode.parent != null) balancing(removeNode.parent as AVLNode)
+ }
+
+ }
+
+ //use tail recursion to balance after removing and inserting a node
+ private tailrec fun balancing(node: AVLNode): AVLNode {
+ var currentNode = node
+ updateHeight(currentNode)
+
+ //if the balancing factor by module is greater than one,
+ //it is necessary to do the balancing
+ if (balanceFactor(currentNode) >= 2) {
+ if (balanceFactor(currentNode.right as AVLNode) >= 0) {
+ currentNode = rotation(currentNode, RotationType.LEFT) as AVLNode
+ updateHeightAfterRotation(currentNode.left as AVLNode?)
+ } else {
+ currentNode = rotation(currentNode.right, RotationType.RIGHT) as AVLNode
+ updateHeightAfterRotation(currentNode.right as AVLNode?)
+ currentNode = rotation(currentNode.parent, RotationType.LEFT) as AVLNode
+ updateHeightAfterRotation(currentNode.left as AVLNode?)
+ }
+ } else if (balanceFactor(currentNode) <= -2) {
+ if (balanceFactor(currentNode.left as AVLNode) <= 0) {
+ currentNode = rotation(currentNode, RotationType.RIGHT) as AVLNode
+ updateHeightAfterRotation(currentNode.right as AVLNode?)
+ } else {
+ currentNode = rotation(currentNode.left, RotationType.LEFT) as AVLNode
+ updateHeightAfterRotation(currentNode.left as AVLNode?)
+ currentNode = rotation(currentNode.parent, RotationType.RIGHT) as AVLNode
+ updateHeightAfterRotation(currentNode.right as AVLNode?)
+ }
+ }
+ (currentNode.parent as AVLNode?)?.let { return balancing(it) }
+ return currentNode
+ }
+
+ private fun updateHeight(node: AVLNode) {
+ val left = node.left?.let { (it as AVLNode).height } ?: 0U
+ val right = node.right?.let { (it as AVLNode).height } ?: 0U
+ node.height = (maxOf(left, right) + 1U).toUByte()
+ }
+
+ private fun updateHeightAfterRotation(node: AVLNode?) {
+ node?.let { updateHeight(it) } ?: 0U
+ node?.parent?.let { updateHeight(it as AVLNode) } ?: 0U
+ }
+
+ private fun balanceFactor(node: AVLNode): Int {
+ val left = node.left?.let { (it as AVLNode).height.toInt() } ?: 0
+ val right = node.right?.let { (it as AVLNode).height.toInt() } ?: 0
+ return (right - left)
+ }
+}
diff --git a/lib/src/main/kotlin/trees/BSTree.kt b/lib/src/main/kotlin/trees/BSTree.kt
new file mode 100644
index 0000000..6216dfe
--- /dev/null
+++ b/lib/src/main/kotlin/trees/BSTree.kt
@@ -0,0 +1,16 @@
+package trees
+
+open class BSTree, Value> : BinTree {
+ constructor() : super()
+ constructor(key: Key, value: Value) : super(key, value)
+ constructor(vararg pairs: Pair) : super(pairs)
+
+ override fun insert(key: Key, value: Value) {
+ insertService(BinNode(key, value))
+ }
+
+ override fun remove(key: Key) {
+ val node = getNode(key) ?: return
+ removeService(node)
+ }
+}
diff --git a/lib/src/main/kotlin/trees/BalanceTree.kt b/lib/src/main/kotlin/trees/BalanceTree.kt
new file mode 100644
index 0000000..de09d82
--- /dev/null
+++ b/lib/src/main/kotlin/trees/BalanceTree.kt
@@ -0,0 +1,35 @@
+package trees
+
+abstract class BalanceTree, Value> : BinTree {
+ constructor() : super()
+ constructor(key: Key, value: Value) : super(key, value)
+ constructor(pairs: Array>) : super(pairs)
+
+ enum class RotationType { LEFT, RIGHT }
+
+ protected fun rotation(parent: BinNode?, type: RotationType): BinNode? {
+ //giving the parentNode
+ parent?.let {
+ val node = if (type == RotationType.LEFT) it.right ?: error("rotation is not possible")
+ else it.left ?: error("rotation is not possible")
+
+ when (type) {
+ RotationType.LEFT -> {
+ it.right = node.left
+ node.left?.parent = it
+ node.left = it
+ }
+
+ RotationType.RIGHT -> {
+ it.left = node.right
+ node.right?.parent = it
+ node.right = it
+ }
+ }
+ replaceNodeParent(it, node)
+ it.parent = node
+ return node
+ }
+ return null
+ }
+}
diff --git a/lib/src/main/kotlin/trees/BinTree.kt b/lib/src/main/kotlin/trees/BinTree.kt
new file mode 100644
index 0000000..b3499f9
--- /dev/null
+++ b/lib/src/main/kotlin/trees/BinTree.kt
@@ -0,0 +1,292 @@
+package trees
+
+import java.util.LinkedList
+import java.util.Queue
+import kotlin.math.abs
+
+abstract class BinTree, Value> : Tree {
+ protected open class BinNode, Value>(
+ var key: Key,
+ var value: Value,
+ var parent: BinNode? = null,
+ var left: BinNode? = null,
+ var right: BinNode? = null
+ ) : Comparable {
+ override fun compareTo(other: Key): Int {
+ return key.compareTo(other)
+ }
+
+ fun equalKey(other: Key): Boolean {
+ return this.compareTo(other) == 0
+ }
+ }
+
+
+ protected open var rootNode: BinNode? = null
+
+ constructor()
+
+ /**
+ * creates tree with one node, where node have these key, value
+ */
+ constructor(key: Key, value: Value) {
+ insert(key, value)
+ }
+
+ /**
+ * creates tree with nodes
+ *
+ * @param sort if this param = true, then insert the nodes starting from the average value of the keys
+ */
+ constructor(array: Array>, sort: Boolean = false) {
+ if (sort) sortInsert(*array)
+ else insert(*array)
+ }
+
+ /**
+ * inserts the nodes starting from the average value of the keys
+ */
+ fun sortInsert(vararg array: Pair) {
+ val serArray = array.sortedBy { it.first }.toTypedArray()
+ var indices = serArray.indices.toList()
+ indices = indices.sortedBy { abs(serArray.size / 2 - it) }
+ for (i in indices) {
+ insert(serArray[i].first, serArray[i].second)
+ }
+ }
+
+ override fun insert(vararg array: Pair) {
+ for (i in array) insert(i.first, i.second)
+ }
+
+ override fun remove(vararg keys: Key) {
+ for (i in keys) remove(i)
+ }
+
+ /**
+ * @return the inserted node if the node with the same key wasn't in the tree and null in otherwise
+ *
+ * doesn't balance the tree
+ */
+ protected fun insertService(node: BinNode): BinNode? {
+ if (rootNode == null) {
+ rootNode = node
+ return node
+ } else {
+ val parent = getParent(node.key)
+ if (parent != null) {
+ if (parent < node.key) if (parent.right == null) {
+ node.parent = parent
+ parent.right = node
+ return node
+ } else parent.right?.value = node.value ?: error("unexpected null")
+ else if (parent.left == null) {
+ node.parent = parent
+ parent.left = node
+ return node
+ } else (parent.left)?.value = node.value ?: error("unexpected null")
+ } else rootNode?.value = node.value ?: error("unexpected null")
+ }
+ return null
+ }
+
+ protected fun removeService(node: BinNode) {
+ if ((node.left == null) && (node.right == null)) {
+ val parent: BinNode? = node.parent
+ if (parent == null) rootNode = null
+ else if (node == parent.left) parent.left = null
+ else parent.right = null
+ } else if (node.left == null) replaceNodeParent(node, node.right ?: error("remove error: unexpected null"))
+ else if (node.right == null) replaceNodeParent(node, node.left ?: error("remove error: unexpected null"))
+ else {
+ val nextNode = nextElement(node) ?: error("remove error: unexpected null")
+ val parent = nextNode.parent ?: error("remove error: unexpected null")
+ if (parent != node) {
+ if (nextNode.right != null) replaceNodeParent(nextNode, nextNode.right)
+ else parent.left = null
+ nextNode.right = node.right
+ nextNode.right?.parent = nextNode
+ }
+ nextNode.left = node.left
+ nextNode.left?.parent = nextNode
+ replaceNodeParent(node, nextNode)
+ }
+ }
+
+ protected fun getParent(key: Key): BinNode? {
+ tailrec fun recFind(curNode: BinNode?): BinNode? {
+ return if (curNode == null) null
+ else if (curNode > key) {
+ if (curNode.left?.equalKey(key) != false) curNode
+ else recFind(curNode.left)
+ } else if (curNode.equalKey(key)) {
+ return curNode.parent
+ } else {
+ if (curNode.right?.equalKey(key) != false) curNode
+ else recFind(curNode.right)
+ }
+ }
+ return recFind(rootNode)
+ }
+
+ protected fun getNode(key: Key): BinNode? {
+ if (rootNode?.equalKey(key) == true) return rootNode
+ val parent = getParent(key)
+ return if (parent == null) null
+ else if (parent.left?.equalKey(key) == true) parent.left
+ else if (parent.right?.equalKey(key) == true) parent.right
+ else null
+ }
+
+ override fun get(key: Key): Value? {
+ return getNode(key)?.value
+ }
+
+ override fun get(vararg keys: Key): List {
+ return List(keys.size) { get(keys[it]) }
+ }
+
+ protected open fun nextElement(node: BinNode): BinNode? {
+ val nodeRight: BinNode = node.right ?: return null
+ return minElement(nodeRight.key)
+ }
+
+ protected fun minElement(key: Key): BinNode? {
+ var minNode: BinNode? = getNode(key) ?: return null
+ while (minNode?.left != null) {
+ minNode = minNode.left ?: error("min element not found: unexpected null")
+ }
+ return minNode
+ }
+
+ protected fun maxElement(key: Key): BinNode? {
+ var maxNode: BinNode? = getNode(key) ?: return null
+ while (maxNode?.right != null) {
+ maxNode = maxNode.right ?: error("max element not found: unexpected null")
+ }
+ return maxNode
+ }
+
+ protected open fun replaceNodeParent(oldNode: BinNode, newNode: BinNode?) {
+ val parent: BinNode? = oldNode.parent
+ if (parent == null) rootNode = newNode
+ else if (oldNode == parent.left) {
+ parent.left = newNode
+ } else {
+ parent.right = newNode
+ }
+ newNode?.let { it.parent = parent }
+ }
+
+ /**
+ * removes all nodes from the tree
+ */
+ fun clean() {
+ rootNode = null
+ }
+
+ protected fun breadthFirstSearch(addNullNodes: Boolean = false, function: (BinNode?) -> Unit) {
+ val queue: Queue?> = LinkedList(listOf(rootNode))
+
+ fun notNullInQueue(): Boolean {
+ for (i in queue) if (i != null) return true
+ return false
+ }
+
+ while (queue.isNotEmpty()) {
+ val node = queue.remove()
+ function(node)
+ if (node != null) {
+ queue.add(node.left)
+ queue.add(node.right)
+ } else if (addNullNodes) {
+ queue.add(null)
+ queue.add(null)
+ }
+ if (!notNullInQueue()) return
+ }
+ }
+
+ /**
+ * @return all key, value of all nodes in the tree.
+ * In order from left to right, by level.
+ */
+ fun getKeyValueList(): List> {
+ val list = mutableListOf>()
+ breadthFirstSearch { node -> if (node != null) list.add(Pair(node.key, node.value)) }
+ return list
+ }
+
+ fun getParentData(key: Key): Pair? {
+ val parent = getParent(key)
+ return if (parent != null)
+ Pair(parent.key, parent.value)
+ else null
+ }
+
+ /**
+ * @return all key, value of all nodes in the tree with value of its parent (null if parent doesn't exist).
+ * In order from left to right, by level.
+ */
+ fun getNodesDataWithParentKeys(): MutableList> {
+ val list = mutableListOf>()
+ breadthFirstSearch { node -> if (node != null) list.add(Triple(node.key, node.value, node.parent?.key)) }
+ return list
+ }
+
+ /**
+ * changes value of all nodes
+ * in order from left to right, by level.
+ *
+ * @param addNullNodes adds null nodes to all places where nodes do not exist, so that each node has two children
+ */
+ fun rewriteAllValue(addNullNodes: Boolean = false, function: (Value?, Int, Int) -> Value?) {
+ val listOfAllNodes = mutableListOf?>>()
+ var listOfLevel = mutableListOf?>()
+ var sizeOfLevel = 1
+ var elemInTheLevel = 0
+ breadthFirstSearch(addNullNodes) { node ->
+ listOfLevel.add(node)
+ elemInTheLevel += 1
+
+ if (elemInTheLevel == sizeOfLevel) {
+ listOfAllNodes.add(listOfLevel)
+ sizeOfLevel *= 2
+ elemInTheLevel = 0
+ listOfLevel = mutableListOf()
+ }
+ }
+ if (listOfLevel.isNotEmpty())
+ listOfAllNodes.add(listOfLevel)
+
+ var curLevel = 0
+ val height = listOfAllNodes.size
+ listOfAllNodes.forEach {
+ it.forEach { node ->
+ val value: Value? = function(node?.value, curLevel, height)
+ if (value != null) node?.value = value
+ }
+ curLevel++
+ }
+ }
+
+ internal open inner class Debug {
+ fun treeKeysInString(): String {
+ var sizeOfLevel = 1
+ var elemInTheLevel = 0
+ var string = ""
+
+ breadthFirstSearch(true) { node ->
+ string += node?.key ?: "-"
+ string += " "
+ elemInTheLevel += 1
+ if (elemInTheLevel == sizeOfLevel) {
+ sizeOfLevel *= 2
+ elemInTheLevel = 0
+ string += "\n"
+ }
+ }
+ return string
+ }
+ }
+}
diff --git a/lib/src/main/kotlin/trees/RBTree.kt b/lib/src/main/kotlin/trees/RBTree.kt
new file mode 100644
index 0000000..ce2fed2
--- /dev/null
+++ b/lib/src/main/kotlin/trees/RBTree.kt
@@ -0,0 +1,263 @@
+package trees
+
+open class RBTree, Value> : BalanceTree {
+ companion object {
+ const val RED = false
+ const val BLACK = true
+ }
+
+ protected class RBNode, Value>(
+ key: Key,
+ value: Value,
+ var color: Boolean = RED
+ ) : BinNode(key, value) {
+ fun swapColor() {
+ color = !color
+ }
+ }
+
+ constructor() : super()
+ constructor(key: Key, value: Value) : super(key, value)
+ constructor(vararg pairs: Pair) : super(pairs)
+
+ override fun insert(key: Key, value: Value) {
+ val node = insertService(RBNode(key, value))
+ if (node != null) balancingInsert(node as RBNode)
+ }
+
+ override fun remove(key: Key) {
+ val node = getNode(key) as RBNode? ?: return
+ val removeNode: RBNode
+ //find removeNode
+ if ((node.left != null) && (node.right != null)) {
+ val nextNode = nextElement(node) as RBNode? ?: error("remove is not possible: unexpected null")
+ node.key = nextNode.key
+ node.value = nextNode.value
+ removeNode = nextNode
+ } else removeNode = node
+
+ //delete node without child
+ if ((removeNode.left == null) && (removeNode.right == null)) {
+ val parent: BinNode? = removeNode.parent
+
+ if (parent == null) rootNode = null
+
+ //when the color of the node is red, just delete it
+ else if (removeNode.color == RED) replaceNodeParent(removeNode, null)
+
+ //when the color of the node without children is black,
+ //the tree needs to be balanced
+ else {
+ balancingRemove(removeNode)
+ replaceNodeParent(removeNode, null)
+ }
+
+ }
+ //delete black node with one red child
+ else if (removeNode.left == null) {
+ replaceNodeParent(removeNode, removeNode.right ?: error("remove error: unexpected null"))
+ (removeNode.right as RBNode).swapColor()
+ } else {
+ replaceNodeParent(removeNode, removeNode.left ?: error("remove error: unexpected null"))
+ (removeNode.left as RBNode).swapColor()
+ }
+ }
+
+ protected fun getGrandparent(node: RBNode?): RBNode? {
+ val parent = node?.parent
+ parent?.let { it -> it.parent?.let { return it as RBNode } }
+ return null
+ }
+
+ protected fun getSibling(node: RBNode?): RBNode? {
+ val parent = node?.parent ?: return null
+ return if (parent.left == node) parent.right as RBNode?
+ else parent.left as RBNode?
+ }
+
+ protected fun getUncle(node: RBNode?): RBNode? {
+ val parent = node?.parent ?: return null
+ return getSibling(parent as RBNode?)
+ }
+
+ private fun balancingInsert(node: RBNode) {
+ val parent = getParent(node.key) as RBNode?
+
+ //root color should always be black
+ if (parent == null) (rootNode as RBNode?)?.color = BLACK
+ else if (parent.color == BLACK) return
+ else {
+ val uncle = getUncle(node)
+ val grandparent = getGrandparent(node) ?: error("balancing error")
+
+ if (uncle?.color == RED) {
+ parent.swapColor()
+ uncle.swapColor()
+ grandparent.swapColor()
+ balancingInsert(grandparent)
+ } else {
+ if (grandparent.left == parent) {
+ if (parent.right == node) rotation(parent, RotationType.LEFT)
+ val newNode = rotation(grandparent, RotationType.RIGHT) as RBNode?
+ newNode?.swapColor() ?: error("balancing error")
+ } else {
+ if (parent.left == node) rotation(parent, RotationType.RIGHT)
+ val newNode = rotation(grandparent, RotationType.LEFT) as RBNode?
+ newNode?.swapColor() ?: error("balancing error")
+ }
+ grandparent.swapColor()
+ }
+ }
+ }
+
+
+ protected fun balancingRemove(removeNode: RBNode?) {
+ var node = removeNode
+
+ while ((node != rootNode) && (node?.color == BLACK)) {
+ val parent = node.parent as RBNode?
+ val brother = getSibling(node) ?: error("remove error: brother must exist")
+
+ //balancing when a node is the left child of its parent
+ if (node == parent?.left) {
+
+ //if the parent color is red, the brother's color must be black
+ if (parent.color == RED) {
+
+ //case when brother has a red child
+ if (((brother.left as RBNode?)?.color == RED) ||
+ ((brother.right as RBNode?)?.color == RED)) {
+ parent.color = BLACK
+ if ((brother.left as RBNode?)?.color == RED) {
+ rotation(brother, RotationType.RIGHT)
+ } else {
+ brother.color = RED
+ (brother.right as RBNode?)?.color = BLACK
+ }
+ node = rotation(parent, RotationType.LEFT) as RBNode?
+ } else {
+ brother.swapColor()
+ parent.swapColor()
+ }
+ break
+ }
+
+ else if (brother.color == RED) {
+ //brother's left child must exist and his color must be black
+ var brotherLeftChild =
+ brother.left as RBNode? ?: error("remove error: brother's left child must exist")
+
+ if (((brotherLeftChild.left as RBNode?)?.color == RED) ||
+ ((brotherLeftChild.right as RBNode?)?.color == RED)) {
+ if ((brotherLeftChild.left as RBNode?)?.color == RED) {
+ brotherLeftChild.swapColor()
+ (brotherLeftChild.left as RBNode?)?.swapColor()
+ brotherLeftChild = rotation(brotherLeftChild, RotationType.RIGHT) as RBNode
+ }
+ (brotherLeftChild.right as RBNode?)?.swapColor()
+ rotation(brother, RotationType.RIGHT)
+ }
+
+ else {
+ brother.swapColor()
+ brotherLeftChild.swapColor()
+ }
+ rotation(parent, RotationType.LEFT) as RBNode?
+ break
+ }
+
+ //if brother's color is black
+ else {
+ if (((brother.left == null) || (brother.left as RBNode?)?.color == BLACK) &&
+ ((brother.right == null) || (brother.right as RBNode?)?.color == BLACK)) {
+ brother.color = RED
+ node = parent
+ }
+
+ else {
+ if ((brother.right == null) || (brother.right as RBNode?)?.color == BLACK) {
+ (brother.left as RBNode?)?.color = BLACK
+ rotation(brother, RotationType.RIGHT)
+ }
+ else {
+ (brother.right as RBNode?)?.color = BLACK
+ }
+ rotation(parent, RotationType.LEFT) as RBNode?
+ break
+ }
+ }
+ }
+
+ //balancing when a node is the right child of its parent
+ else {
+
+ //if the parent color is red, the brother's color must be black
+ if (parent?.color == RED) {
+
+ //case when brother has a red child
+ if (((brother.left as RBNode?)?.color == RED) ||
+ ((brother.right as RBNode?)?.color == RED)) {
+ parent.color = BLACK
+ if ((brother.right as RBNode?)?.color == RED) {
+ rotation(brother, RotationType.LEFT)
+ } else {
+ brother.color = RED
+ (brother.left as RBNode?)?.color = BLACK
+ }
+ node = rotation(parent, RotationType.RIGHT) as RBNode?
+ }
+
+ else {
+ brother.swapColor()
+ parent.swapColor()
+ }
+ break
+ }
+
+ else if (brother.color == RED) {
+ //brother's right child must exist and his color must be black
+ var brotherRightChild =
+ brother.right as RBNode? ?: error("remove error: brother's right child must exist")
+
+ if (((brotherRightChild.left as RBNode?)?.color == RED) ||
+ ((brotherRightChild.right as RBNode?)?.color == RED)
+ ) {
+ if ((brotherRightChild.right as RBNode?)?.color == RED) {
+ brotherRightChild.swapColor()
+ (brotherRightChild.right as RBNode?)?.swapColor()
+ brotherRightChild = rotation(brotherRightChild, RotationType.LEFT) as RBNode
+ }
+ (brotherRightChild.left as RBNode?)?.swapColor()
+ rotation(brother, RotationType.LEFT)
+ }
+
+ else {
+ brother.swapColor()
+ brotherRightChild.swapColor()
+ }
+ rotation(parent, RotationType.RIGHT) as RBNode?
+ break
+ }
+
+ //if brother's color is black
+ else {
+ if (((brother.left == null) || (brother.left as RBNode?)?.color == BLACK) &&
+ ((brother.right == null) || (brother.right as RBNode?)?.color == BLACK)) {
+ brother.color = RED
+ node = parent
+ } else {
+ if ((brother.left == null) || (brother.left as RBNode?)?.color == BLACK) {
+ (brother.right as RBNode?)?.color = BLACK
+ rotation(brother, RotationType.LEFT)
+ }
+ else {
+ (brother.left as RBNode?)?.color = BLACK
+ }
+ rotation(parent, RotationType.RIGHT) as RBNode?
+ break
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/lib/src/main/kotlin/trees/Tree.kt b/lib/src/main/kotlin/trees/Tree.kt
new file mode 100644
index 0000000..0ef2c9c
--- /dev/null
+++ b/lib/src/main/kotlin/trees/Tree.kt
@@ -0,0 +1,39 @@
+package trees
+
+interface Tree {
+ /**
+ * inserts node in tree
+ *
+ * @param key which will have a node, affects its position in the tree, must be a Comparable type
+ *
+ * If a node with this key already exists in the tree, its value will be overwritten
+ * @param value which will have a node, can be any type
+ */
+ fun insert(key: Key, value: Value)
+ /**
+ * Calls an [insert] for each pair in the order of pairs' order.
+ */
+ fun insert(vararg array: Pair)
+
+ /**
+ * deletes node with this key
+ *
+ * if node with this key doesn't exist, do nothing
+ */
+ fun remove(key: Key)
+ /**
+ * Calls an [remove] for each key in the order of keys' order.
+ */
+ fun remove(vararg keys: Key)
+
+ /**
+ * @return value of node with this key or null if node doesn't exist
+ */
+ fun get(key: Key): Value?
+ /**
+ * Calls an [get] for each key in the order of keys' order.
+ *
+ * @return List of values
+ */
+ fun get(vararg keys: Key): List
+}
diff --git a/lib/src/test/kotlin/trees/AVLTreeTest.kt b/lib/src/test/kotlin/trees/AVLTreeTest.kt
new file mode 100644
index 0000000..44278e9
--- /dev/null
+++ b/lib/src/test/kotlin/trees/AVLTreeTest.kt
@@ -0,0 +1,155 @@
+package trees
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+import java.util.stream.Stream
+import kotlin.random.Random
+import kotlin.test.assertContains
+
+class AVLTreeTest {
+ fun generateTreeWithInsert(vararg arr: Int): AVLTree {
+ val tree = AVLTree()
+ for (i in arr) tree.insert(i, "${i}k")
+ return tree
+ }
+
+ companion object {
+ @JvmStatic
+ fun insertTestsFactory(): Stream {
+ return Stream.of(
+ Arguments.of(arrayOf(4), null, "insert one node test"),
+ Arguments.of(arrayOf(4), Pair(4, "5k"), "two inserts with eq. keys of the first node"),
+ Arguments.of(
+ arrayOf(4, 1, 5, 6),
+ Pair(4, "5k"),
+ "two inserts with eq. keys of the first node in non-degenerate tree"
+ ),
+ Arguments.of(arrayOf(5, 6, 4), Pair(4, "5k"), "two inserts with eq. keys of node"),
+ Arguments.of(Array(1000) { Random.nextInt() }, Pair(Random.nextInt(), "random"), "random insert")
+ )
+ }
+
+ @JvmStatic
+ fun removeTestsFactory(): Stream {
+ return Stream.of(
+ Arguments.of(arrayOf(4), 4, "remove one root node"),
+ Arguments.of(arrayOf(4, 6, 5, 7), 6, "remove non-root node"),
+ Arguments.of(arrayOf(4, 6, 5, 7), 4, "remove left leaf"),
+ Arguments.of(arrayOf(4, 6, 5, 7), 7, "remove right leaf"),
+ Arguments.of(arrayOf(4, 6, 5, 7), 5, "remove root node in non-degenerate tree"),
+ Arguments.of(Array(0) { it }, 4, "remove in empty tree"),
+ Arguments.of(arrayOf(2, 1, 3, 5), 4, "remove non-inserted node"),
+ Arguments.of(Array(1000) { Random.nextInt() }, Random.nextInt(), "random remove")
+ )
+ }
+
+ @JvmStatic
+ fun debugTestsFactory(): Stream {
+ return Stream.of(
+ Arguments.of(arrayOf(4, 2, 1), "2 \n1 4 \n", "right rotation"),
+ Arguments.of(arrayOf(2, 5, 4, 3, 1, 0), "2 \n1 4 \n0 - 3 5 \n", "difficult right rotation 1"),
+ Arguments.of(arrayOf(2, 5, 4, 3, 0, 1), "2 \n0 4 \n- 1 3 5 \n", "difficult right rotation 2"),
+ Arguments.of(arrayOf(2, 1, 4, 3, 5, 6), "4 \n2 5 \n1 3 - 6 \n", "difficult left rotation 1"),
+ Arguments.of(arrayOf(2, 1, 4, 3, 6, 5), "4 \n2 6 \n1 3 5 ", "difficult left rotation 2"),
+ Arguments.of(arrayOf(1, 3, 2), "2 \n1 3 \n", "right left rotation"),
+ Arguments.of(arrayOf(3, 1, 2), "2 \n1 3 \n", "left right rotation"),
+ Arguments.of(arrayOf(4, 3, 6, 5, 8, 7, 1, 2), "6 \n4 8 \n2 5 7 - \n1 3 ", "combine"),
+ )
+ }
+
+ @JvmStatic
+ fun debugRemoveTestsFactory(): Stream {
+ return Stream.of(
+ Arguments.of(arrayOf(4, 3, 6, 5, 8, 7, 1, 2), 1, "6 \n4 8 \n2 5 7 - \n- 3 ", "remove right leaf"),
+ Arguments.of(arrayOf(4, 3, 6, 5, 8, 7, 1, 2), 3, "6 \n4 8 \n2 5 7 - \n1 ", "remove left leaf"),
+ Arguments.of(arrayOf(4, 3, 6, 5, 8, 7, 1, 2), 2, "6 \n4 8 \n3 5 7 - \n1 ", "remove root of two leafs"),
+ Arguments.of(arrayOf(4, 3, 6, 5, 8, 7, 1, 2), 8, "4 \n2 6 \n1 3 5 7 \n", "remove with rebalancing"),
+ Arguments.of(arrayOf(4, 3, 6, 5, 8, 7, 1, 2), 6, "4 \n2 7 \n1 3 5 8 \n", "remove root"),
+ Arguments.of(arrayOf(4, 3, 6, 5, 8, 7, 1, 2), 4, "6 \n2 8 \n1 5 7 - \n- - 3 ", "one more remove"),
+ )
+ }
+ }
+
+ @ParameterizedTest(name = "{2} ({0}, {1})")
+ @MethodSource("insertTestsFactory")
+ @DisplayName("insert-get simple tests")
+ fun `insert-get simple tests`(arrKeys: Array, extraInsert: Pair?, name: String) {
+ val tree = generateTreeWithInsert(*arrKeys.toIntArray())
+ if (extraInsert != null) tree.insert(extraInsert.first, extraInsert.second)
+ Assertions.assertArrayEquals(
+ keysToValues(*arrKeys.toIntArray(), chValue = extraInsert), tree.get(*arrKeys).toTypedArray()
+ )
+ }
+
+ @ParameterizedTest(name = "{2} ({0}, {1})")
+ @MethodSource("removeTestsFactory")
+ @DisplayName("remove tests")
+ fun `remove tests`(arrKeys: Array, remove: Int, name: String) {
+ val tree = generateTreeWithInsert(*arrKeys.toIntArray())
+ tree.remove(remove)
+ Assertions.assertArrayEquals(
+ keysToValues(*arrKeys.toIntArray(), remove = remove), tree.get(*arrKeys).toTypedArray()
+ )
+ }
+
+ @ParameterizedTest(name = "{2} ({0})")
+ @MethodSource("debugTestsFactory")
+ @DisplayName("insert tests using debug")
+ fun testsWithDebug(keys: Array, treeInString: String, name: String) {
+ val tree = generateTreeWithInsert(*keys.toIntArray())
+ Assertions.assertEquals(treeInString, tree.Debug().treeKeysInString())
+ }
+
+ @ParameterizedTest(name = "{3} ({0}, {1})")
+ @MethodSource("debugRemoveTestsFactory")
+ @DisplayName("remove tests using debug")
+ fun removeTestsWithDebug(keys: Array, remove: Int, treeInString: String, name: String) {
+ val tree = generateTreeWithInsert(*keys.toIntArray())
+ tree.remove(remove)
+ Assertions.assertEquals(treeInString, tree.Debug().treeKeysInString())
+ }
+
+ @Nested
+ @DisplayName("constructors test")
+ inner class ConstructorsTest {
+ @Test
+ fun `insert key, value`() {
+ val tree = AVLTree(4, "4k")
+ Assertions.assertEquals("4k", tree.get(4))
+ }
+
+ @Test
+ fun `insert two node`() {
+ val tree = AVLTree(Pair(4, "4k"), Pair(5, "5k"))
+ Assertions.assertArrayEquals(arrayOf("4k", "5k"), tree.get(4, 5).toTypedArray())
+ }
+
+ @Test
+ fun `insert equal nodes`() {
+ val tree = AVLTree(Pair(4, "4k"), Pair(5, "5k"), Pair(4, "7k"))
+ Assertions.assertAll({ assertContains(arrayOf("4k", "7k"), tree.get(4)) },
+ { Assertions.assertEquals("5k", tree.get(5)) })
+ }
+ }
+
+
+ @Test
+ fun `my struct`() {
+ class My (
+ val arg1: String
+ ) : Comparable {
+ override fun compareTo(other: My): Int = arg1.compareTo(other.arg1)
+ }
+
+ val tree = AVLTree(Pair(My("11"), 1), Pair(My("111"), 111), Pair(My("321"), 321))
+ tree.remove(My("321"))
+ Assertions.assertAll({ Assertions.assertEquals(1, tree.get(My("11"))) },
+ { Assertions.assertEquals(111, tree.get(My("111"))) },
+ { Assertions.assertNull(tree.get(My("321"))) })
+ }
+}
diff --git a/lib/src/test/kotlin/trees/BSTreeTest.kt b/lib/src/test/kotlin/trees/BSTreeTest.kt
new file mode 100644
index 0000000..c3ef1e9
--- /dev/null
+++ b/lib/src/test/kotlin/trees/BSTreeTest.kt
@@ -0,0 +1,147 @@
+package trees
+
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+import java.util.stream.Stream
+import kotlin.random.Random
+import kotlin.test.assertContains
+
+fun keysToValues(vararg arr: Int, remove: Int? = null, chValue: Pair? = null): Array {
+ return Array(arr.size) {
+ if (arr[it] != remove) {
+ if (arr[it] == chValue?.first) chValue.second else "${arr[it]}k"
+ } else null
+ }
+}
+
+class BSTreeTest {
+ fun generateTreeWithInsert(vararg arr: Int): BSTree {
+ val tree = BSTree()
+ for (i in arr) tree.insert(i, "${i}k")
+ return tree
+ }
+
+ companion object {
+ @JvmStatic
+ fun insertTestsFactory(): Stream {
+ return Stream.of(
+ Arguments.of(arrayOf(4), null, "insert one node test"),
+ Arguments.of(arrayOf(4), Pair(4, "5k"), "two inserts with eq. keys of the first node"),
+ Arguments.of(
+ arrayOf(4, 1, 5, 6),
+ Pair(4, "5k"),
+ "two inserts with eq. keys of the first node in non-degenerate tree"
+ ),
+ Arguments.of(arrayOf(5, 6, 4), Pair(4, "5k"), "two inserts with eq. keys of node"),
+ Arguments.of(Array(1000) { Random.nextInt() }, Pair(Random.nextInt(), "random"), "random insert")
+ )
+ }
+
+ @JvmStatic
+ fun removeTestsFactory(): Stream {
+ return Stream.of(
+ Arguments.of(arrayOf(4), 4, "remove one root node"),
+ Arguments.of(arrayOf(4, 6, 5, 7), 6, "remove non-root node"),
+ Arguments.of(arrayOf(4, 6, 5, 7), 5, "remove left leaf"),
+ Arguments.of(arrayOf(4, 6, 5, 7), 7, "remove right leaf"),
+ Arguments.of(arrayOf(4, 6, 5, 7), 4, "remove root node in non-degenerate tree"),
+ Arguments.of(Array(0) { it }, 4, "remove in empty tree"),
+ Arguments.of(arrayOf(2, 1, 3, 5), 4, "remove non-inserted node"),
+ Arguments.of(Array(1000) { Random.nextInt() }, Random.nextInt(), "random remove")
+ )
+ }
+ }
+
+ @ParameterizedTest(name = "{2}")
+ @MethodSource("insertTestsFactory")
+ @DisplayName("insert-get simple tests")
+ fun `insert-get simple tests`(arrKeys: Array, extraInsert: Pair?, name: String) {
+ val tree = generateTreeWithInsert(*arrKeys.toIntArray())
+ if (extraInsert != null) tree.insert(extraInsert.first, extraInsert.second)
+ assertArrayEquals(keysToValues(*arrKeys.toIntArray(), chValue = extraInsert), tree.get(*arrKeys).toTypedArray())
+ }
+
+ @ParameterizedTest(name = "{2}")
+ @MethodSource("removeTestsFactory")
+ @DisplayName("remove tests")
+ fun `remove tests`(arrKeys: Array, remove: Int, name: String) {
+ val tree = generateTreeWithInsert(*arrKeys.toIntArray())
+ tree.remove(remove)
+ assertArrayEquals(keysToValues(*arrKeys.toIntArray(), remove = remove), tree.get(*arrKeys).toTypedArray())
+ }
+
+ @Nested
+ @DisplayName("constructors test")
+ inner class ConstructorsTest {
+ @Test
+ fun `insert key, value`() {
+ val tree = BSTree(4, "4k")
+ assertEquals("4k", tree.get(4))
+ }
+
+ @Test
+ fun `insert two node`() {
+ val tree = BSTree(Pair(4, "4k"), Pair(5, "5k"))
+ assertArrayEquals(arrayOf("4k", "5k"), tree.get(4, 5).toTypedArray())
+ }
+
+ @Test
+ fun `insert equal nodes`() {
+ val tree = BSTree(Pair(4, "4k"), Pair(5, "5k"), Pair(4, "7k"))
+ assertAll({ assertContains(arrayOf("4k", "7k"), tree.get(4)) }, { assertEquals("5k", tree.get(5)) })
+ }
+ }
+
+
+ @Nested
+ @DisplayName("tests using debug")
+ inner class TestsUsingDebug {
+ @Test
+ fun `insert three nodes test`() {
+ assertEquals("2 \n1 3 \n", generateTreeWithInsert(2, 1, 3).Debug().treeKeysInString())
+ }
+
+ @Test
+ fun `degenerate tree`() {
+ assertEquals("1 \n- 2 \n- - - 3 \n", generateTreeWithInsert(1, 2, 3).Debug().treeKeysInString())
+ }
+
+ @Test
+ fun `two inserts of node with equal keys`() {
+ val tree = generateTreeWithInsert(5, 6, 4)
+ tree.insert(4, "5k")
+ assertEquals("5 \n4 6 \n", tree.Debug().treeKeysInString())
+ }
+
+ @Test
+ fun `multiple removal`() {
+ val tree = generateTreeWithInsert(10, 7, 15, 13, 17, 16, 18, 14, 12, 6, 9)
+ tree.remove(15)
+ assertEquals("10 \n7 16 \n6 9 13 17 \n- - - - 12 14 - 18 \n", tree.Debug().treeKeysInString())
+ tree.remove(10)
+ assertEquals("12 \n7 16 \n6 9 13 17 \n- - - - - 14 - 18 \n", tree.Debug().treeKeysInString())
+ tree.remove(17)
+ assertEquals("12 \n7 16 \n6 9 13 18 \n- - - - - 14 ", tree.Debug().treeKeysInString())
+ }
+ }
+
+ @Test
+ fun `my struct`() {
+ class My(
+ val arg1: String
+ ) : Comparable {
+ override fun compareTo(other: My): Int = arg1.compareTo(other.arg1)
+ }
+
+ val tree = BSTree(Pair(My("11"), 1), Pair(My("111"), 111), Pair(My("321"), 321))
+ tree.remove(My("321"))
+ assertAll({ assertEquals(1, tree.get(My("11"))) },
+ { assertEquals(111, tree.get(My("111"))) },
+ { assertNull(tree.get(My("321"))) })
+ }
+}
diff --git a/lib/src/test/kotlin/trees/RBTreeTest.kt b/lib/src/test/kotlin/trees/RBTreeTest.kt
new file mode 100644
index 0000000..c331d12
--- /dev/null
+++ b/lib/src/test/kotlin/trees/RBTreeTest.kt
@@ -0,0 +1,154 @@
+package trees
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+import java.util.stream.Stream
+import kotlin.random.Random
+import kotlin.test.assertContains
+
+class RBTreeTest {
+ private fun generateTreeWithInsert(vararg arr: Int): RBTree {
+ val tree = RBTree()
+ for (i in arr) tree.insert(i, "${i}k")
+ return tree
+ }
+
+ companion object {
+ @JvmStatic
+ fun insertTestsFactory(): Stream {
+ return Stream.of(
+ Arguments.of(arrayOf(4), null, "insert one node test"),
+ Arguments.of(arrayOf(4), Pair(4, "5k"), "two inserts with eq. keys of the first node"),
+ Arguments.of(arrayOf(4, 1, 5, 6), Pair(4, "5k"), "two inserts with eq. keys of the first node in non-degenerate tree"),
+ Arguments.of(arrayOf(5, 6, 4), Pair(4, "5k"), "two inserts with eq. keys of node"),
+ Arguments.of(Array(1000) { Random.nextInt() }, Pair(Random.nextInt(), "random"), "random insert")
+ )
+ }
+
+ @JvmStatic
+ fun removeTestsFactory(): Stream {
+ return Stream.of(
+ Arguments.of(arrayOf(4), 4, "remove one root node"),
+ Arguments.of(arrayOf(4, 6, 5, 7), 6, "remove non-root node"),
+ Arguments.of(arrayOf(4, 6, 5, 7), 4, "remove left leaf"),
+ Arguments.of(arrayOf(4, 6, 5, 7), 7, "remove right leaf"),
+ Arguments.of(arrayOf(4, 6, 5, 7), 5, "remove root node in non-degenerate tree"),
+ Arguments.of(Array(0) { it }, 4, "remove in empty tree"),
+ Arguments.of(arrayOf(2, 1, 3, 5), 4, "remove non-inserted node"),
+ Arguments.of(Array(1000) { Random.nextInt() }, Random.nextInt(), "random remove")
+ )
+ }
+
+ @JvmStatic
+ fun debugTestsFactory(): Stream {
+ return Stream.of(
+ Arguments.of(arrayOf(4), "4 \n", "insert root"),
+ Arguments.of(arrayOf(5, 6, 3, 4, 1, 2), "5 \n3 6 \n1 4 - - \n- 2 ", "grandfather isn't root, uncle red"),
+ Arguments.of(arrayOf(6, 3, 8, 4), "6 \n3 8 \n- 4 ", "grandfather root, red uncle)"),
+ Arguments.of(arrayOf(6, 4, 5), "5 \n4 6 \n", "zigzag, null uncle"),
+ Arguments.of(arrayOf(5, 4, 3), "4 \n3 5 \n", "straight line, null uncle"),
+ Arguments.of(arrayOf(8, 9, 5, 6, 3, 2, 4, 1), "5 \n3 8 \n2 4 6 9 \n1 ", "change color, right rotation"),
+ Arguments.of(arrayOf(8, 9, 5, 6, 3, 1, 2), "8 \n5 9 \n2 6 - - \n1 3 ", "two rotation"),
+ )
+ }
+
+ @JvmStatic
+ fun debugRemoveTestsFactory(): Stream {
+ return Stream.of(
+// Arguments.of(arrayOf(4, 2, 5, 3), arrayOf(3), "4 \n2 5 \n", "remove red leaf"),
+// Arguments.of(arrayOf(4, 2, 5, 3), arrayOf(2), "4 \n3 5 \n", "remove black with red child"),
+ Arguments.of(arrayOf(5, 2, 8, 7, 9, 10, 6), arrayOf(2), "6 \n5 8 \n- - 7 9 \n- - - - - - - 10 \n", "remove left black with red brother 1"),
+ Arguments.of(arrayOf(5, 2, 8, 10, 6, 7, 9), arrayOf(2), "6 \n5 8 \n- - 7 10 \n- - - - - - 9 ", "remove left black with red brother 2"),
+ Arguments.of(arrayOf(5, 2, 8, 1, 3, 0, 4), arrayOf(8), "4 \n2 5 \n1 3 - - \n0 ", "remove right black with red brother 1"),
+ Arguments.of(arrayOf(5, 2, 8, 0, 4, 1, 3), arrayOf(8), "4 \n2 5 \n0 3 - - \n- 1 ", "remove right black with red brother 2"),
+ Arguments.of(arrayOf(5, 2, 8, 7, 9), arrayOf(2), "8 \n5 9 \n- 7 ", "remove black with black brother right"),
+ Arguments.of(arrayOf(5, 2, 8, 1, 3), arrayOf(8), "2 \n1 5 \n- - 3 ", "remove black with black brother left"),
+ Arguments.of(arrayOf(3, 1, 9, 7, 11, 5, 8), arrayOf(7), "3 \n1 9 \n- - 8 11 \n- - - - 5 ", "remove node with two red children"),
+ Arguments.of(arrayOf(96, 69, 3, 49, 89, 61, 61, 16, 33, 21), arrayOf(69, 49), "61 \n16 89 \n3 33 - 96 \n- - 21 ", "add something"),
+ )
+ }
+ }
+
+ @ParameterizedTest(name = "{2} ({1}, {2})")
+ @MethodSource("insertTestsFactory")
+ @DisplayName("insert-get simple tests")
+ fun `insert-get simple tests`(arrKeys: Array, extraInsert: Pair?, name: String) {
+ val tree = generateTreeWithInsert(*arrKeys.toIntArray())
+ if (extraInsert != null) tree.insert(extraInsert.first, extraInsert.second)
+ Assertions.assertArrayEquals(
+ keysToValues(*arrKeys.toIntArray(), chValue = extraInsert), tree.get(*arrKeys).toTypedArray()
+ )
+ }
+
+ @ParameterizedTest(name = "{2}, ({0}, {1})")
+ @MethodSource("removeTestsFactory")
+ @DisplayName("remove tests")
+ fun `remove tests`(arrKeys: Array, remove: Int, name: String) {
+ val tree = generateTreeWithInsert(*arrKeys.toIntArray())
+ tree.remove(remove)
+ Assertions.assertArrayEquals(
+ keysToValues(*arrKeys.toIntArray(), remove = remove), tree.get(*arrKeys).toTypedArray()
+ )
+ }
+
+ @ParameterizedTest(name = "{2} ({0})")
+ @MethodSource("debugTestsFactory")
+ @DisplayName("insert tests using debug")
+ fun testsWithDebug(keys: Array, treeInString: String, name: String) {
+ val tree = generateTreeWithInsert(*keys.toIntArray())
+ Assertions.assertEquals(treeInString, tree.Debug().treeKeysInString())
+ }
+
+ @ParameterizedTest(name = "{3} ({0}, {1}")
+ @MethodSource("debugRemoveTestsFactory")
+ @DisplayName("remove tests using debug")
+ fun removeTestsWithDebug(keys: Array, remove: Array, treeInString: String, name: String) {
+ val tree = generateTreeWithInsert(*keys.toIntArray())
+ remove.forEach { tree.remove(it)}
+ Assertions.assertEquals(treeInString, tree.Debug().treeKeysInString())
+ }
+
+ @Nested
+ @DisplayName("constructors test")
+ inner class ConstructorsTest {
+ @Test
+ fun `insert key, value`() {
+ val tree = RBTree(4, "4k")
+ Assertions.assertEquals("4k", tree.get(4))
+ }
+
+ @Test
+ fun `insert two node`() {
+ val tree = RBTree(Pair(4, "4k"), Pair(5, "5k"))
+ Assertions.assertArrayEquals(arrayOf("4k", "5k"), tree.get(4, 5).toTypedArray())
+ }
+
+ @Test
+ fun `insert equal nodes`() {
+ val tree = RBTree(Pair(4, "4k"), Pair(5, "5k"), Pair(4, "7k"))
+ Assertions.assertAll({ assertContains(arrayOf("4k", "7k"), tree.get(4)) },
+ { Assertions.assertEquals("5k", tree.get(5)) })
+ }
+ }
+
+
+ @Test
+ fun `my struct`() {
+ class My(
+ val arg1: String
+ ) : Comparable {
+ override fun compareTo(other: My): Int = arg1.compareTo(other.arg1)
+ }
+
+ val tree = RBTree(Pair(My("11"), 1), Pair(My("111"), 111), Pair(My("321"), 321))
+ tree.remove(My("321"))
+ Assertions.assertAll({ Assertions.assertEquals(1, tree.get(My("11"))) },
+ { Assertions.assertEquals(111, tree.get(My("111"))) },
+ { Assertions.assertNull(tree.get(My("321"))) })
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..248e7e9
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,6 @@
+pluginManagement {
+ includeBuild("build-logic")
+}
+
+rootProject.name = "trees"
+include("lib", "app")