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 @@ +logo + +# 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. + + +[![Apache License](https://img.shields.io/badge/license-Apache%202.0-black.svg)](https://www.apache.org/licenses/LICENSE-2.0) + +![image](https://user-images.githubusercontent.com/113186929/235809712-99ff32ab-0c26-4095-8914-edbe727d6343.png) + + +## 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")