diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..097f9f98 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.github/mergeable.yml b/.github/mergeable.yml new file mode 100644 index 00000000..1817f8db --- /dev/null +++ b/.github/mergeable.yml @@ -0,0 +1,44 @@ +version: 2 +mergeable: + - when: pull_request.*, pull_request_review.* + filter: + # ignore 'Feedback' PR + - do: payload + pull_request: + title: + must_exclude: + regex: '^Feedback$' + + validate: + # Work in progress + - do: title + must_exclude: + regex: '^\[WIP\]' + - do: label + must_exclude: + regex: 'wip' + + # No empty description + - do: description + no_empty: + enabled: true + message: Description matter and should not be empty. + + # Some approve + - do: approvals + min: + count: 1 + required: + assignees: true + + + # Pull request to main only from release and hotfix branches + - do: or + validate: + - do: baseRef + must_exclude: + regex: '^main$' + - do: headRef + must_include: + regex: '^(release|hotfix)\/.+$' + message: "Create PR to main only from release and hotfix branches" diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml new file mode 100644 index 00000000..018d25c3 --- /dev/null +++ b/.github/workflows/actions.yml @@ -0,0 +1,40 @@ +name: Build project + +on: + push: +jobs: + build-gradle-project: + strategy: + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout project sources + uses: actions/checkout@v3 + + - name: Set up JDK 11 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 11 + + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + - name: Run build with Gradle Wrapper + run: ./gradlew build + + - name: Upload reports for library + if: success() || failure() + uses: actions/upload-artifact@v3 + with: + name: library-reports (${{ matrix.os }}) + path: binaryTree/build/reports/ + if-no-files-found: ignore + + - name: Upload reports for app + if: success() || failure() + uses: actions/upload-artifact@v3 + with: + name: app-reports (${{ matrix.os }}) + path: app/build/reports/ + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..36f47ba8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Gradle +.gradle +build + +# Idea +.idea diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..88490323 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,70 @@ +# Внесение правок + +## Основные советы + +1. Никогда не используйте merge, только rebase для сохранения линейной истории коммитов +2. **Осторожно** с изменениями в чужих ветках. Придется больно и мучительно делать rebase. Лучше не трогайте чужие + ветки +3. Перепроверьте историю коммитов перед созданием пулл реквеста +4. Каждый коммит должен быть осознанным и быть одним логическим элементом +5. Каждый пулл реквест должен быть осознанным и быть одним логическим элементом +6. **Перепроверьте, что вы в правильной ветке**, никогда не коммитьте напрямую в main + +## Правила добавления коммитов + +Коммиты добавляются в соответствии с conventional commits. Т.е +`(): `. + +Поле `` должно принимать одно из этих значений: + +* `feat` для добавления новой функциональности +* `fix` для исправления бага в программе +* `refactor` для рефакторинга кода, например, переименования переменной +* `perf` для изменения кода, повышающего производительность +* `test` для добавления тестов, их рефакторинга +* `struct` для изменений связанных с изменением структуры проекта (НО НЕ КОДА), например изменение + расположения папок +* `ci` для различных задач ci/cd +* `remove` для удаления + +Поле `` опционально и показывает к какому модулю, классу, методу функции и т.п применены изменения. + +Поле `` содержит суть изменений в повелительном наклонении настоящего времени на английском языке без точки в +конце, первое слово - глагол с большой буквы. Текст сообщения должен включать мотивацию к изменению и контрасты с +предыдущим поведением. + +Примеры: + +* Хорошо: "Add module for future BST implementations" +* Плохо: "Added module for future BST implementations." +* Плохо: "Adds module". +* Очень плохо: "new bug." + +## Правила работы с ветками + +1. Из ветки `develop` создается ветка `release`. +2. Из ветки `develop` создаются ветки `feature`. +3. Когда работа над веткой `feature` завершается, она сливается в ветку `develop`. +4. Когда работа над веткой `release` завершается, она сливается с ветками `develop` и `main`. +5. Если в ветке `main` обнаруживается проблема, из `main` создается ветка `hotfix`. +6. Когда работа над веткой `hotfix` завершается, она сливается с ветками `develop` и `main`. + +### Правила именования и создания веток `feature` + +Ветка под одно (большое) логическое изменение. Формат для веток `/`. Тип аналогичен тому же в +коммитах, +а `` представляет собой короткое описание назначения ветки в kebab-case стиле. + +Примеры хороших названий: + +* `feat/add-avl-tree` +* `ci/add-klint` + +После одобрения пулл реквеста, ветка удаляется. А новая функциональность разрабатывается в новой ветке. + +### Именование веток `hotfix` + +Формат для веток `hotfix/`. Где `` представляет собой короткое описание назначения ветки в +kebab-case стиле. + +**Ветка `hotfix` создается только из ветки `main`** diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..a4623430 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,16 @@ +MIT License + +Copyright (c) 2023 Akhmedov David, Ermolovich Anna, Efremov Alexey, Yakshigulov Vadim, Dyachkov Vitaliy, Perevalov Efim + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..3788afa6 --- /dev/null +++ b/README.md @@ -0,0 +1,266 @@ +logo + +# Trees-2 + +Program for binary tree visualization. + +--- +![gui-example](https://user-images.githubusercontent.com/66139162/235777850-84e8d881-cbc0-429d-a74e-b9b3cbf388fe.png) + +## Features + +You can: + +- **Create** 3 different types of search trees: **AVL**, **RedBlack** and **Common binary tree** +- **Edit** trees: **insert** nodes, **move** nodes and **remove** them +- **Save** it with 3 different ways: **Neo4j**, **Json** and **SQLite** +- Or **load** your own tree from these "databases" +- Use our [library](#Library) to **implement** binary search trees in your own project + +--- + +## Get started + +### Requirements + +- Java JDK version 11 or later + +### Build + +To build and run this application locally, you'll need Git and JDK installed on your computer. From your command +line: + +```bash +# Clone this repository +git clone https://github.com/spbu-coding-2022/trees-2.git trees-2 + +# Go into the repository +cd trees-2 + +# Build +./gradlew assemble +# use `./gradlew build` if you want to test app + +# Run the app +./gradlew run +``` + +### Neo4j database + +Since to save or download from neo4j, the application needs to connect to an instance of this database. +We will tell you how it can be started quickly. + +- download + archive [\[linux/mac\]](https://neo4j.com/download-thanks/?edition=community&release=5.6.0&flavour=unix) [\[windows\]](https://neo4j.com/download-thanks/?edition=community&release=5.6.0&flavour=winzip) +- extract files from it to a dir `~/neo4j-dir/` +- run `~/neo4j-dir/bin/neo4j console` +- now you can visit http://localhost:7474 in your web browser. +- connect using the username 'neo4j' with default password 'neo4j'. You'll then be prompted to change the password. +- to save/load tree enter this url `bolt://localhost:7687` with correct login and password to app + +If you got trouble during installation or for more +information visit https://neo4j.com/docs/operations-manual/current/installation/ + +--- + +## Databases format specifications + +### Plain text + +At plain text database we store AVLTree. We use json files. There are 2 types of objects with the following struct: + +- `Tree` + - `root` *AVLNode* +- `AVLNode` + - `key` *int* + - `value` *string* + - `height` *int* - node height + - `x` *int* - node position at ui + - `y` *int* - node position at ui + - `left` *AVLNode* - node left child + - `right` *AVLNode* - node right child + +We have methods: + +- `exportTree(TreeController, file.json)` - writes information stored in TreeController object to a file `file.json`. +- `importTree(file.json)` - reads the tree stored in the file `file.json` and returns TreeController object. + + +### SQLite + +At SQLite database we store BinSearchTree. There are 2 types of objects with the following struct: + +- `Tree` + - `root` *Node* +- `Node` + - `key` *int* + - `parentKey` *int* + - `value` *string* + - `x` *int* - node position at ui + - `y` *int* - node position at ui + +We have methods: + +- `exportTree(TreeController, file.sqlite)` - writes information stored in TreeController object to a `file` in preorder + order. +- `importTree(file.sqlite)` - reads the tree stored in the `file` and returns TreeController object. **Nodes must be + stored in preorder order (check root -> then check left child -> then check right child).** + If there are some extra tree nodes, that can't fit in the tree, then `importTree` will throw `HandledIOException`. + +### Neo4j + +At neo4j database we store **RBTree**. There are 2 types of labels with the following struct: + +- `Tree` + - `name` *string* +- `RBNode` + - `key` *int* + - `value` *string* + - `isBlack` *boolean* - node colour + - `x` *int* - node position at ui + - `y` *int* - node position at ui + +Also, there are 3 types of links: + +- `ROOT`: from **tree**(`Tree`) to its **root**(`RBNode`) +- `LEFT_CHILD`: from **parent**(`RBNode`) to its **left-child**(`RBNode`) +- `RIGHT_CHILD`: from **parent**(`RBNode`) to its **right-child**(`RBNode`) + +Using the `Tree` labels, we can store several trees in the database at once. But their nodes should not intersect +(shouldn't have links between each other). +![neo4j-example](https://user-images.githubusercontent.com/66139162/233449145-15476b7d-d1c9-4dfa-b4a6-bd500d3a25d4.png) + +--- + +## Library + +[Our library](binaryTree) gives you the opportunity to work with already written +trees (`BinSearchTree`, `AVLTree`, `RBTree`), and write your own based on written templates. + +### Gradle + +Since our library is a separate gradle module, it is enough to import it: + +- copy the module folder to your gradle project +- specify it in the dependencies + +```kotlin +dependencies { + implementation(project(":binaryTree")) +} +``` + +### Examples + +#### Trees + +```kotlin +fun example() { + // create RBTree with int elements + val intTree = RBTree() + + // create AVLTree with string elements + val stringTree = AVLTree() + + // create common BinSearchTree with int keys and string values + val keyValuePairTree = BinSearchTree>() + + intTree.insert(3) // insert new node + intTree.remove(3) // remove node if it exists + val foundNode = intTree.find(3) // find node + + for (i in 0..10) { + stringTree.insert(i.toString()) + } + val traverseList = stringTree.traversal(TemplateNode.Traversal.INORDER) + // get list of nodes' elements at INORDER traverse +} +``` + +#### Implement your tree + +```kotlin +class CoolNode>(v: T, var coolness: Double) : TemplateNode>(v) { + fun sayYourCoolness() { + println("My coolness equal to $coolness cats") + } +} + +class MyCoolTree> : TemplateBSTree>() { + override fun insert(curNode: CoolNode?, obj: T): CoolNode? { + val newNode = CoolNode(obj, 0.0) + val parentNode = insertNode(curNode, newNode) + parentNode?.let { + newNode.coolness = it.coolness * 1.3 + } + return parentNode + } +} +``` + +#### Implement your balance tree + +```kotlin +class MyCoolTree> : TemplateBalanceBSTree>() { + override fun insert(curNode: CoolNode?, obj: T): CoolNode? { + return insertNode(curNode, CoolNode(obj, 0.0)) + } + + override fun balance( + curNode: CoolNode?, + changedChild: BalanceCase.ChangedChild, + operationType: BalanceCase.OperationType, + recursive: BalanceCase.Recursive + ) { + when (changedChild) { + BalanceCase.ChangedChild.ROOT -> println("root was changed") + BalanceCase.ChangedChild.LEFT -> println("left child of curNode was changed") + BalanceCase.ChangedChild.RIGHT -> println("right child of curNode was changed") + } + + when (operationType) { + BalanceCase.OpType.REMOVE_0 -> println("removed node with 0 null children") + BalanceCase.OpType.REMOVE_1 -> println("removed node with 1 null child") + BalanceCase.OpType.REMOVE_2 -> println("removed node with 2 null children") + BalanceCase.OpType.INSERT -> println("inserted new node") + } + + when (recursive) { + BalanceCase.Recursive.RECURSIVE_CALL -> println("just returning from traverse") + BalanceCase.Recursive.END -> println("this was last iteration, so it can change something") + } + + if (curNode != null) { + curNode.left?.let { + rotateLeft(it, curNode) + // do left rotate on the curNode.left with curNode as parent + } + } + } +} + +``` + +#### More examples + +For more examples you can watch code of our [app](app/src/main/kotlin/org/tree/app) +or [library](binaryTree/src/main/kotlin/org/tree/binaryTree). + +--- + +## Contributing + +**Quick start**: + +1. Create a branch with new feature from `develop` branch (`git checkout -b feat/my-feature develop`) +2. Commit the changes (`git commit -m "feat: Add some awesome feature"`) +3. Push the branch to origin (`git push origin feat/add-amazing-feature`) +4. Open the pull request + +For more details, see [CONTRIBUTING.md](CONTRIBUTING.md) + +--- + +## License + +This project is licensed under the terms of the **MIT** license. See the [LICENSE](LICENSE.md) for more information. diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..9f133777 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + id("org.tree.kotlin-application-conventions") + + kotlin("plugin.serialization") version "1.8.20" + + id("org.jetbrains.compose") version "1.4.0" +} + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() +} + +dependencies { + implementation(project(":binaryTree")) + + implementation("org.neo4j.driver", "neo4j-java-driver", "5.7.0") + + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") + + implementation("io.github.microutils:kotlin-logging-jvm:3.0.5") + implementation("org.slf4j:slf4j-simple:2.0.5") + + implementation("org.xerial:sqlite-jdbc:3.40.1.0") + implementation("org.jetbrains.exposed:exposed-core:0.40.1") + implementation("org.jetbrains.exposed:exposed-dao:0.40.1") + implementation("org.jetbrains.exposed:exposed-jdbc:0.40.1") + + implementation(compose.desktop.currentOs) +} + +application { + // Define the main class for the application. + mainClass.set("org.tree.app.AppKt") +} diff --git a/app/src/main/kotlin/org/tree/app/App.kt b/app/src/main/kotlin/org/tree/app/App.kt new file mode 100644 index 00000000..dc8d9383 --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/App.kt @@ -0,0 +1,358 @@ +/* + * This Kotlin source file was generated by the Gradle 'init' task. + */ +package org.tree.app + +import TreeController +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.MenuBar +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import newTree +import org.tree.app.controller.io.AppDataController +import org.tree.app.controller.io.SavedTree +import org.tree.app.controller.io.SavedType +import org.tree.app.controller.io.handleIOException +import org.tree.app.view.* +import org.tree.app.view.dialogs.AlertDialog +import org.tree.app.view.dialogs.ExitDialog +import org.tree.app.view.dialogs.io.* +import org.tree.binaryTree.AVLNode +import org.tree.binaryTree.KVP +import org.tree.binaryTree.Node +import org.tree.binaryTree.RBNode +import org.tree.binaryTree.trees.AVLTree +import org.tree.binaryTree.trees.BinSearchTree +import org.tree.binaryTree.trees.RBTree + +enum class DialogType { + EMPTY, + IMPORT_RB, + EXPORT_RB +} + +val appDataController = AppDataController() +fun main() = application { + val icon = painterResource("icon.png") + var showExitDialog by remember { mutableStateOf(false) } + val windowState = rememberWindowState(width = 800.dp, height = 600.dp) + Window( + onCloseRequest = { showExitDialog = true }, + title = "Trees", + state = windowState, + icon = icon, + ) { + var throwException by remember { mutableStateOf(false) } + var exceptionContent by remember { mutableStateOf("Nothing...") } + + var widthOfPanel by remember { mutableStateOf(400) } + var dialogType by remember { mutableStateOf(DialogType.EMPTY) } + var logString by remember { mutableStateOf("Log string") } + var logColor by remember { mutableStateOf(Color.DarkGray) } + val treeOffsetX = remember { mutableStateOf(0) } + val treeOffsetY = remember { mutableStateOf(0) } + var treeController by remember { + mutableStateOf( + handleIOException(onCatch = { ex -> + exceptionContent = "Can't open the last tree because: $ex" + throwException = true + }) { + appDataController.loadLastTree() + } ?: newTree(BinSearchTree()) + ) + } + + fun convertKey(keyString: String): Int? { + if (keyString.isEmpty()) { + return null + } + return try { + keyString.toInt() + } catch (ex: NumberFormatException) { + logString = "Can't convert \"$keyString\" to int." + logColor = Color.Red + null + } + } + + fun toTreeRoot() { + val treeRoot = treeController.tree.root + if (treeRoot != null) { + val coordinates = treeController.nodes[treeController.find(treeRoot.element)] + if (coordinates != null) { + logString = "Moved to root." + logColor = Color.Green + treeOffsetX.value = -coordinates.x.value + treeOffsetY.value = -coordinates.y.value + } + } else { + logString = "Current tree is empty." + logColor = Color.Yellow + } + } + + + MenuBar { + Menu("File", mnemonic = 'F') { + Menu("New tree", mnemonic = 'N') { + Item("Bin Search Tree", onClick = { treeController = newTree(BinSearchTree()) }) + Item("Red black Tree", onClick = { treeController = newTree(RBTree()) }) + Item("AVL Tree") { treeController = newTree(AVLTree()) } + } + Menu("Open", mnemonic = 'O') { + Item("Bin Search Tree", onClick = { + val tc = handleIOException(onCatch = { ex -> + exceptionContent = "Failed to import tree from file: ${ex.message}" + throwException = true + }) { + importBST() + } + if (tc != null) { + treeController = tc + } + }) + Item("Red black Tree", onClick = { dialogType = DialogType.IMPORT_RB }) + Item("AVL Tree", onClick = { + val tc = handleIOException(onCatch = { ex -> + exceptionContent = "Failed to import tree from file: ${ex.message}" + throwException = true + }) { + importAVLT() + } + if (tc != null) { + treeController = tc + } + }) + } + Menu("Save", mnemonic = 'S') { + Item( + "Bin Search Tree", + onClick = { + handleIOException(onCatch = { ex -> + exceptionContent = "Failed to import tree from file: ${ex.message}" + throwException = true + }) { + @Suppress("UNCHECKED_CAST") exportBST( + treeController as TreeController>> + ) + } + }, + enabled = (treeController.nodeType() is Node<*>?) + ) + Item( + "Red black Tree", + onClick = { dialogType = DialogType.EXPORT_RB }, + enabled = (treeController.nodeType() is RBNode<*>?) + ) + Item( + "AVL Tree", + onClick = { + handleIOException(onCatch = { ex -> + exceptionContent = "Failed to import tree from file: ${ex.message}" + throwException = true + }) { + @Suppress("UNCHECKED_CAST") exportAVLT( + treeController as TreeController>> + ) + } + }, + enabled = (treeController.nodeType() is AVLNode<*>?) + ) + } + } + } + + + + + Row(modifier = Modifier.background(Color.LightGray)) { + Column(modifier = Modifier.width(widthOfPanel.dp)) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + InsertRow(onClick = { keyString, value -> + val key = convertKey(keyString) ?: return@InsertRow + val treeBeforeInsert = treeController + treeController = treeController.insert(KVP(key, value)) + if (treeBeforeInsert == treeController) { + logString = "The node with key = $key is already in the tree. Nothing has been done." + logColor = Color.Yellow + } else { + logString = "The node with key = $key and value = \"$value\" has been inserted." + logColor = Color.Green + } + }) + Spacer(modifier = Modifier.size(5.dp)) + RemoveRow(onClick = { keyString -> + val key = convertKey(keyString) ?: return@RemoveRow + val treeBeforeInsert = treeController + treeController = treeController.remove(KVP(key)) + if (treeBeforeInsert == treeController) { + logString = "There is no node with key = $key in the tree. Nothing has been done." + logColor = Color.Yellow + } else { + logString = "The node with key = $key has been removed." + logColor = Color.Green + } + }) + Spacer(modifier = Modifier.size(5.dp)) + FindRow(onClick = { keyString -> + val key = convertKey(keyString) ?: return@FindRow + val node = treeController.find(KVP(key)) + if (node != null) { + logString = "Node with key = $key and value = \"${node.element.v}\" found." + logColor = Color.Green + val coordinates = treeController.nodes[node] + if (coordinates != null) { + treeOffsetX.value = -coordinates.x.value + treeOffsetY.value = -coordinates.y.value + } + } else { + logString = "Node with key = $key not found." + logColor = Color.Yellow + } + }) + Spacer(modifier = Modifier.size(5.dp)) + Row(horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier.fillMaxWidth()) { + Button( + onClick = { + treeController = TreeController(treeController.tree) + logString = "Tree coordinates reset." + logColor = Color.Green + }, + colors = ButtonDefaults.buttonColors( + backgroundColor = Color(0xffff5b79), + contentColor = Color.White + ) + ) { + Text("Reset coordinates") + } + Button( + onClick = { + toTreeRoot() + }, + colors = ButtonDefaults.buttonColors( + backgroundColor = Color.Gray, + contentColor = Color.White + ) + ) { + Text("To root") + } + } + } + Spacer(Modifier.height(5.dp)) + Row(horizontalArrangement = Arrangement.Center) { + Spacer(Modifier.width(5.dp)) + Box( + Modifier.background(color = Color.Gray.copy(alpha = 0.5f), shape = RoundedCornerShape(5.dp)) + .height(3.dp).fillMaxWidth() + ) + Spacer(Modifier.width(5.dp)) + } + Spacer(Modifier.height(5.dp)) + Logger(logString, logColor) + } + + Spacer(modifier = Modifier.width(3.dp)) + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxHeight()) { + Box( + Modifier.background(color = Color.Gray, shape = RoundedCornerShape(5.dp)).size(5.dp, 100.dp) + .draggable( + orientation = androidx.compose.foundation.gestures.Orientation.Horizontal, + state = rememberDraggableState { delta -> + val newWidth = widthOfPanel + delta.toInt() + if ((windowState.size.width > (newWidth + 10).dp) && (newWidth > 0)) { + widthOfPanel = newWidth + } + } + ) + ) + } + Spacer(modifier = Modifier.width(3.dp)) + + TreeView(treeController, treeOffsetX, treeOffsetY) + + } + + when (dialogType) { + DialogType.EMPTY -> { + } + + DialogType.IMPORT_RB -> { + ImportRBDialog( + onCloseRequest = { dialogType = DialogType.EMPTY }, + onSuccess = { newTreeController, treeName -> + treeController = newTreeController + appDataController.data.lastTree = SavedTree(SavedType.Neo4j, treeName) + appDataController.saveData() + }) + } + + DialogType.EXPORT_RB -> { + @Suppress("UNCHECKED_CAST") + ExportRBDialog( + onCloseRequest = { dialogType = DialogType.EMPTY }, + onSuccess = { treeName -> + appDataController.data.lastTree = SavedTree(SavedType.Neo4j, treeName) + appDataController.saveData() + }, + treeController as TreeController>> + ) + } + } + + AlertDialog(exceptionContent, throwException, onCloseRequest = { throwException = false }) + ExitDialog( + showExitDialog, + onCloseRequest = { showExitDialog = false }, + onExitRequest = ::exitApplication + ) { + if (treeController.nodeType() is Node<*>?) { + Button(onClick = { + handleIOException(onCatch = { ex -> + exceptionContent = "Failed to import tree from file: ${ex.message}" + throwException = true + }) { + @Suppress("UNCHECKED_CAST") exportBST(treeController as TreeController>>) + } + }) { + Text("Save as Bin Search Tree") + } + } + if (treeController.nodeType() is AVLNode<*>?) { + Button(onClick = { + handleIOException(onCatch = { ex -> + exceptionContent = "Failed to import tree from file: ${ex.message}" + throwException = true + }) { + @Suppress("UNCHECKED_CAST") exportAVLT(treeController as TreeController>>) + } + }) { + Text("Save as AVL Tree") + } + } + if (treeController.nodeType() is RBNode<*>?) { + Button(onClick = { + dialogType = DialogType.EXPORT_RB + }) { + Text("Save as RB Tree") + } + } + } + } +} diff --git a/app/src/main/kotlin/org/tree/app/controller/TreeController.kt b/app/src/main/kotlin/org/tree/app/controller/TreeController.kt new file mode 100644 index 00000000..2a58322f --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/controller/TreeController.kt @@ -0,0 +1,110 @@ +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color +import org.tree.binaryTree.AVLNode +import org.tree.binaryTree.KVP +import org.tree.binaryTree.Node +import org.tree.binaryTree.RBNode +import org.tree.binaryTree.templates.TemplateBSTree +import org.tree.binaryTree.templates.TemplateNode +import kotlin.random.Random + +data class NodeExtension(var x: MutableState, var y: MutableState, var color: Color = Color.Gray) +class TreeController, NODE_T>>( + val tree: TemplateBSTree, NODE_T>, + val nodeSize: Int = 50 +) { + val nodes = mutableMapOf() + + init { + val nodesWithWidths = mutableMapOf>() + tree.root?.let { + countWidth(it, nodesWithWidths) + getCoordinatesOfNode(it, 0, 0, nodesWithWidths) + } + } + + private fun countWidth(node: NODE_T?, map: MutableMap>): Int { + var leftWidth = 0 + var rightWidth = 0 + if (node?.left != null) { + leftWidth = countWidth(node.left, map) + 1 + } + if (node?.right != null) { + rightWidth = countWidth(node.right, map) + 1 + } + if (node != null) map[node] = Pair(leftWidth, rightWidth) + return (leftWidth + rightWidth) + } + + private fun getCoordinatesOfNode(node: NODE_T, x: Int, y: Int, mapOfWidths: MutableMap>) { + val stateX = mutableStateOf(x) + val stateY = mutableStateOf(y) + val col = getNodeCol(node) + nodes[node] = NodeExtension(stateX, stateY, col) + + node.left?.let { + val count = mapOfWidths[it]?.second ?: 0 + getCoordinatesOfNode(it, x - nodeSize - (count * nodeSize), y + nodeSize, mapOfWidths) + } + + node.right?.let { + val count = mapOfWidths[it]?.first ?: 0 + getCoordinatesOfNode(it, x + nodeSize + (count * nodeSize), y + nodeSize, mapOfWidths) + } + } + + fun insert(obj: KVP): TreeController { + var res = this + val isChanged = tree.insert(obj) + if (isChanged) { + res = TreeController(tree, nodeSize) + } + return res + } + + fun remove(obj: KVP): TreeController { + var res = this + val isChanged = tree.remove(obj) + if (isChanged) { + res = TreeController(tree, nodeSize) + } + return res + } + + fun find(obj: KVP): NODE_T? { + return tree.find(obj) + } + + fun getNodeCol(curNode: NODE_T): Color { + return if (curNode is RBNode<*>) { + if (curNode.color == RBNode.Color.BLACK) { + Color.DarkGray + } else { + Color.Red + } + } else if (curNode is AVLNode<*>) { + Color(0xff875bff) + } else if (curNode is Node<*>) { + Color(0xffffb74b) + } else { + Color.Gray + } + } + + fun nodeType(): NODE_T? { + return tree.root + } + +} + +fun , NODE_T>, TREE_T : TemplateBSTree, NODE_T>> newTree( + emptyTree: TREE_T, + nodesCount: Int = 10 +): TreeController { + val rand = Random(0x1337) + for (i in 1..nodesCount) { + emptyTree.insert(KVP(rand.nextInt(100), "Num: $i")) + } + return TreeController(emptyTree) +} diff --git a/app/src/main/kotlin/org/tree/app/controller/io/AppDataController.kt b/app/src/main/kotlin/org/tree/app/controller/io/AppDataController.kt new file mode 100644 index 00000000..9994ef64 --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/controller/io/AppDataController.kt @@ -0,0 +1,158 @@ +package org.tree.app.controller.io + +import TreeController +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encodeToString +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import newTree +import org.tree.app.appDataController +import org.tree.binaryTree.trees.BinSearchTree +import java.io.File +import java.io.FileNotFoundException +import java.nio.file.Files + +@Serializable(with = Neo4jDataSerializer::class) +class Neo4jData( + urlValue: String = "bolt://localhost:7687", + loginValue: String = "neo4j", + passwordValue: String = "qwertyui" +) { + var url by mutableStateOf(urlValue) + var login by mutableStateOf(loginValue) + var password by mutableStateOf(passwordValue) +} + + +object Neo4jDataSerializer : KSerializer { + @Serializable + class SurrogateNeo4jData(val url: String, val login: String, val password: String) + + override val descriptor: SerialDescriptor = SurrogateNeo4jData.serializer().descriptor + + override fun serialize(encoder: Encoder, value: Neo4jData) { + val surrogate = SurrogateNeo4jData(value.url, value.login, value.password) + encoder.encodeSerializableValue(SurrogateNeo4jData.serializer(), surrogate) + } + + override fun deserialize(decoder: Decoder): Neo4jData { + val surrogate = decoder.decodeSerializableValue(SurrogateNeo4jData.serializer()) + return Neo4jData(surrogate.url, surrogate.login, surrogate.password) + } +} + +enum class SavedType { + SQLite, + Json, + Neo4j +} + +@Serializable +class SavedTree( + val type: SavedType, + val path: String +) + +@Serializable +class AppData { + var neo4j = Neo4jData() + var lastTree: SavedTree? = null +} + +class AppDataController { + val file = getAppDataFile() + var data: AppData + + init { + data = loadData() + saveData() + } + + fun loadData(): AppData { + val appData = try { + val fileContent = file.readText() + Json.decodeFromString(fileContent) + } catch (ex: Exception) { + when (ex) { + is FileNotFoundException, is IllegalArgumentException -> { + AppData() + } + + else -> throw ex + } + } + return appData + } + + fun saveData(appData: AppData = data) { + try { + file.writeText(Json.encodeToString(appData)) + } catch (ex: SecurityException) { + // If we can't create file, we wouldn't create it. + // Because it is bad to throw warning or exception at the start of app + // throw HandledIOException("Directory ${file.toPath()} cannot be created: no access", ex) + } + } + + fun loadLastTree(): TreeController<*> { + var treeController: TreeController<*> = newTree(BinSearchTree()) + data.lastTree?.let { + when (it.type) { + SavedType.SQLite -> { + val db = SQLiteIO() + treeController = db.importTree(File(it.path)) + } + + SavedType.Json -> { + val db = JsonIO() + treeController = db.importTree(File(it.path)) + } + + SavedType.Neo4j -> { + val db = Neo4jIO() + db.open( + appDataController.data.neo4j.url, + appDataController.data.neo4j.login, + appDataController.data.neo4j.password + ) + treeController = db.importRBTree(it.path) + db.close() + } + } + } + return treeController + } + + private fun getAppDataFile(): File { + val workFile = File(getWorkingDirPath() + "appData.json") + try { + Files.createDirectories(workFile.toPath().parent) + workFile.createNewFile() + } catch (ex: SecurityException) { + // If we can't create directory, we wouldn't create it. + // Because it is bad to throw warning or exception at the start of app + // throw HandledIOException("Directory ${workFile.toPath().parent} cannot be created: no access", ex) + } + return workFile + } + + private fun getWorkingDirPath(): String { + val os = (System.getProperty("os.name")).uppercase() + var workingDirectory: String + if (os.contains("WIN")) { + workingDirectory = System.getenv("AppData") + workingDirectory += "\\trees2\\" + } else { + workingDirectory = System.getProperty("user.home") + workingDirectory += "/.trees2/" + } + return workingDirectory + } +} diff --git a/app/src/main/kotlin/org/tree/app/controller/io/HandledIOException.kt b/app/src/main/kotlin/org/tree/app/controller/io/HandledIOException.kt new file mode 100644 index 00000000..13de3762 --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/controller/io/HandledIOException.kt @@ -0,0 +1,22 @@ +package org.tree.app.controller.io + +import java.io.IOException + +fun handleIOException(onCatch: (HandledIOException) -> Unit, handledCode: () -> T): T? { + try { + return handledCode() + } catch (ex: HandledIOException) { + onCatch(ex) + return null + } +} + +class HandledIOException : IOException { + + constructor() : super() + + constructor(message: String) : super(message) + + constructor(message: String, cause: Throwable) : super(message, cause) + +} diff --git a/app/src/main/kotlin/org/tree/app/controller/io/JsonIO.kt b/app/src/main/kotlin/org/tree/app/controller/io/JsonIO.kt new file mode 100644 index 00000000..69f1570a --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/controller/io/JsonIO.kt @@ -0,0 +1,92 @@ +package org.tree.app.controller.io + +import NodeExtension +import TreeController +import androidx.compose.runtime.mutableStateOf +import kotlinx.serialization.Serializable +import kotlinx.serialization.SerializationException +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.tree.binaryTree.AVLNode +import org.tree.binaryTree.KVP +import org.tree.binaryTree.trees.AVLTree +import java.io.File +import java.io.FileNotFoundException +import java.nio.file.Files + +@Serializable +private data class JsonAVLNode( + val key: Int, + val value: String?, + val x: Int, + val y: Int, + val height: Int, + val left: JsonAVLNode?, + val right: JsonAVLNode? +) + +@Serializable +private data class JsonAVLTree( + val root: JsonAVLNode? +) + +class JsonIO { + + private fun AVLNode>.serialize(treeController: TreeController>>): JsonAVLNode { + return JsonAVLNode( + key = this.element.key, + value = this.element.v, + x = treeController.nodes[this]?.x?.value ?: 0, + y = treeController.nodes[this]?.y?.value ?: 0, + height = this.height, + left = this.left?.serialize(treeController), + right = this.right?.serialize(treeController) + ) + } + + private fun JsonAVLNode.deserialize(treeController: TreeController>>): AVLNode> { + val nv = AVLNode(KVP(key, value)) + nv.height = height + treeController.nodes[nv] = NodeExtension(mutableStateOf(x), mutableStateOf(y), treeController.getNodeCol(nv)) + nv.left = left?.deserialize(treeController) + nv.right = right?.deserialize(treeController) + return nv + } + + fun exportTree(treeController: TreeController>>, file: File) { + try { + Files.createDirectories(file.toPath().parent) + } catch (ex: SecurityException) { + throw HandledIOException("Directory ${file.toPath().parent} cannot be created: no access", ex) + } + + val jsonTree = JsonAVLTree(treeController.tree.root?.serialize(treeController)) + + file.run { + createNewFile() + writeText(Json.encodeToString(jsonTree)) + } + } + + fun importTree(file: File): TreeController>> { + val json = try { + file.readText() + } catch (ex: FileNotFoundException) { + throw HandledIOException("File ${file.toPath().fileName} not found: no access", ex) + } + + val treeController = TreeController(AVLTree()) + val jsonTree = try { + Json.decodeFromString(json) + } catch (ex: IllegalArgumentException) { + throw HandledIOException("This file is not json tree format", ex) + } catch (ex: SerializationException) { + throw HandledIOException("Something go wrong during tree import", ex) + } + + treeController.tree.root = jsonTree.root?.deserialize(treeController) + return treeController + + } +} diff --git a/app/src/main/kotlin/org/tree/app/controller/io/Neo4jIO.kt b/app/src/main/kotlin/org/tree/app/controller/io/Neo4jIO.kt new file mode 100644 index 00000000..19695903 --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/controller/io/Neo4jIO.kt @@ -0,0 +1,292 @@ +package org.tree.app.controller.io + +import NodeExtension +import TreeController +import androidx.compose.runtime.mutableStateOf +import org.neo4j.driver.* +import org.neo4j.driver.exceptions.AuthenticationException +import org.neo4j.driver.exceptions.ClientException +import org.neo4j.driver.exceptions.ServiceUnavailableException +import org.neo4j.driver.exceptions.SessionExpiredException +import org.neo4j.driver.exceptions.value.Uncoercible +import org.tree.binaryTree.KVP +import org.tree.binaryTree.RBNode +import org.tree.binaryTree.trees.RBTree +import java.io.Closeable + +class Neo4jIO() : Closeable { + private var driver: Driver? = null + + private companion object { + // Labels + const val RBNODE = "RBNode" + const val TREE = "Tree" + const val NEW_NODE = "NewNode" + + // Links + const val ROOT = "ROOT" + const val LCHILD = "LEFT_CHILD" + const val RCHILD = "RIGHT_CHILD" + } + + fun exportRBTree( + treeController: TreeController>>, + treeName: String = "Tree" + ) { + val session = driver?.session() ?: throw HandledIOException("Driver is not open") + val root = treeController.tree.root + handleTransactionException { + session.executeWrite { tx -> + deleteTree(tx, treeName) + exportRBNode(tx, root, treeController.nodes) + tx.run( + "MATCH (p: $RBNODE) " + + "MATCH (l: $NEW_NODE {key: p.lkey}) " + + "CREATE (p)-[: $LCHILD]->(l) " + + "REMOVE p.lkey, l: $NEW_NODE" + ) // connect parent and left child + tx.run( + "MATCH (p: $RBNODE) " + + "MATCH (r: $NEW_NODE {key: p.rkey}) " + + "CREATE (p)-[: $RCHILD]->(r) " + + "REMOVE p.rkey, r: $NEW_NODE" + )// connect parent and right child + + tx.run( + "CREATE (t: $TREE {name: \$treeName})", mutableMapOf( + "treeName" to treeName + ) as Map? + ) // create tree label + + tx.run( + "MATCH (r: $NEW_NODE) " + + "MATCH (t: $TREE {name: \$treeName}) " + + "CREATE (t)-[:$ROOT]->(r) " + + "REMOVE r:$NEW_NODE", + mutableMapOf( + "treeName" to treeName + ) as Map + )// connect tree and root + + } + } + session.close() + } + + + fun importRBTree(treeName: String = "Tree"): TreeController>> { + val session = driver?.session() ?: throw HandledIOException("Driver is not open") + val res: TreeController>> = + handleTransactionException { + session.executeRead { tx -> + val tree = RBTree>() + val treeController = TreeController(tree) + importRBNodes(tx, treeController, treeName) + treeController + } + } + session.close() + return res + } + + fun removeTree(treeName: String = "Tree") { + val session = driver?.session() ?: throw HandledIOException("Driver is not open") + handleTransactionException { + session.executeWrite { tx -> + deleteTree(tx, treeName) + } + } + session.close() + } + + fun getTreesNames(): MutableList { + val session = driver?.session() ?: throw HandledIOException("Driver is not open") + val res: MutableList = handleTransactionException { + session.executeRead { tx -> + val nameRecords = tx.run("MATCH (t: $TREE) RETURN t.name AS name") + parseNames(nameRecords) + } + } + session.close() + return res + } + + private fun deleteTree(tx: TransactionContext, treeName: String) { + tx.run( + "MATCH (t: $TREE {name: \$treeName})" + + "OPTIONAL MATCH (t)-[*]->(n:$RBNODE) " + + "DETACH DELETE t, n", + mutableMapOf( + "treeName" to treeName + ) as Map + ) // delete tree and all its nodes + } + + private fun exportRBNode( + tx: TransactionContext, + curNode: RBNode>?, + nodes: MutableMap>, NodeExtension> + ) { + if (curNode != null) { + val lkey = curNode.left?.element?.key + val rkey = curNode.right?.element?.key + val ext = nodes[curNode] + if (ext == null) { + throw HandledIOException("Can't find coordinates for node with key ${curNode.element.key}") + } else { + tx.run( + "CREATE (:$RBNODE:$NEW_NODE {key : \$key, " + + "value: \$value, " + + "x: \$x, y: \$y, " + + "isBlack: \$isBlack, " + + "lkey: \$lkey, " + + "rkey: \$rkey" + + "})", + mutableMapOf( + "key" to curNode.element.key, + "value" to (curNode.element.v ?: ""), + "x" to ext.x.value, "y" to ext.y.value, + "isBlack" to (curNode.color == RBNode.Color.BLACK), + "lkey" to lkey, + "rkey" to rkey + ) as Map + ) + exportRBNode(tx, curNode.left, nodes) + exportRBNode(tx, curNode.right, nodes) + } + } + } + + private fun importRBNodes( + tx: TransactionContext, + treeController: TreeController>>, + treeName: String + ) { + val nodeAndKeysRecords = tx.run( + "MATCH (:$TREE {name: \$treeName})-[*]->(p: $RBNODE)" + + "OPTIONAL MATCH (p)-[: $LCHILD]->(l: $RBNODE) " + + "OPTIONAL MATCH (p)-[: $RCHILD]->(r: $RBNODE) " + + "RETURN p.x AS x, p.y AS y, p.isBlack AS isBlack, p.key AS key, p.value AS value, " + + " l.key AS lKey, r.key AS rKey", + mutableMapOf( + "treeName" to treeName + ) as Map + ) // for all nodes get their properties + keys of their children + return parseRBNodes(nodeAndKeysRecords, treeController) + } + + private class NodeAndKeys( + val nd: RBNode>, + val lkey: Int?, + val rkey: Int? + ) + + private fun parseRBNodes(nodeAndKeysRecords: Result, treeController: TreeController>>) { + val key2nk = mutableMapOf() + for (nkRecord in nodeAndKeysRecords) { + try { + val key = nkRecord["key"].asInt() + val value = nkRecord["value"].asString() + val node = RBNode(null, KVP(key, value)) + + val x = nkRecord["x"].asInt() + val y = nkRecord["y"].asInt() + + val isBlack = nkRecord["isBlack"].asBoolean() + node.color = if (isBlack) { + RBNode.Color.BLACK + } else { + RBNode.Color.RED + } + + treeController.nodes[node] = + NodeExtension(mutableStateOf(x), mutableStateOf(y), treeController.getNodeCol(node)) + + val lkey = if (nkRecord["lKey"].isNull) { + null + } else { + nkRecord["lKey"].asInt() + } + val rkey = if (nkRecord["rKey"].isNull) { + null + } else { + nkRecord["rKey"].asInt() + } + + key2nk[key] = NodeAndKeys(node, lkey, rkey) + } catch (ex: Uncoercible) { + throw HandledIOException("Invalid nodes label in the database", ex) + } + } + val nks = key2nk.values.toTypedArray() + + if (key2nk.isEmpty()) { // tree was empty + treeController.tree.root = null + } else { + for (nk in nks) { + nk.lkey?.let { + nk.nd.left = key2nk[it]?.nd + nk.nd.left?.parent = nk.nd + key2nk.remove(it) + } + + nk.rkey?.let { + nk.nd.right = key2nk[it]?.nd + nk.nd.right?.parent = nk.nd + key2nk.remove(it) + } + } + if (key2nk.values.size != 1) { + throw HandledIOException("Found ${key2nk.values.size} nodes without parents in database, expected only 1 node") + } + treeController.tree.root = key2nk.values.first().nd + } + } + + private fun parseNames(nameRecords: Result): MutableList { + val res = mutableListOf() + for (nmRecord in nameRecords) { + try { + val name = nmRecord["name"].asString() + res.add(name) + } catch (ex: Uncoercible) { + throw HandledIOException("Invalid tree label in the database", ex) + } + } + return res + } + + fun open(uri: String, username: String, password: String) { + try { + driver = GraphDatabase.driver(uri, AuthTokens.basic(username, password)) + } catch (ex: IllegalArgumentException) { + throw HandledIOException("Wrong URI", ex) + } catch (ex: SessionExpiredException) { + throw HandledIOException("Session failed, try restarting the app", ex) + } + sendTestQuery() + } + + override fun close() { + driver?.close() + } + + private fun sendTestQuery() { + getTreesNames() + } + + private fun handleTransactionException(transaction: () -> T): T { + try { + return transaction() + } catch (ex: AuthenticationException) { + throw HandledIOException("Wrong username or password", ex) + } catch (ex: ClientException) { + println(ex.message) + throw HandledIOException("Use the bolt:// URI scheme or some other expected labels", ex) + } catch (ex: ServiceUnavailableException) { + throw HandledIOException("Check your network connection", ex) + } + } +} + + diff --git a/app/src/main/kotlin/org/tree/app/controller/io/SQLiteIO.kt b/app/src/main/kotlin/org/tree/app/controller/io/SQLiteIO.kt new file mode 100644 index 00000000..de498c1d --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/controller/io/SQLiteIO.kt @@ -0,0 +1,184 @@ +package org.tree.app.controller.io + +import NodeExtension +import TreeController +import androidx.compose.runtime.mutableStateOf +import org.jetbrains.exposed.dao.IntEntity +import org.jetbrains.exposed.dao.IntEntityClass +import org.jetbrains.exposed.dao.id.EntityID +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.exceptions.ExposedSQLException +import org.jetbrains.exposed.sql.Database +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.exists +import org.jetbrains.exposed.sql.transactions.transaction +import org.tree.binaryTree.KVP +import org.tree.binaryTree.Node +import org.tree.binaryTree.trees.BinSearchTree +import java.io.File +import java.nio.file.Files +import java.sql.SQLException + +object Nodes : IntIdTable() { + val key = integer("key") + val parentKey = integer("parentKey").nullable() + val value = text("value", eagerLoading = true) + val x = integer("x") + val y = integer("y") +} + +class InstanceOfNode(id: EntityID) : IntEntity(id) { // A separate row with data in SQLite + companion object : IntEntityClass(Nodes) + + var key by Nodes.key + var parentKey by Nodes.parentKey + var value by Nodes.value + var x by Nodes.x + var y by Nodes.y +} + +class SQLiteIO { + fun exportTree(treeController: TreeController>>, file: File) { + try { + Files.createDirectories(file.toPath().parent) + } catch (ex: SecurityException) { + throw HandledIOException("Directory ${file.toPath().parent} cannot be created: no access", ex) + } + Database.connect("jdbc:sqlite:${file.path}", "org.sqlite.JDBC") + transaction { + try { + SchemaUtils.drop(Nodes) + SchemaUtils.create(Nodes) + } catch (ex: ExposedSQLException) { + throw HandledIOException("File is not a database", ex) + } + val root = treeController.tree.root + if (root != null) { + parseNodesForExport(root, null, treeController) + } + } + } + + fun importTree(file: File): TreeController>> { + val treeController = TreeController(BinSearchTree()) + val setOfNodes = getSetOfNodesFromDB(file) + val amountOfNodes = setOfNodes.count() + if (amountOfNodes > 0) { + parseRootForImport(setOfNodes, treeController) + } + return treeController + } + + private fun parseNodesForExport( + curNode: Node>, + parNode: Node>?, + treeController: TreeController>> + ) { + InstanceOfNode.new { + key = curNode.element.key + parentKey = parNode?.element?.key + value = curNode.element.v.toString() + x = treeController.nodes[curNode]?.x?.value ?: 0 + y = treeController.nodes[curNode]?.y?.value ?: 0 + } + val leftChild = curNode.left + if (leftChild != null) { + parseNodesForExport(leftChild, curNode, treeController) + } + val rightChild = curNode.right + if (rightChild != null) { + parseNodesForExport(rightChild, curNode, treeController) + } + } + + private fun getSetOfNodesFromDB(file: File): MutableSet { + Database.connect("jdbc:sqlite:${file.path}", "org.sqlite.JDBC") + return transaction { + try { + if (Nodes.exists()) { + InstanceOfNode.all().toMutableSet() + } else { + throw HandledIOException("Database without a Nodes table") + } + } catch (ex: SQLException) { + throw HandledIOException("File is not a SQLite database", ex) + } + } + } + + private fun parseRootForImport( + setOfNodes: MutableSet, + treeController: TreeController>> + ) { + try { + val node = setOfNodes.elementAt(0) + setOfNodes.remove(node) + val parsedKey = node.key + val parsedValue = node.value + val parsedX = node.x + val parsedY = node.y + val bst = treeController.tree + bst.insert(KVP(parsedKey, parsedValue)) + val root = bst.root + if (root != null) { + addCoordinatesToNode(root, parsedX, parsedY, treeController) + parseNodesForImport(setOfNodes, root, treeController) + if (setOfNodes.isNotEmpty()) { + throw HandledIOException("Incorrect binary tree: there are at least two left/right children of some node") + } + } + } catch (ex: NumberFormatException) { + throw HandledIOException( + "Node keys must be integers, value must be string and coordinates must be doubles", + ex + ) + } + } + + private fun parseNodesForImport( + setOfNodes: MutableSet, + curNode: Node>, + treeController: TreeController>> + ) { + if (setOfNodes.size == 0) { + return + } + val nodes = setOfNodes.elementAt(0) + val parsedKey = nodes.key + val parsedParentKey: Int = + nodes.parentKey ?: throw HandledIOException("Incorrect binary tree: there are at least 2 roots") + val parsedValue = nodes.value + val parsedX = nodes.x + val parsedY = nodes.y + if (parsedKey == parsedParentKey) throw HandledIOException("Child with key = ${curNode.element.key} is parent for himself") + if (parsedParentKey == curNode.element.key) { + val newNode = Node(KVP(parsedKey, parsedValue)) + addCoordinatesToNode(newNode, parsedX, parsedY, treeController) + setOfNodes.remove(nodes) + if (parsedKey < parsedParentKey) { + if (curNode.left != null) throw HandledIOException("Incorrect binary tree: there are at least two left children of node with key = ${curNode.element.key}") + curNode.left = newNode + val leftChild = curNode.left + if (leftChild != null) { + parseNodesForImport(setOfNodes, leftChild, treeController) + } + parseNodesForImport(setOfNodes, curNode, treeController) + } else { // When parsedKey is greater than parsedParentKey + if (curNode.right != null) throw HandledIOException("Incorrect binary tree: there are at least two right children of node with key = ${curNode.element.key}") + curNode.right = newNode + val rightChild = curNode.right + if (rightChild != null) { + parseNodesForImport(setOfNodes, rightChild, treeController) + } + } + } + } + + private fun addCoordinatesToNode( + node: Node>, + x: Int, y: Int, treeController: TreeController>> + ) { + treeController.nodes[node] = + NodeExtension(mutableStateOf(x), mutableStateOf(y), treeController.getNodeCol(node)) + } +} diff --git a/app/src/main/kotlin/org/tree/app/view/AutoSizedText.kt b/app/src/main/kotlin/org/tree/app/view/AutoSizedText.kt new file mode 100644 index 00000000..3a17f254 --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/view/AutoSizedText.kt @@ -0,0 +1,125 @@ +package org.tree.app.view + +import androidx.compose.material.LocalTextStyle +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.TextUnit + +sealed class AutoSizeConstraint(open val min: TextUnit = TextUnit.Unspecified) { + data class Width(override val min: TextUnit = TextUnit.Unspecified) : AutoSizeConstraint(min) + data class Height(override val min: TextUnit = TextUnit.Unspecified) : AutoSizeConstraint(min) +} + +@Composable +fun AutoSizeText( + text: AnnotatedString, + fontSize: TextUnit, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + style: TextStyle = LocalTextStyle.current, + constraint: AutoSizeConstraint = AutoSizeConstraint.Width(), +) { + var newFontSize by remember { mutableStateOf(fontSize) } + var readyToDraw by remember { mutableStateOf(false) } + Text( + modifier = modifier.drawWithContent { + if (readyToDraw) drawContent() + }, + text = text, + color = color, + fontSize = newFontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + style = style, + onTextLayout = { result -> + fun constrain() { + newFontSize *= 0.9f + } + when (constraint) { + is AutoSizeConstraint.Height -> { + if (result.didOverflowHeight) { + constrain() + } else { + readyToDraw = true + } + } + + is AutoSizeConstraint.Width -> { + if (result.didOverflowWidth) { + constrain() + } else { + readyToDraw = true + } + } + } + } + ) +} + +@Composable +fun AutoSizeText( + text: String, + fontSize: TextUnit, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + style: TextStyle = LocalTextStyle.current, + constraint: AutoSizeConstraint = AutoSizeConstraint.Width(), +) { + AutoSizeText( + modifier = modifier, + text = AnnotatedString(text), + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + style = style, + constraint = constraint + ) +} diff --git a/app/src/main/kotlin/org/tree/app/view/ImportRemoveFindRows.kt b/app/src/main/kotlin/org/tree/app/view/ImportRemoveFindRows.kt new file mode 100644 index 00000000..9f7752ff --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/view/ImportRemoveFindRows.kt @@ -0,0 +1,136 @@ +package org.tree.app.view + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.material.Button +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.key.* +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun InsertRow(onClick: (keyString: String, value: String) -> Unit) { + var keyString by remember { mutableStateOf("") } + var valueString by remember { mutableStateOf("") } + + fun execute() { + onClick(keyString, valueString) + keyString = "" + valueString = "" + } + + Row(verticalAlignment = Alignment.CenterVertically) { + + Button( + onClick = { + execute() + }, + modifier = Modifier.weight(0.3f).defaultMinSize(minWidth = 100.dp) + ) { + Text("Insert") + } + OutlinedTextField( + label = { Text("Key") }, + value = keyString, + onValueChange = { keyString = it }, + singleLine = true, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.White.copy(alpha = 0.3f)), + modifier = Modifier.weight(0.35f).onKeyEvent { + if ((it.key == Key.Enter) && (it.type == KeyEventType.KeyDown)) { + execute() + return@onKeyEvent true + } + return@onKeyEvent false + } + ) + OutlinedTextField( + label = { Text("Value") }, + value = valueString, + onValueChange = { valueString = it }, + singleLine = true, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.White.copy(alpha = 0.3f)), + modifier = Modifier.weight(0.35f).onKeyEvent { + if ((it.key == Key.Enter) && (it.type == KeyEventType.KeyDown)) { + execute() + return@onKeyEvent true + } + return@onKeyEvent false + } + ) + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun RemoveRow(onClick: (key: String) -> Unit) { + var keyString by remember { mutableStateOf("") } + + fun execute() { + onClick(keyString) + keyString = "" + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Button( + onClick = { + execute() + }, + modifier = Modifier.weight(0.3f) + ) { + Text("Remove") + } + + OutlinedTextField( + label = { Text("Key") }, + value = keyString, + onValueChange = { keyString = it }, + singleLine = true, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.White.copy(alpha = 0.3f)), + modifier = Modifier.weight(0.7f).onKeyEvent { + if ((it.key == Key.Enter) && (it.type == KeyEventType.KeyDown)) { + execute() + return@onKeyEvent true + } + return@onKeyEvent false + } + ) + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun FindRow(onClick: (key: String) -> (Unit)) { + var keyString by remember { mutableStateOf("") } + + fun execute() { + onClick(keyString) + keyString = "" + } + + Row(verticalAlignment = Alignment.CenterVertically) { + Button(onClick = ::execute, modifier = Modifier.weight(0.3f)) { + Text("Find") + } + OutlinedTextField( + label = { Text("Key") }, + value = keyString, + onValueChange = { keyString = it }, + singleLine = true, + colors = TextFieldDefaults.textFieldColors(backgroundColor = Color.White.copy(alpha = 0.3f)), + modifier = Modifier.weight(0.7f).onKeyEvent { + if ((it.key == Key.Enter) && (it.type == KeyEventType.KeyDown)) { + execute() + return@onKeyEvent true + } + return@onKeyEvent false + } + ) + } +} diff --git a/app/src/main/kotlin/org/tree/app/view/Line.kt b/app/src/main/kotlin/org/tree/app/view/Line.kt new file mode 100644 index 00000000..7e84b660 --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/view/Line.kt @@ -0,0 +1,25 @@ +package org.tree.app.view + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun Line(x0: Int, y0: Int, x1: Int, y1: Int) { + val x0 = x0.dp + val x1 = x1.dp + val y0 = y0.dp + val y1 = y1.dp + Box(modifier = Modifier.offset(x0, y0)) { + Box( + modifier = Modifier.size(x1 - x0, y1 - y0) + .drawBehind { drawLine(Color.Black, Offset.Zero, Offset((x1 - x0).toPx(), (y1 - y0).toPx()), 1f) } + ) + } +} diff --git a/app/src/main/kotlin/org/tree/app/view/Logger.kt b/app/src/main/kotlin/org/tree/app/view/Logger.kt new file mode 100644 index 00000000..e7bb0aea --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/view/Logger.kt @@ -0,0 +1,28 @@ +package org.tree.app.view + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddCircle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun Logger(content: String, col: Color) { + Row(horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically) { + Icon( + Icons.Default.AddCircle, + contentDescription = content, + tint = col, + modifier = Modifier.size(25.dp).padding(start = 10.dp, end = 5.dp), + ) + Text(content) + } +} diff --git a/app/src/main/kotlin/org/tree/app/view/Node.kt b/app/src/main/kotlin/org/tree/app/view/Node.kt new file mode 100644 index 00000000..ada1e6a3 --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/view/Node.kt @@ -0,0 +1,79 @@ +package org.tree.app.view + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.TooltipArea +import androidx.compose.foundation.TooltipPlacement +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun Node( + x: Int, + y: Int, + key: Int, value: String, + onDrag: (PointerInputChange, Offset) -> Unit, + modifier: Modifier = Modifier, + color: Color = Color.Gray, + size: Int = 10, +) { + Box(modifier = Modifier.fillMaxSize()) { + TooltipArea( + tooltip = { + Surface( + modifier = Modifier.shadow(4.dp), + color = Color(255, 255, 210), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = "value: $value", + modifier = Modifier.padding(10.dp) + ) + } + }, + modifier = Modifier.offset((x - size / 2).dp, (y - size / 2).dp), + delayMillis = 600, + tooltipPlacement = TooltipPlacement.CursorPoint( + alignment = Alignment.BottomEnd, + offset = DpOffset(5.dp, 5.dp) + ) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .requiredSize(size.dp).clip(CircleShape).background(color = color) + .pointerInput(onDrag) { + detectDragGestures { change, dragAmount -> + onDrag(change, dragAmount) + } + } + ) { + AutoSizeText( + fontSize = 16.sp, + color = Color.White, + text = key.toString(), + maxLines = 1, + softWrap = false + ) + } + } + } + +} diff --git a/app/src/main/kotlin/org/tree/app/view/Tree.kt b/app/src/main/kotlin/org/tree/app/view/Tree.kt new file mode 100644 index 00000000..2f628389 --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/view/Tree.kt @@ -0,0 +1,83 @@ +package org.tree.app.view + +import TreeController +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import org.tree.binaryTree.KVP +import org.tree.binaryTree.templates.TemplateNode + +@Composable +fun , NODE_T>> TreeView( + treeController: TreeController, + offsetX: MutableState, + offsetY: MutableState, +) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.background(Color.White, shape = RoundedCornerShape(16.dp)).clipToBounds().fillMaxSize() + .pointerInput( + offsetX, offsetY + ) { + detectDragGestures { change, dragAmount -> + change.consume() + offsetX.value += dragAmount.x.toInt() + offsetY.value += dragAmount.y.toInt() + } + }) + { + Box(modifier = Modifier.size(treeController.nodeSize.dp)) { + for (n in treeController.nodes) { + val x by n.value.x + val y by n.value.y + + with(n.key) { + val l = treeController.nodes[left] + if (l != null) { + Line(x + offsetX.value, y + offsetY.value, l.x.value + offsetX.value, l.y.value + offsetY.value) + } + + val r = treeController.nodes[right] + if (r != null) { + Line(x + offsetX.value, y + offsetY.value, r.x.value + offsetX.value, r.y.value + offsetY.value) + } + } + } + + for (n in treeController.nodes) { + var x by n.value.x + var y by n.value.y + val col = n.value.color + with(n.key.element) { + key(key) { + Node( + x + offsetX.value, + y + offsetY.value, + key, + v ?: "", + size = treeController.nodeSize, + color = col, + onDrag = { change, dragAmount -> + change.consume() + x += dragAmount.x.toInt() + y += dragAmount.y.toInt() + }) + } + } + } + } + } + + +} + diff --git a/app/src/main/kotlin/org/tree/app/view/dialogs/AlertDialog.kt b/app/src/main/kotlin/org/tree/app/view/dialogs/AlertDialog.kt new file mode 100644 index 00000000..f9e00ddd --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/view/dialogs/AlertDialog.kt @@ -0,0 +1,39 @@ +package org.tree.app.view.dialogs + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.rememberDialogState + + +@Composable +fun AlertDialog(description: String, isOpen: Boolean, onCloseRequest: () -> Unit) { + if (isOpen) { + Dialog(state = rememberDialogState(width = 500.dp, height = 250.dp), + title = "Something go wrong...", + onCloseRequest = { + onCloseRequest() + }) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.weight(0.1f)) + Row { + Spacer(modifier = Modifier.weight(0.1f)) + Text(description, modifier = Modifier.weight(1.0f)) + Spacer(modifier = Modifier.weight(0.1f)) + } + + Spacer(modifier = Modifier.weight(0.1f)) + Button(onClick = { onCloseRequest() }) { + Text("Ok") + } + } + } + } +} diff --git a/app/src/main/kotlin/org/tree/app/view/dialogs/ExitDialog.kt b/app/src/main/kotlin/org/tree/app/view/dialogs/ExitDialog.kt new file mode 100644 index 00000000..d3e131aa --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/view/dialogs/ExitDialog.kt @@ -0,0 +1,69 @@ +package org.tree.app.view.dialogs + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.rememberDialogState + +@Composable +fun ExitDialog( + showExitDialog: Boolean, + onCloseRequest: () -> Unit, + onExitRequest: () -> Unit, + additionalButtons: @Composable () -> Unit +) { + if (showExitDialog) { + Dialog( + title = "Confirm exit", + onCloseRequest = { onCloseRequest() }, + state = rememberDialogState(width = 750.dp, height = 300.dp) + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.weight(0.1f)) + Row { + Spacer(modifier = Modifier.weight(0.1f)) + Text( + "Are you sure you want to exit?", + textAlign = TextAlign.Center, + modifier = Modifier.weight(1.0f) + ) + Spacer(modifier = Modifier.weight(0.1f)) + } + + Spacer(modifier = Modifier.weight(0.1f)) + + Column { + Row(horizontalArrangement = Arrangement.SpaceAround, modifier = Modifier.fillMaxWidth()) { + additionalButtons() + } + Row(horizontalArrangement = Arrangement.SpaceAround, modifier = Modifier.fillMaxWidth()) { + Button( + onClick = { onCloseRequest() }, colors = ButtonDefaults.buttonColors( + backgroundColor = Color.Gray, + contentColor = Color.White + ) + ) { + Text("Cancel") + } + Button( + onClick = { onExitRequest() }, colors = ButtonDefaults.buttonColors( + backgroundColor = Color(0xffff5b79), + contentColor = Color.White + ) + ) { + Text("Exit") + } + } + } + } + } + } +} diff --git a/app/src/main/kotlin/org/tree/app/view/dialogs/io/FilesIOHandler.kt b/app/src/main/kotlin/org/tree/app/view/dialogs/io/FilesIOHandler.kt new file mode 100644 index 00000000..0c180c00 --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/view/dialogs/io/FilesIOHandler.kt @@ -0,0 +1,73 @@ +package org.tree.app.view.dialogs.io + +import TreeController +import androidx.compose.ui.awt.ComposeWindow +import org.tree.app.appDataController +import org.tree.app.controller.io.JsonIO +import org.tree.app.controller.io.SQLiteIO +import org.tree.app.controller.io.SavedTree +import org.tree.app.controller.io.SavedType +import org.tree.binaryTree.AVLNode +import org.tree.binaryTree.KVP +import org.tree.binaryTree.Node +import java.awt.FileDialog +import java.io.File + +fun importAVLT(): TreeController>>? { + val fileString = selectFile("json", Mode.IMPORT) ?: return null + val file = File(fileString) + val db = JsonIO() + val treeController = db.importTree(file) + appDataController.data.lastTree = SavedTree(SavedType.Json, file.path) + appDataController.saveData() + return treeController +} + +fun exportAVLT(tc: TreeController>>) { + val fileString = selectFile("json", Mode.EXPORT) ?: return + val file = File(fileString) + val db = JsonIO() + db.exportTree(tc, file) + appDataController.data.lastTree = SavedTree(SavedType.Json, file.path) + appDataController.saveData() +} + +fun importBST(): TreeController>>? { + val fileString = selectFile("sqlite", Mode.IMPORT) ?: return null + val file = File(fileString) + val db = SQLiteIO() + val treeController = db.importTree(file) + appDataController.data.lastTree = SavedTree(SavedType.SQLite, file.path) + appDataController.saveData() + return treeController +} + +fun exportBST(tc: TreeController>>) { + val fileString = selectFile("sqlite", Mode.EXPORT) ?: return + val file = File(fileString) + val db = SQLiteIO() + db.exportTree(tc, file) + appDataController.data.lastTree = SavedTree(SavedType.SQLite, file.path) + appDataController.saveData() +} + +enum class Mode { + IMPORT, + EXPORT +} + +fun selectFile(fileExtension: String, mode: Mode): String? { + val fd = if (mode == Mode.IMPORT) { + FileDialog(ComposeWindow(), "Choose .$fileExtension file to import", FileDialog.LOAD) + } else { + FileDialog(ComposeWindow(), "Choose .$fileExtension file to export", FileDialog.SAVE) + } + fd.directory = "C:\\" + fd.file = "*.$fileExtension" + fd.isVisible = true + val fileString = fd.directory + fd.file + if (fileString != "nullnull") { + return fd.directory + fd.file + } + return null +} diff --git a/app/src/main/kotlin/org/tree/app/view/dialogs/io/Neo4jConnect.kt b/app/src/main/kotlin/org/tree/app/view/dialogs/io/Neo4jConnect.kt new file mode 100644 index 00000000..2ae16f8a --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/view/dialogs/io/Neo4jConnect.kt @@ -0,0 +1,68 @@ +package org.tree.app.view.dialogs.io + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material.Button +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.input.PasswordVisualTransformation +import org.tree.app.appDataController +import org.tree.app.controller.io.HandledIOException +import org.tree.app.controller.io.Neo4jIO +import org.tree.app.view.Logger + + +@Composable +fun Neo4jConnect(onSuccess: (Neo4jIO) -> Unit, onFail: (Neo4jIO) -> Unit) { + var connectionStatus by remember { mutableStateOf("Not connected") } + var iconColor by remember { mutableStateOf(Color.Red) } + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + OutlinedTextField( + label = { Text("Url") }, + value = appDataController.data.neo4j.url, + onValueChange = { appDataController.data.neo4j.url = it }, + singleLine = true + ) + OutlinedTextField( + label = { Text("Username") }, + value = appDataController.data.neo4j.login, + onValueChange = { appDataController.data.neo4j.login = it }, + singleLine = true + ) + OutlinedTextField( + label = { Text("Password") }, + value = appDataController.data.neo4j.password, + onValueChange = { appDataController.data.neo4j.password = it }, + visualTransformation = PasswordVisualTransformation(), + singleLine = true + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Button(onClick = { + val db = Neo4jIO() + appDataController.saveData() + try { + db.open( + appDataController.data.neo4j.url, + appDataController.data.neo4j.login, + appDataController.data.neo4j.password + ) + } catch (ex: HandledIOException) { + connectionStatus = ex.toString() + iconColor = Color.Yellow + onFail(db) + return@Button + } + iconColor = Color.Green + connectionStatus = "Connected" + onSuccess(db) + }) { + Text("Connect") + } + Logger(connectionStatus, iconColor) + } + } +} diff --git a/app/src/main/kotlin/org/tree/app/view/dialogs/io/Neo4jDialogs.kt b/app/src/main/kotlin/org/tree/app/view/dialogs/io/Neo4jDialogs.kt new file mode 100644 index 00000000..769ed224 --- /dev/null +++ b/app/src/main/kotlin/org/tree/app/view/dialogs/io/Neo4jDialogs.kt @@ -0,0 +1,188 @@ +package org.tree.app.view.dialogs.io + +import TreeController +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +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 androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.rememberDialogState +import org.tree.app.controller.io.Neo4jIO +import org.tree.app.controller.io.handleIOException +import org.tree.app.view.dialogs.AlertDialog +import org.tree.binaryTree.KVP +import org.tree.binaryTree.RBNode + +@Composable +fun ImportRBDialog( + onCloseRequest: () -> Unit, + onSuccess: (newTreeController: TreeController>>, treeName: String) -> Unit +) { + var throwException by remember { mutableStateOf(false) } + var exceptionContent by remember { mutableStateOf("Nothing...") } + + AlertDialog(exceptionContent, throwException, { throwException = false }) + Neo4jIODialog("Import RBTree", onCloseRequest = onCloseRequest) { enabled, closeRequest, db, treeName -> + Button( + enabled = enabled, + onClick = { + val treeController = + handleIOException(onCatch = { + exceptionContent = it.toString() + throwException = true + }) { db.importRBTree(treeName) } + if (treeController != null) { + db.close() + onSuccess(treeController, treeName) + closeRequest() + } + } + ) { + Text("Import") + } + } +} + +@Composable +fun ExportRBDialog( + onCloseRequest: () -> Unit, + onSuccess: (treeName: String) -> Unit, + treeController: TreeController>> +) { + + var throwException by remember { mutableStateOf(false) } + var exceptionContent by remember { mutableStateOf("Nothing...") } + + AlertDialog(exceptionContent, throwException, { throwException = false }) + Neo4jIODialog("Export RBTree", onCloseRequest = onCloseRequest) { enabled, closeRequest, db, treeName -> + Button( + enabled = enabled, + onClick = { + handleIOException(onCatch = { + exceptionContent = it.toString() + throwException = true + }) { db.exportRBTree(treeController, treeName) } + if (!throwException) { + db.close() + onSuccess(treeName) + closeRequest() + } + } + ) { + Text("Export") + } + } +} + +@Composable +fun Neo4jIODialog( + title: String, + onCloseRequest: () -> Unit, + button: @Composable() ((enabled: Boolean, closeRequest: () -> Unit, db: Neo4jIO, treeName: String) -> Unit + ) +) { + var isDialogOpen by remember { mutableStateOf(true) } + var isDBEnable by remember { mutableStateOf(false) } + var expandMenu by remember { mutableStateOf(false) } + var treeName by remember { mutableStateOf("Tree") } + var throwException by remember { mutableStateOf(false) } + var exceptionContent by remember { mutableStateOf("Nothing...") } + + AlertDialog(exceptionContent, throwException, { throwException = false }) + + var db = Neo4jIO() + + if (isDialogOpen) { + Dialog(state = rememberDialogState(width = 500.dp, height = 500.dp), + title = title, + onCloseRequest = { + isDialogOpen = false + }) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Neo4jConnect( + onSuccess = { dataBase -> + db = dataBase + isDBEnable = true + val names = handleIOException( + onCatch = { + exceptionContent = it.toString() + throwException = true + } + ) { db.getTreesNames() } + if (names != null) { + if (names.size > 0) + treeName = names[0] + } + }, + onFail = { + db = it + isDBEnable = false + }) + + Box { + Row { + OutlinedTextField( + enabled = isDBEnable, + value = treeName, + onValueChange = { treeName = it }, + ) + IconButton( + enabled = isDBEnable, + onClick = { expandMenu = true } + ) { + Icon(Icons.Default.MoreVert, contentDescription = "Show tree names in db") + } + } + + DropdownMenu( + expanded = expandMenu, + onDismissRequest = { expandMenu = false } + ) { + if (isDBEnable) { + val names = handleIOException( + onCatch = { + exceptionContent = it.toString() + throwException = true + } + ) { db.getTreesNames() } + if (names != null) { + for (name in names) { + Text(name, modifier = Modifier.padding(10.dp).clickable(onClick = { + treeName = name + expandMenu = false + })) + } + } + } + } + } + + Row { + Spacer(modifier = Modifier.weight(0.1f)) + Button( + onClick = { + isDialogOpen = false + }, + colors = ButtonDefaults.buttonColors(backgroundColor = Color.LightGray) + ) { + Text("Cancel") + } + Spacer(modifier = Modifier.weight(1f)) + button(isDBEnable, { isDialogOpen = false }, db, treeName) + Spacer(modifier = Modifier.weight(0.1f)) + + } + } + } + } else { + db.close() + onCloseRequest() + } +} + diff --git a/app/src/main/resources/icon.png b/app/src/main/resources/icon.png new file mode 100644 index 00000000..47e9c3b9 Binary files /dev/null and b/app/src/main/resources/icon.png differ diff --git a/binaryTree/build.gradle.kts b/binaryTree/build.gradle.kts new file mode 100644 index 00000000..33cf2ad8 --- /dev/null +++ b/binaryTree/build.gradle.kts @@ -0,0 +1,7 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + id("org.tree.kotlin-library-conventions") +} diff --git a/binaryTree/src/main/kotlin/org/tree/binaryTree/KeyValuePair.kt b/binaryTree/src/main/kotlin/org/tree/binaryTree/KeyValuePair.kt new file mode 100644 index 00000000..d9640bbf --- /dev/null +++ b/binaryTree/src/main/kotlin/org/tree/binaryTree/KeyValuePair.kt @@ -0,0 +1,7 @@ +package org.tree.binaryTree + +data class KVP, V>(val key: K, var v: V? = null) : Comparable> { + override fun compareTo(other: KVP): Int { + return key.compareTo(other.key) + } +} diff --git a/binaryTree/src/main/kotlin/org/tree/binaryTree/Nodes.kt b/binaryTree/src/main/kotlin/org/tree/binaryTree/Nodes.kt new file mode 100644 index 00000000..80d65074 --- /dev/null +++ b/binaryTree/src/main/kotlin/org/tree/binaryTree/Nodes.kt @@ -0,0 +1,44 @@ +package org.tree.binaryTree + +import org.tree.binaryTree.templates.TemplateNode +import org.tree.binaryTree.trees.AVLTree +import org.tree.binaryTree.trees.BinSearchTree +import org.tree.binaryTree.trees.RBTree + + +/** + * Node for BinSearchTree + * + * @param value the value that the node will contain + * + * @see TemplateNode + * @see BinSearchTree + */ +class Node>(value: T) : TemplateNode>(value) + +/** + * Node for RBTree + * + * @param parent the parent node of the current node + * @param value the value that the node will contain + * + * @see TemplateNode + * @see RBTree + */ +class RBNode>(var parent: RBNode?, value: T) : TemplateNode>(value) { + var color: Color = Color.RED + + enum class Color { RED, BLACK } +} + +/** + * Node for AVLTree + * + * @param value the value that the node will contain + * + * @see TemplateNode + * @see AVLTree + */ +class AVLNode>(value: T) : TemplateNode>(value) { + var height: Int = 1 +} diff --git a/binaryTree/src/main/kotlin/org/tree/binaryTree/templates/TemplateBSTree.kt b/binaryTree/src/main/kotlin/org/tree/binaryTree/templates/TemplateBSTree.kt new file mode 100644 index 00000000..f18c3eba --- /dev/null +++ b/binaryTree/src/main/kotlin/org/tree/binaryTree/templates/TemplateBSTree.kt @@ -0,0 +1,210 @@ +package org.tree.binaryTree.templates + +/** + * This class is template class for creating your own binary search trees. + * @param T the type of element stored in the tree's nodes + * @param NODE_T the type of nodes in the tree + * + * @property root the root node of the tree + */ +abstract class TemplateBSTree, NODE_T : TemplateNode> { + var root: NODE_T? = null + + // Insert + /** + * Insert [newNode] into the subtree of the [curNode]. + * + * @return the parent of the inserted node, + * null if node with the same element already in tree or if inserted node is root + */ + protected open fun insertNode(curNode: NODE_T?, newNode: NODE_T): NODE_T? { + if (curNode == null) { + if (root === curNode) { + root = newNode + return null + } else { + throw IllegalArgumentException("Received a non-root null node") + } + } else { + if (newNode.element < curNode.element) { + if (curNode.left == null) { + curNode.left = newNode + return curNode + } else { + return insertNode(curNode.left, newNode) + } + } else if (newNode.element > curNode.element) { + if (curNode.right == null) { + curNode.right = newNode + return curNode + } else { + return insertNode(curNode.right, newNode) + } + } else { + return null + } + } + } + + /** + * Insert [element] into the subtree of the [curNode]. + * + * @return the parent of the inserted node, + * null if node with the same element already in tree or if inserted node is root + * + * @see insertNode + */ + protected abstract fun insert(curNode: NODE_T?, element: T): NODE_T? + + /** + * Insert [element] into tree. + * + * @return true if the element has been inserted, false if the element is already contained in the tree. + */ + fun insert(element: T): Boolean { + val rootInsert = root == null + // insert returns null if the same element is found or when the root is inserted + return ((insert(root, element) != null) or rootInsert) + } + + // Find + /** + * Find node with the given [element] into subtree of the [curNode]. + * + * @return the found node or null if the node was not found + */ + protected fun find(curNode: NODE_T?, element: T): NODE_T? { + if (curNode == null) { + return null + } + + if (element < curNode.element) { + return find(curNode.left, element) + } else if (element > curNode.element) { + return find(curNode.right, element) + } else { + return curNode + } + } + + /** + * Find node with the given [element] into tree. + * + * @return the found node or null if the node was not found + */ + fun find(element: T): NODE_T? { + return find(root, element) + } + + // Remove + /** + * Delete [curNode] with [parentNode] as parent from tree. + * + * @return the count of null children of deleted node + */ + protected fun deleteNode(curNode: NODE_T, parentNode: NODE_T?): Int { + var res = 0 + if (curNode.left == null) + res += 1 + if (curNode.right == null) + res += 1 + + when (res) { + 0 -> { + val nxt = + findNext(curNode) ?: throw IllegalArgumentException("Got null as next than right child isn't null") + val buf = nxt.element + remove(nxt.element) + curNode.element = buf + } + + 1 -> { + replaceNode(curNode, parentNode, curNode.left ?: curNode.right) + } + + else -> { + replaceNode(curNode, parentNode, null) + } + } + return res + } + + /** + * Remove [element] from subtree of the [curNode] with [parentNode] as parent. + * + * @return the count of null children of deleted node or null if the node was not found + */ + protected open fun remove(curNode: NODE_T?, parentNode: NODE_T?, element: T): Int? { + if (curNode == null) { + return null + } + + if (element < curNode.element) { + return remove(curNode.left, curNode, element) + } else if (element > curNode.element) { + return remove(curNode.right, curNode, element) + } else { + return deleteNode(curNode, parentNode) + } + } + + /** + * Remove [element] from tree. + * + * @return true if the element has been successfully removed; false if it was not present in the tree. + */ + fun remove(element: T): Boolean { + return remove(root, null, element) != null + } + + // Additional + /** + * Find next node after [curNode] + * + * @return node with minimal element that greater than element of current node; + * null if element of current node is the greatest + */ + protected fun findNext(curNode: NODE_T): NODE_T? { + var res = curNode.right + if (res == null) { + return null + } else { + var nextNode = res.left + while (nextNode != null) { + res = nextNode + nextNode = res.left + } + return res + } + } + + /** + * Replace [replacedNode] with [parentNode] as parent by [newNode]. + */ + protected open fun replaceNode(replacedNode: NODE_T, parentNode: NODE_T?, newNode: NODE_T?) { + if (parentNode == null) { + if (root === replacedNode) { + root = newNode + } else { + throw IllegalArgumentException("Received a non-root node with a null parent") + } + } else { + if (parentNode.right === replacedNode) { + parentNode.right = newNode + } else if (parentNode.left === replacedNode) { + parentNode.left = newNode + } else { + throw IllegalArgumentException("Received a node with a wrong parent") + } + } + } + + /** + * @param order specified traversal order + * + * @return a list of tree's elements in the specified order + */ + fun traversal(order: TemplateNode.Traversal): MutableList { + return root?.traverse(order) ?: mutableListOf() + } +} diff --git a/binaryTree/src/main/kotlin/org/tree/binaryTree/templates/TemplateBalanceBSTree.kt b/binaryTree/src/main/kotlin/org/tree/binaryTree/templates/TemplateBalanceBSTree.kt new file mode 100644 index 00000000..ba412bb2 --- /dev/null +++ b/binaryTree/src/main/kotlin/org/tree/binaryTree/templates/TemplateBalanceBSTree.kt @@ -0,0 +1,170 @@ +package org.tree.binaryTree.templates + +/** + * This class is template class for creating your own balance binary search trees. + * @param T the type of element stored in the tree's nodes + * @param NODE_T the type of nodes in the tree + * + * @property balance abstract method that should balance your tree + */ +abstract class TemplateBalanceBSTree, NODE_T : TemplateNode> : + TemplateBSTree() { + + //Balance + /** + * @property ChangedChild what child was changed + * @property Recursive was call recursive or last + * @property OperationType from what method was called + */ + protected class BalanceCase { + /** + * @property LEFT left child was changed + * @property RIGHT right child was changed + * @property ROOT root was changed + */ + enum class ChangedChild { LEFT, RIGHT, ROOT } + + /** + * @property RECURSIVE_CALL the function was called recursively for traverse + * @property END the last, significant call + */ + enum class Recursive { RECURSIVE_CALL, END } + + /** + * @property INSERT called from the insert method + * @property REMOVE_0 called from the remove method when current node had 0 null children + * @property REMOVE_1 called from the remove method when current node had 1 null children + * @property REMOVE_2 called from the remove method when current node had 2 null children + */ + enum class OperationType { INSERT, REMOVE_0, REMOVE_1, REMOVE_2 } + } + + /** + * @return remove type from [BalanceCase.OperationType] based on the [nullChildrenCount] + * + * @throws IllegalArgumentException if [nullChildrenCount] < 0 or > 2 + */ + protected fun getBalanceRemoveType(nullChildrenCount: Int): BalanceCase.OperationType { + return when (nullChildrenCount) { + 2 -> BalanceCase.OperationType.REMOVE_2 + 1 -> BalanceCase.OperationType.REMOVE_1 + 0 -> BalanceCase.OperationType.REMOVE_0 + else -> throw IllegalArgumentException("Expected number was <= 2, because in a binary tree a node can have no more than two children") + } + } + + /** + * @return what child of [curNode] should have [element] + */ + protected fun getDirectionChangedChild(curNode: NODE_T?, element: T): BalanceCase.ChangedChild { + return if (curNode == null) { + BalanceCase.ChangedChild.ROOT + } else if (element < curNode.element) { + BalanceCase.ChangedChild.LEFT + } else { + BalanceCase.ChangedChild.RIGHT + } + } + + /** + * This method automatic called after inserting or removing element from tree + * + * @param curNode - this is parent of changed node, if curNode is null => changed node is root + * @property changedChild what child was changed + * @property operationType from what method was called + * @property recursive was call recursive or last + */ + protected abstract fun balance( + curNode: NODE_T?, + changedChild: BalanceCase.ChangedChild, + operationType: BalanceCase.OperationType, + recursive: BalanceCase.Recursive + ) + + /** + * Insert [newNode] into the subtree of the [curNode]. And after call [balance] with right arguments. + * + * @return the parent of the inserted node, + * null if node with the same element already in tree or if inserted node is root + */ + override fun insertNode(curNode: NODE_T?, newNode: NODE_T): NODE_T? { + val parNode = super.insertNode(curNode, newNode) + if (curNode != null) { + if (curNode === parNode) { + balance( + curNode, + getDirectionChangedChild(curNode, newNode.element), + BalanceCase.OperationType.INSERT, + BalanceCase.Recursive.END + ) + } else { + balance( + curNode, + getDirectionChangedChild(curNode, newNode.element), + BalanceCase.OperationType.INSERT, + BalanceCase.Recursive.RECURSIVE_CALL + ) + if (curNode === root) { + balance( + null, + BalanceCase.ChangedChild.ROOT, + BalanceCase.OperationType.INSERT, + BalanceCase.Recursive.RECURSIVE_CALL + ) + } + } + } + return parNode + } + + /** + * Remove [element] from subtree of the [curNode] with [parentNode] as parent. + * And after call [balance] with right arguments + * + * @return the count of null children of deleted node or null if the node was not found + */ + override fun remove(curNode: NODE_T?, parentNode: NODE_T?, element: T): Int? { + if (curNode == null) { + return null + } + + val targetNode: NODE_T? + val isRec: BalanceCase.Recursive + val res = + if (element < curNode.element) { + isRec = BalanceCase.Recursive.RECURSIVE_CALL + targetNode = parentNode + remove(curNode.left, curNode, element) + } else if (element > curNode.element) { + isRec = BalanceCase.Recursive.RECURSIVE_CALL + targetNode = parentNode + remove(curNode.right, curNode, element) + } else { + isRec = BalanceCase.Recursive.END + targetNode = parentNode + deleteNode(curNode, parentNode) + } + + if (res != null) { + balance(targetNode, getDirectionChangedChild(targetNode, element), getBalanceRemoveType(res), isRec) + } + return res + } + + //Rotates + protected open fun rotateRight(curNode: NODE_T, parentNode: NODE_T?) { + val replacementNode = curNode.left ?: throw IllegalArgumentException("Received a node with a null left child") + curNode.left = replacementNode.right + replacementNode.right = curNode + + replaceNode(curNode, parentNode, replacementNode) + } + + protected open fun rotateLeft(curNode: NODE_T, parentNode: NODE_T?) { + val replacementNode = curNode.right ?: throw IllegalArgumentException("Received a node with a null right child") + curNode.right = replacementNode.left + replacementNode.left = curNode + + replaceNode(curNode, parentNode, replacementNode) + } +} diff --git a/binaryTree/src/main/kotlin/org/tree/binaryTree/templates/TemplateNode.kt b/binaryTree/src/main/kotlin/org/tree/binaryTree/templates/TemplateNode.kt new file mode 100644 index 00000000..31575916 --- /dev/null +++ b/binaryTree/src/main/kotlin/org/tree/binaryTree/templates/TemplateNode.kt @@ -0,0 +1,50 @@ +package org.tree.binaryTree.templates + +/** + * This class is template class for creating your own nodes. + * @param T the type of element stored in the node + * @param NODE_T the type of your node + * + * @property element the element stored in the node. + * @property left the left child node of this node, or null if this node does not have a left child node. + * @property right the right child node of this node, or null if this node does not have a right child node. + */ +abstract class TemplateNode, NODE_T : TemplateNode>(var element: T) { + var left: NODE_T? = null + var right: NODE_T? = null + + /** + * @property INORDER first the left child, then the parent and the right child + * @property PREORDER first the parent, then the left child and the right child + * @property POSTORDER first the left child, then the right child and the parent + */ + enum class Traversal { + INORDER, + PREORDER, + POSTORDER + } + + private fun traverse(res: MutableList, traversalOrder: Traversal) { + if (traversalOrder == Traversal.PREORDER) { + res.add(element) + } + left?.traverse(res, traversalOrder) + if (traversalOrder == Traversal.INORDER) { + res.add(element) + } + right?.traverse(res, traversalOrder) + if (traversalOrder == Traversal.POSTORDER) { + res.add(element) + } + } + + /** + * @return a list of nodes' elements in the specified order + * @param order specified traversal order + */ + fun traverse(order: Traversal): MutableList { + val res: MutableList = mutableListOf() + traverse(res, order) + return res + } +} diff --git a/binaryTree/src/main/kotlin/org/tree/binaryTree/trees/AVLTree.kt b/binaryTree/src/main/kotlin/org/tree/binaryTree/trees/AVLTree.kt new file mode 100644 index 00000000..55b0439f --- /dev/null +++ b/binaryTree/src/main/kotlin/org/tree/binaryTree/trees/AVLTree.kt @@ -0,0 +1,81 @@ +package org.tree.binaryTree.trees + +import org.tree.binaryTree.AVLNode +import org.tree.binaryTree.templates.TemplateBalanceBSTree +import kotlin.math.max + +class AVLTree> : TemplateBalanceBSTree>() { + private fun heightOrZero(avlNode: AVLNode?): Int { + return avlNode?.height ?: 0 + } + + private fun balanceFactor(avlNode: AVLNode): Int = //avl balance factor - difference in the heights of the right and left subtrees + avlNode.run { + fixHeight(this) + fixHeight(left) + fixHeight(right) + heightOrZero(right) - heightOrZero(left) + } + + private fun fixHeight(avlNode: AVLNode?) { + if (avlNode != null) { + val hl = heightOrZero(avlNode.left) + val hr = heightOrZero(avlNode.right) + avlNode.height = max(hl, hr) + 1 + } + } + + override fun rotateRight(curNode: AVLNode, parentNode: AVLNode?) { + val replacementNode = curNode.left + super.rotateRight(curNode, parentNode) + fixHeight(curNode) + fixHeight(replacementNode) + } + + override fun rotateLeft(curNode: AVLNode, parentNode: AVLNode?) { + val replacementNode = curNode.right + super.rotateLeft(curNode, parentNode) + fixHeight(replacementNode) + fixHeight(curNode) + } + + override fun insert(curNode: AVLNode?, element: T): AVLNode? { + return super.insertNode(curNode, AVLNode(element)) + } + + private fun balanceNode(curNode: AVLNode, parentNode: AVLNode?) { + if (balanceFactor(curNode) == 2) { + curNode.right?.let { + if (balanceFactor(it) < 0) { + rotateRight(it, curNode) + } + } + rotateLeft(curNode, parentNode) + return + } + + if (balanceFactor(curNode) == -2) { + curNode.left?.let { + if (balanceFactor(it) > 0) { + rotateLeft(it, curNode) + } + } + rotateRight(curNode, parentNode) + } + } + + override fun balance( + curNode: AVLNode?, + changedChild: BalanceCase.ChangedChild, + operationType: BalanceCase.OperationType, + recursive: BalanceCase.Recursive + ) { + if (operationType == BalanceCase.OperationType.REMOVE_0) return + if (curNode == null) { + root?.let { balanceNode(it, curNode) } + return + } + curNode.right?.let { balanceNode(it, curNode) } + curNode.left?.let { balanceNode(it, curNode) } + } +} diff --git a/binaryTree/src/main/kotlin/org/tree/binaryTree/trees/BinSearchTree.kt b/binaryTree/src/main/kotlin/org/tree/binaryTree/trees/BinSearchTree.kt new file mode 100644 index 00000000..8c4a9e46 --- /dev/null +++ b/binaryTree/src/main/kotlin/org/tree/binaryTree/trees/BinSearchTree.kt @@ -0,0 +1,10 @@ +package org.tree.binaryTree.trees + +import org.tree.binaryTree.Node +import org.tree.binaryTree.templates.TemplateBSTree + +class BinSearchTree> : TemplateBSTree>() { + override fun insert(curNode: Node?, element: T): Node? { + return insertNode(curNode, Node(element)) + } +} diff --git a/binaryTree/src/main/kotlin/org/tree/binaryTree/trees/RBTree.kt b/binaryTree/src/main/kotlin/org/tree/binaryTree/trees/RBTree.kt new file mode 100644 index 00000000..d8d7d17c --- /dev/null +++ b/binaryTree/src/main/kotlin/org/tree/binaryTree/trees/RBTree.kt @@ -0,0 +1,413 @@ +package org.tree.binaryTree.trees + +import org.tree.binaryTree.RBNode +import org.tree.binaryTree.templates.TemplateBalanceBSTree + +// algorithm source: https://www.youtube.com/watch?v=T70nn4EyTrs&ab_channel=%D0%9B%D0%B5%D0%BA%D1%82%D0%BE%D1%80%D0%B8%D0%B9%D0%A4%D0%9F%D0%9C%D0%98 + +class RBTree> : TemplateBalanceBSTree>() { + private fun findParentForNewNode(curNode: RBNode?, obj: T): RBNode? { + if (curNode != null) { + if (obj > curNode.element) { + if (curNode.right == null) { + return curNode + } + return findParentForNewNode(curNode.right, obj) + } else if (obj < curNode.element) { + if (curNode.left == null) { + return curNode + } + return findParentForNewNode(curNode.left, obj) + } + } + return null + } + + override fun insert(curNode: RBNode?, element: T): RBNode? { + val parentForObj = findParentForNewNode(curNode, element) + val newNode = RBNode(parentForObj, element) + if (parentForObj == null) { // in case of root insert | node already exist (nothing will be changed) + if (root == null) { + newNode.color = RBNode.Color.BLACK + } else { + return null + } + } + return insertNode(parentForObj, newNode) + } + + override fun balance( + curNode: RBNode?, + changedChild: BalanceCase.ChangedChild, + operationType: BalanceCase.OperationType, + recursive: BalanceCase.Recursive + ) { + if (recursive == BalanceCase.Recursive.END) { + if (curNode != null) { + when (operationType) { + BalanceCase.OperationType.INSERT -> { + balanceInsert(curNode) + } // curNode is parent Node of inserted Node + BalanceCase.OperationType.REMOVE_0 -> {} // does nothing + BalanceCase.OperationType.REMOVE_1 -> { + balanceRemove1(curNode, changedChild) + } + + BalanceCase.OperationType.REMOVE_2 -> { + balanceRemove2(curNode, changedChild) + } + } + } + } + } + + enum class BalancePosition { + LEFT_UNCLE, RIGHT_UNCLE + } + + private fun balanceInsert(parentNode: RBNode) { + if (parentNode.color == RBNode.Color.RED) { + val grandParent = parentNode.parent + if (grandParent != null) { // in case when grandparent is null, there is no need to balance a tree + val unclePosition: BalancePosition + val uncle = if (parentNode.element < grandParent.element) { + unclePosition = BalancePosition.RIGHT_UNCLE + grandParent.right + } else { + unclePosition = BalancePosition.LEFT_UNCLE + grandParent.left + } + if (uncle != null) { + if (uncle.color == RBNode.Color.RED) { + balanceInsertCaseOfRedUncle(parentNode, grandParent, uncle) + } else { + balanceInsertCaseOfBLackUncle(parentNode, grandParent, unclePosition) + } + } else { // null uncle means that he is black + balanceInsertCaseOfBLackUncle(parentNode, grandParent, unclePosition) + } + } + } + } + + + private fun balanceInsertCaseOfRedUncle(parentNode: RBNode, grandParent: RBNode, uncle: RBNode) { + val grandGrandParent = grandParent.parent + uncle.color = RBNode.Color.BLACK + parentNode.color = RBNode.Color.BLACK + if (grandGrandParent != null) { + grandParent.color = RBNode.Color.RED + // https://skr.sh/sJ9LBQU2IGg, when y is curNode + if (grandGrandParent.color == RBNode.Color.RED) { + balanceInsert(grandGrandParent) + } + } + } + + private fun balanceInsertCaseOfBLackUncle( + parentNode: RBNode, + grandParent: RBNode, + position: BalancePosition + ) { // can be null uncle + if (position == BalancePosition.LEFT_UNCLE) { + val leftChild = parentNode.left + if (leftChild?.color == RBNode.Color.RED) { + rotateRight(parentNode, grandParent) + parentNode.parent?.let { balanceInsert(it) } + } else { + val rightChild = parentNode.right + if (rightChild != null) { + parentNode.color = RBNode.Color.BLACK + grandParent.color = RBNode.Color.RED + rotateLeft(grandParent, grandParent.parent) + } + } + } else { + val leftChild = parentNode.left + if (leftChild?.color == RBNode.Color.RED) { + parentNode.color = RBNode.Color.BLACK + grandParent.color = RBNode.Color.RED + rotateRight(grandParent, grandParent.parent) + } else { + val rightChild = parentNode.right + if (rightChild != null) { + if (rightChild.color == RBNode.Color.RED) { + rotateLeft(parentNode, grandParent) + parentNode.parent?.let { balanceInsert(it) } + } + } + } + } + } + + /** Balanced remove with 1 non-null child */ + private fun balanceRemove1(parentNode: RBNode?, removedChild: BalanceCase.ChangedChild) { + when (removedChild) { + BalanceCase.ChangedChild.RIGHT -> { + parentNode?.right?.run { color = RBNode.Color.BLACK } + } + + BalanceCase.ChangedChild.LEFT -> { + parentNode?.left?.run { color = RBNode.Color.BLACK } + } + + else -> {} + } + } + + /** Balanced remove with 2 null children */ + private fun balanceRemove2(parentNode: RBNode, removedChild: BalanceCase.ChangedChild) { + if (getColourOfRemovedNode(parentNode) == RBNode.Color.BLACK) { // red => no need to balance + if (parentNode.color == RBNode.Color.RED) { // then other child is black + if (removedChild == BalanceCase.ChangedChild.RIGHT) { + balanceRemove2InRightChildWithRedParent(parentNode) + } else if (removedChild == BalanceCase.ChangedChild.LEFT) { + balanceRemove2InLeftChildWithRedParent(parentNode) + } + } else { + if (removedChild == BalanceCase.ChangedChild.RIGHT) { + balanceRemove2InRightChildWithBlackParent(parentNode) + } else if (removedChild == BalanceCase.ChangedChild.LEFT) { + balanceRemove2InLeftChildWithBlackParent(parentNode) + } + } + } + } + + private fun balanceRemove2InRightChildWithRedParent(parentNode: RBNode) { + val otherChild = parentNode.left + if (otherChild != null) { + val leftChildOfOtherChild = otherChild.left + val rightChildOfOtherChild = otherChild.right + if (leftChildOfOtherChild?.color == RBNode.Color.RED) { + otherChild.color = RBNode.Color.RED + parentNode.color = RBNode.Color.BLACK + leftChildOfOtherChild.color = RBNode.Color.BLACK + rotateRight(parentNode, parentNode.parent) + } else if (rightChildOfOtherChild?.color == RBNode.Color.RED) { + parentNode.color = RBNode.Color.BLACK + rotateLeft(otherChild, parentNode) + rotateRight(parentNode, parentNode.parent) + } else { + otherChild.color = RBNode.Color.RED + parentNode.color = RBNode.Color.BLACK + } + } + } + + private fun balanceRemove2InLeftChildWithRedParent(parentNode: RBNode) { + val otherChild = parentNode.right + if (otherChild != null) { + val leftChildOfOtherChild = otherChild.left + val rightChildOfOtherChild = otherChild.right + if (leftChildOfOtherChild?.color == RBNode.Color.RED) { + parentNode.color = RBNode.Color.BLACK + rotateRight(otherChild, parentNode) + rotateLeft(parentNode, parentNode.parent) + } else if (rightChildOfOtherChild?.color == RBNode.Color.RED) { + otherChild.color = RBNode.Color.RED + parentNode.color = RBNode.Color.BLACK + rightChildOfOtherChild.color = RBNode.Color.BLACK + rotateLeft(parentNode, parentNode.parent) + } else { + otherChild.color = RBNode.Color.RED + parentNode.color = RBNode.Color.BLACK + } + } + } + + private fun balanceRemove2InRightChildWithBlackParent(parentNode: RBNode) { + val otherChild = parentNode.left + if (otherChild != null) { + if (otherChild.color == RBNode.Color.RED) { + balanceRemove2InRightChildWithBlackParentRedOtherChild(parentNode, otherChild) + } else { + balanceRemove2InRightChildWithBlackParentBlackOtherChild(parentNode, otherChild) + } + } + } + + private fun balanceRemove2InLeftChildWithBlackParent(parentNode: RBNode) { + val otherChild = parentNode.right + if (otherChild != null) { + if (otherChild.color == RBNode.Color.RED) { + balanceRemove2InLeftChildWithBlackParentRedOtherChild(parentNode, otherChild) + } else { + balanceRemove2InLeftChildWithBlackParentBlackOtherChild(parentNode, otherChild) + } + } + } + + private fun balanceRemove2InRightChildWithBlackParentRedOtherChild( + parentNode: RBNode, + otherChild: RBNode + ) { + val rightChildOfOtherChild = otherChild.right + if (rightChildOfOtherChild != null) { + val leftChildOfRightChildOfOtherChild = + rightChildOfOtherChild.left // https://skr.sh/sJD6DQ2ML5B + val rightChildOfRightChildOfOtherChild = rightChildOfOtherChild.right + + if (leftChildOfRightChildOfOtherChild?.color == RBNode.Color.RED) { + leftChildOfRightChildOfOtherChild.color = RBNode.Color.BLACK + rotateLeft(otherChild, parentNode) + rotateRight(parentNode, parentNode.parent) + } else if (rightChildOfRightChildOfOtherChild?.color == RBNode.Color.RED) { + rightChildOfOtherChild.color = RBNode.Color.RED + rightChildOfRightChildOfOtherChild.color = RBNode.Color.BLACK + rotateLeft(rightChildOfOtherChild, otherChild) + balanceRemove2InRightChildWithBlackParentRedOtherChild(parentNode, otherChild) + // case: leftChildOfRightChildOfOtherChild?.col == RBNode.Colour.RED + } else { + otherChild.color = RBNode.Color.BLACK + rightChildOfOtherChild.color = RBNode.Color.RED + rotateRight(parentNode, parentNode.parent) + } + } + } + + private fun balanceRemove2InLeftChildWithBlackParentRedOtherChild( + parentNode: RBNode, + otherChild: RBNode + ) { + val leftChildOfOtherChild = otherChild.left + if (leftChildOfOtherChild != null) { + val rightChildOfLeftChildOfOtherChild = + leftChildOfOtherChild.right // https://skr.sh/sJD6DQ2ML5B (inverted) + val leftChildOfLeftChildOfOtherChild = leftChildOfOtherChild.left + + if (rightChildOfLeftChildOfOtherChild?.color == RBNode.Color.RED) { + rightChildOfLeftChildOfOtherChild.color = RBNode.Color.BLACK + rotateRight(otherChild, parentNode) + rotateLeft(parentNode, parentNode.parent) + } else if (leftChildOfLeftChildOfOtherChild?.color == RBNode.Color.RED) { + leftChildOfOtherChild.color = RBNode.Color.RED + leftChildOfLeftChildOfOtherChild.color = RBNode.Color.BLACK + rotateRight(leftChildOfOtherChild, otherChild) + balanceRemove2InLeftChildWithBlackParentRedOtherChild(parentNode, otherChild) + // case: rightChildOfLeftChildOfOtherChild?.col == RBNode.Colour.RED + } else { + otherChild.color = RBNode.Color.BLACK + leftChildOfOtherChild.color = RBNode.Color.RED + rotateLeft(parentNode, parentNode.parent) + } + } + } + + private fun balanceRemove2InRightChildWithBlackParentBlackOtherChild( + parentNode: RBNode, + otherChild: RBNode + ) { + val rightChildOfOtherChild = otherChild.right + if (rightChildOfOtherChild != null) { + if (rightChildOfOtherChild.color == RBNode.Color.RED) { + rightChildOfOtherChild.color = RBNode.Color.BLACK + rotateLeft(otherChild, parentNode) + rotateRight(parentNode, parentNode.parent) + return + } + } + val leftChildOfOtherChild = otherChild.left + if (leftChildOfOtherChild != null) { + if (leftChildOfOtherChild.color == RBNode.Color.RED) { + leftChildOfOtherChild.color = RBNode.Color.BLACK + rotateRight(parentNode, parentNode.parent) + return + } + } + otherChild.color = RBNode.Color.RED + val grandParent = parentNode.parent + if (grandParent != null) { + if (parentNode.element < grandParent.element) { + balanceRemove2(grandParent, BalanceCase.ChangedChild.LEFT) + } else { + balanceRemove2(grandParent, BalanceCase.ChangedChild.RIGHT) + } + } + } + + + private fun balanceRemove2InLeftChildWithBlackParentBlackOtherChild( + parentNode: RBNode, + otherChild: RBNode + ) { + val leftChildOfOtherChild = otherChild.left + if (leftChildOfOtherChild != null) { + if (leftChildOfOtherChild.color == RBNode.Color.RED) { + leftChildOfOtherChild.color = RBNode.Color.BLACK + rotateRight(otherChild, parentNode) + rotateLeft(parentNode, parentNode.parent) + return + } + } + val rightChildOfOtherChild = otherChild.right + if (rightChildOfOtherChild != null) { + if (rightChildOfOtherChild.color == RBNode.Color.RED) { + rightChildOfOtherChild.color = RBNode.Color.BLACK + rotateLeft(parentNode, parentNode.parent) + return + } + } + otherChild.color = RBNode.Color.RED + val grandParent = parentNode.parent + if (grandParent != null) { + if (parentNode.element < grandParent.element) { + balanceRemove2(grandParent, BalanceCase.ChangedChild.LEFT) + } else { + balanceRemove2(grandParent, BalanceCase.ChangedChild.RIGHT) + } + } + } + + private fun getColourOfRemovedNode(parentNode: RBNode): RBNode.Color { + return if (getBlackHeight(parentNode.left) != getBlackHeight(parentNode.right)) { + RBNode.Color.BLACK + } else { + RBNode.Color.RED + } + } + + private fun getBlackHeight(curNode: RBNode?, blackHeight: Int = 0): Int { + if (curNode != null) { + return getBlackHeight( + curNode.left, + if (curNode.color == RBNode.Color.BLACK) { + blackHeight + 1 + } else { + blackHeight + } + ) + } + return blackHeight + } + + override fun replaceNode(replacedNode: RBNode, parentNode: RBNode?, newNode: RBNode?) { + newNode?.parent = parentNode + if (parentNode == null) { + if (newNode != null) { + newNode.color = RBNode.Color.BLACK + } + } + super.replaceNode(replacedNode, parentNode, newNode) + } + + override fun rotateRight(curNode: RBNode, parentNode: RBNode?) { + curNode.parent = curNode.left + curNode.left?.parent = parentNode + curNode.left?.right?.parent = curNode + if (curNode === root) { + curNode.left?.color = RBNode.Color.BLACK + } + super.rotateRight(curNode, parentNode) + } + + override fun rotateLeft(curNode: RBNode, parentNode: RBNode?) { + curNode.parent = curNode.right + curNode.right?.parent = parentNode + curNode.right?.left?.parent = curNode + if (curNode === root) { + curNode.right?.color = RBNode.Color.BLACK + } + super.rotateLeft(curNode, parentNode) + } +} diff --git a/binaryTree/src/test/kotlin/org/tree/binaryTree/AVLTreeTest.kt b/binaryTree/src/test/kotlin/org/tree/binaryTree/AVLTreeTest.kt new file mode 100644 index 00000000..320bce74 --- /dev/null +++ b/binaryTree/src/test/kotlin/org/tree/binaryTree/AVLTreeTest.kt @@ -0,0 +1,102 @@ +package org.tree.binaryTree + +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.tree.binaryTree.trees.AVLTree +import java.util.stream.Stream +import kotlin.random.Random + +class AVLTreeTest { + @DisplayName("AVLTree.insert() tests") + class InsertTests { + @ParameterizedTest(name = "[{index}]: insertCount = {1}, seed = {0}") + @MethodSource("testInsertArgs") + fun testInsert(seed: Long, insertCount: Int) { + val tree = AVLTree() + val values = mutableSetOf() + val randomizer = Random(seed) + + for (i in 1..insertCount) { + val newVal = randomizer.nextInt() + val exp = values.add(newVal) + val act = tree.insert(newVal) + + MatcherAssert.assertThat(act, Matchers.equalTo(exp)) + checkAVLTree(tree, values.toTypedArray()) + } + } + + companion object { + @JvmStatic + fun testInsertArgs(): Stream { + return Stream.of( + genArguments(0xdeadbeef), + genArguments(0xabacaba, 10), + genArguments(42), + genArguments(13), + genArguments(0xcafe), + genArguments(1337), + ) + } + + private fun genArguments(seed: Long, insertCount: Int = 1000): Arguments { + return Arguments.of(seed, insertCount) + } + } + } + + + @DisplayName("AVLTree.remove() tests") + class RemoveTests { + @ParameterizedTest(name = "[{index}]: treeSize = {1}, removeCount = {2}, seed = {0}") + @MethodSource("testRemoveArgs") + fun testRemove(seed: Long, treeSize: Int, removeCount: Int) { + val tree = AVLTree() + val values = mutableSetOf() + val randomizer = Random(seed) + for (i in 1..treeSize) { + val newVal = randomizer.nextInt() + values.add(newVal) + tree.insert(newVal) + } + + for (i in 1..removeCount * 2) { + val curVal = if (i % 2 != 0 && values.isNotEmpty()) { + values.random(randomizer) + } else { + randomizer.nextInt() + } + val exp = values.remove(curVal) + val act = tree.remove(curVal) + + MatcherAssert.assertThat(act, Matchers.equalTo(exp)) + checkAVLTree(tree, values.toTypedArray()) + } + } + + companion object { + @JvmStatic + fun testRemoveArgs(): Stream { + return Stream.of( + genArguments(100), + genArguments(0xdeadbeef, 1000, 1050), + genArguments(0xdeadbeef, 1050, 1000), + genArguments(0xdeadbeef, 10, 5), + genArguments(0xdeadbeef, 0, 1), + genArguments(42), + genArguments(13), + genArguments(0xcafe), + genArguments(1337), + ) + } + + private fun genArguments(seed: Long, treeSize: Int = 1000, removeCount: Int = 1000): Arguments { + return Arguments.of(seed, treeSize, removeCount) + } + } + } +} diff --git a/binaryTree/src/test/kotlin/org/tree/binaryTree/BinSearchTreeTest.kt b/binaryTree/src/test/kotlin/org/tree/binaryTree/BinSearchTreeTest.kt new file mode 100644 index 00000000..7c59a563 --- /dev/null +++ b/binaryTree/src/test/kotlin/org/tree/binaryTree/BinSearchTreeTest.kt @@ -0,0 +1,301 @@ +package org.tree.binaryTree + + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.junit.jupiter.api.* +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.tree.binaryTree.templates.TemplateNode +import org.tree.binaryTree.trees.BinSearchTree +import java.util.stream.Stream +import kotlin.random.Random + +class BinSearchTreeTest { + @DisplayName("BinSearchTree.insert() tests") + class InsertTests { + @ParameterizedTest(name = "[{index}]: tree = {0}, insert = {1}") + @MethodSource("testInsertArgs") + fun testInsert(values: List, insVal: Int) { + val tree = BinSearchTree() + tree.root = generateNodeTree(values) + val valuesSet = values.filterNotNull().toMutableSet() + val exp = valuesSet.add(insVal) + val act = tree.insert(insVal) + + assertThat(act, equalTo(exp)) + checkBinSearchTree(tree, valuesSet.toTypedArray()) + } + + companion object { + @JvmStatic + fun testInsertArgs(): Stream { + return Stream.of( + //[1] root insert + Arguments.of(listOf(null), 40), + //[2] left insert + Arguments.of( + listOf( + 40, + ), + 20 + ), + //[3] right insert + Arguments.of( + listOf( + 40, + ), + 60 + ), + //[4] left rec insert + Arguments.of( + listOf( + 40, + 20, 60 + ), + 21 + ), + //[5] right rec insert + Arguments.of( + listOf( + 40, + 20, 60 + ), + 55 + ), + //[6] same insert + Arguments.of( + listOf( + 40, + null, 60, + null, null, 45, null, + ), + 45 + ), + ) + } + } + + @Test + fun bigInsertTest() { + val tree = BinSearchTree() + val values = mutableSetOf() + val randomizer = Random(0xdeadbeef) + + for (i in 0..100) { + val newVal = randomizer.nextInt() + val exp = values.add(newVal) + val act = tree.insert(newVal) + + assertThat(act, equalTo(exp)) + checkBinSearchTree(tree, values.toTypedArray()) + } + } + } + + @DisplayName("BinSearchTree.remove() tests") + class RemoveTests { + @ParameterizedTest(name = "[{index}]: tree = {0}, remove = {1}") + @MethodSource("testRemoveArgs") + fun testRemove(values: List, remVal: Int) { + val tree = BinSearchTree() + tree.root = generateNodeTree(values) + val valuesSet = values.filterNotNull().toMutableSet() + val exp = valuesSet.remove(remVal) + val act = tree.remove(remVal) + + assertThat(act, equalTo(exp)) + checkBinSearchTree(tree, valuesSet.toTypedArray()) + } + + companion object { + @JvmStatic + fun testRemoveArgs(): Stream { + return Stream.of( + //[1] root remove + Arguments.of( + listOf( + 40, + ), 40 + ), + //[2] left remove + Arguments.of( + listOf( + 40, + 20, 60, + ), 20 + ), + //[3] right remove + Arguments.of( + listOf( + 40, + 20, 60, + ), 40 + ), + //[4] empty remove + Arguments.of( + listOf( + 40, + 20, 60, + null, null, null, null, + ), 42 + ), + ) + } + } + + @Test + fun bigRemoveTest() { + val tree = BinSearchTree() + val values = mutableSetOf() + val randomizer = Random(0xdeadbeef) + + for (i in 0..100) { + val newVal = randomizer.nextInt() + values.add(newVal) + tree.insert(newVal) + } + + for (i in 0..150) { + val curVal = if ((i % 2 == 0) and (values.isNotEmpty())) { + values.first() + } else { + randomizer.nextInt() + } + val exp = values.remove(curVal) + val act = tree.remove(curVal) + + assertThat(act, equalTo(exp)) + checkBinSearchTree(tree, values.toTypedArray()) + } + } + } + + @DisplayName("BinSearchTree.find() tests") + class FindTests { + @ParameterizedTest(name = "[{index}]: tree = {0}, find = {1}") + @MethodSource("testFindArgs") + fun testFind(values: List, fndVal: Int) { + val tree = BinSearchTree() + tree.root = generateNodeTree(values) + val valuesSet = values.filterNotNull().toMutableSet() + val exp = valuesSet.contains(fndVal) + val act = tree.find(fndVal) + + assertThat(act != null, equalTo(exp)) + if (act != null) { + assertThat(act.element, equalTo(fndVal)) + } + } + + companion object { + @JvmStatic + fun testFindArgs(): Stream { + return Stream.of( + //[1] root find + Arguments.of( + listOf( + 40, + 20, 60 + ), 40 + ), + //[2] left find + Arguments.of( + listOf( + 40, + 20, 60 + ), 20 + ), + //[3] right find + Arguments.of( + listOf( + 40, + 20, 60, + ), 60 + ), + //[4] impossible find + Arguments.of( + listOf( + 40, + 20, 60 + ), 42 + ), + ) + } + } + + @Test + fun bigFindTest() { + val tree = BinSearchTree() + val values = mutableSetOf() + val randomizer = Random(0xdeadbeef) + + for (i in 0..100) { + val newVal = randomizer.nextInt() + values.add(newVal) + tree.insert(newVal) + } + + for (i in 0..150) { + val curVal = if ((i % 2 == 0) and (values.isNotEmpty())) { + values.random(randomizer) + } else { + randomizer.nextInt() + } + val exp = values.contains(curVal) + val act = tree.find(curVal) + + assertThat(act != null, equalTo(exp)) + if (act != null) { + assertThat(act.element, equalTo(curVal)) + } + } + } + } + + @DisplayName("BinSearchTree.traversal() tests") + class TraversalTests { + private var tree = BinSearchTree() + + @BeforeEach + fun init() { + tree.root = generateNodeTree( + listOf( + 40, + 20, 60, + 15, null, null, 67, + ) + ) + } + + @Test + fun preorderTraversalTest() { + val act = tree.traversal(TemplateNode.Traversal.PREORDER) + val exp = listOf(40, 20, 15, 60, 67) + assertThat(act, equalTo(exp)) + } + + @Test + fun inorderTraversalTest() { + val act = tree.traversal(TemplateNode.Traversal.INORDER) + val exp = listOf(15, 20, 40, 60, 67) + assertThat(act, equalTo(exp)) + } + + @Test + fun postorderTraversalTest() { + val act = tree.traversal(TemplateNode.Traversal.POSTORDER) + val exp = listOf(15, 20, 67, 60, 40) + assertThat(act, equalTo(exp)) + } + + @Test + fun emptyTraversalTest() { + tree.root = null + val act = tree.traversal(TemplateNode.Traversal.PREORDER) + val exp = listOf() + assertThat(act, equalTo(exp)) + } + } +} diff --git a/binaryTree/src/test/kotlin/org/tree/binaryTree/NodeTest.kt b/binaryTree/src/test/kotlin/org/tree/binaryTree/NodeTest.kt new file mode 100644 index 00000000..60e79d07 --- /dev/null +++ b/binaryTree/src/test/kotlin/org/tree/binaryTree/NodeTest.kt @@ -0,0 +1,47 @@ +package org.tree.binaryTree + +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.jupiter.api.* +import org.tree.binaryTree.templates.TemplateNode + +class NodeTest { + var root: Node? = null + + @DisplayName("Traversal tests") + @Nested + inner class TraversalTests { + @BeforeEach + fun init() { + root = generateNodeTree( + listOf( + 40, + 20, 60, + 15, null, null, 67, + ) + ) + } + + @Test + fun preorderTraversalTest() { + val act = root?.traverse(TemplateNode.Traversal.PREORDER) + val exp = listOf(40, 20, 15, 60, 67) + MatcherAssert.assertThat(act, Matchers.equalTo(exp)) + } + + @Test + fun inorderTraversalTest() { + val act = root?.traverse(TemplateNode.Traversal.INORDER) + val exp = listOf(15, 20, 40, 60, 67) + MatcherAssert.assertThat(act, Matchers.equalTo(exp)) + } + + @Test + fun postorderTraversalTest() { + val act = root?.traverse(TemplateNode.Traversal.POSTORDER) + val exp = listOf(15, 20, 67, 60, 40) + MatcherAssert.assertThat(act, Matchers.equalTo(exp)) + } + + } +} diff --git a/binaryTree/src/test/kotlin/org/tree/binaryTree/RBTreeTest.kt b/binaryTree/src/test/kotlin/org/tree/binaryTree/RBTreeTest.kt new file mode 100644 index 00000000..5659773a --- /dev/null +++ b/binaryTree/src/test/kotlin/org/tree/binaryTree/RBTreeTest.kt @@ -0,0 +1,107 @@ +package org.tree.binaryTree + +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import org.tree.binaryTree.trees.RBTree +import java.util.stream.Stream +import kotlin.random.Random + +class RBTreeTest { + @DisplayName("RBTree.insert() tests") + class InsertTests { + @ParameterizedTest(name = "[{index}]: insertCount = {1}, seed = {0}") + @MethodSource("testInsertArgs") + fun testInsert(seed: Long, insertCount: Int) { + val tree = RBTree() + val values = mutableSetOf() + val randomizer = Random(seed) + + for (i in 1..insertCount) { + val newVal = randomizer.nextInt() + val exp = values.add(newVal) + val act = tree.insert(newVal) + + MatcherAssert.assertThat(act, Matchers.equalTo(exp)) + checkRBTree(tree, values.toTypedArray()) + } + + + } + + companion object { + @JvmStatic + fun testInsertArgs(): Stream { + return Stream.of( + genArguments(0xdeadbeef), + genArguments(0xabacaba, 10), + genArguments(42), + genArguments(13), + genArguments(0xcafe), + genArguments(1337), + ) + } + + private fun genArguments(seed: Long, insertCount: Int = 1000): Arguments { + return Arguments.of(seed, insertCount) + } + } + + } + + + @DisplayName("RBTree.remove() tests") + class RemoveTests { + @ParameterizedTest(name = "[{index}]: treeSize = {1}, removeCount = {2}, seed = {0}") + @MethodSource("testRemoveArgs") + fun testRemove(seed: Long, treeSize: Int, removeCount: Int) { + val tree = RBTree() + val values = mutableSetOf() + val randomizer = Random(seed) + for (i in 1..treeSize) { + val newVal = randomizer.nextInt() + values.add(newVal) + tree.insert(newVal) + } + + for (i in 1..removeCount * 2) { + val curVal = if ((i % 2 != 0) and (values.isNotEmpty())) { + values.random(randomizer) + } else { + randomizer.nextInt() + } + val exp = values.remove(curVal) + val act = tree.remove(curVal) + + + MatcherAssert.assertThat(act, Matchers.equalTo(exp)) + checkRBTree(tree, values.toTypedArray()) + } + } + + companion object { + @JvmStatic + fun testRemoveArgs(): Stream { + return Stream.of( + genArguments(0xdeadbeef), + genArguments(0xdeadbeef, 1000, 1050), + genArguments(0xdeadbeef, 1050, 1000), + genArguments(0xdeadbeef, 10, 5), + genArguments(0xdeadbeef, 0, 1), + genArguments(42), + genArguments(13), + genArguments(0xcafe), + genArguments(1337), + ) + } + + private fun genArguments(seed: Long, treeSize: Int = 1000, removeCount: Int = 1000): Arguments { + return Arguments.of(seed, treeSize, removeCount) + } + } + + } +} diff --git a/binaryTree/src/test/kotlin/org/tree/binaryTree/Util.kt b/binaryTree/src/test/kotlin/org/tree/binaryTree/Util.kt new file mode 100644 index 00000000..68fd75fc --- /dev/null +++ b/binaryTree/src/test/kotlin/org/tree/binaryTree/Util.kt @@ -0,0 +1,150 @@ +package org.tree.binaryTree + +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.* +import org.tree.binaryTree.templates.TemplateBSTree +import org.tree.binaryTree.templates.TemplateNode +import org.tree.binaryTree.trees.AVLTree +import org.tree.binaryTree.trees.RBTree +import java.util.* +import kotlin.math.abs +import kotlin.math.log2 +import kotlin.math.max + + +// Generate +fun > generateTree(nodes: List): NODE_T? { + val fheight = log2((nodes.size + 1).toFloat()) + val height = fheight.toInt() + if (fheight - height > 0.0) { + throw IllegalArgumentException("Received an invalid array to create a tree. Size of array should be equal (2**N)-1") + } + + val q: Queue = LinkedList() + val root = nodes[0] + q.add(root) + var i = 1 + while (i < nodes.size) { + val cur = q.poll() + cur?.left = nodes[i] + i++ + q.add(cur?.left) + cur?.right = nodes[i] + i++ + q.add(cur?.right) + } + return root +} + +fun > generateNodeTree(objects: List): Node? { + val new = objects.map { + if (it != null) { + Node(it) + } else { + null + } + } + return generateTree(new) +} + +// Check +fun , NODE_T : TemplateNode> checkTreeNode(curNode: NODE_T?) { + if (curNode != null) { + val l = curNode.left + if (l != null) { + assertThat(curNode.element, greaterThanOrEqualTo(l.element)) + checkTreeNode(l) + } + + val r = curNode.right + if (r != null) { + assertThat(curNode.element, lessThanOrEqualTo(r.element)) + checkTreeNode(r) + } + } +} + +fun , NODE_T : TemplateNode> checkBinSearchTree( + tree: TemplateBSTree, + contain: Array +) { + assertThat(tree.traversal(TemplateNode.Traversal.INORDER), containsInAnyOrder(*contain)) + checkTreeNode(tree.root) +} + +fun > checkRBTreeNode(curNode: RBNode?, parNode: RBNode?): Int { + var blackHeight = 0 + if (curNode != null) { + // two red nodes in row + if (parNode?.color == RBNode.Color.RED) { + assertThat(curNode.color, equalTo(RBNode.Color.BLACK)) + } + + // right parent + assertThat(curNode.parent, equalTo(parNode)) + + var lBlackHeight = 0 + val l = curNode.left + if (l != null) { + assertThat(curNode.element, greaterThanOrEqualTo(l.element)) + lBlackHeight = checkRBTreeNode(l, curNode) + } + + var rBlackHeight = 0 + val r = curNode.right + if (r != null) { + assertThat(curNode.element, lessThanOrEqualTo(r.element)) + rBlackHeight = checkRBTreeNode(r, curNode) + } + + // same black height + assertThat(lBlackHeight, equalTo(rBlackHeight)) + blackHeight = lBlackHeight + + if (curNode.color == RBNode.Color.BLACK) { + blackHeight += 1 + } + } + + return blackHeight +} + +fun > checkRBTree( + tree: RBTree, + contain: Array +) { + assertThat(tree.traversal(TemplateNode.Traversal.INORDER), containsInAnyOrder(*contain)) + checkRBTreeNode(tree.root, null) +} + +private fun > checkHeight(curNode: AVLNode?): Int{ + if (curNode == null) { + return 0 + } + val leftHeight = checkHeight(curNode.left) + val rightHeight = checkHeight(curNode.right) + return 1 + max(leftHeight, rightHeight) +} + +private fun > checkAVLTreeNode(node: AVLNode?) { + if (node == null) { + return + } + + val leftHeight = checkHeight(node.left) + val rightHeight = checkHeight(node.right) + + assertThat(abs(leftHeight - rightHeight), lessThanOrEqualTo(1)) + + checkAVLTreeNode(node.left) + checkAVLTreeNode(node.right) + +} + +fun > checkAVLTree( + tree: AVLTree, + contain: Array +) { + assertThat(tree.traversal(TemplateNode.Traversal.INORDER), containsInAnyOrder(*contain)) + checkAVLTreeNode(tree.root) +} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..d227aa78 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + // Support convention plugins written in Kotlin. Convention plugins are build scripts in 'src/main' that automatically become available as plugins in the main build. + `kotlin-dsl` +} + +repositories { + // Use the plugin portal to apply community plugins in convention plugins. + gradlePluginPortal() +} + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10") +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..3f3e665c --- /dev/null +++ b/buildSrc/settings.gradle.kts @@ -0,0 +1,7 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This settings file is used to specify which projects to include in your build-logic build. + */ + +rootProject.name = "buildSrc" diff --git a/buildSrc/src/main/kotlin/org.tree.kotlin-application-conventions.gradle.kts b/buildSrc/src/main/kotlin/org.tree.kotlin-application-conventions.gradle.kts new file mode 100644 index 00000000..e203baa4 --- /dev/null +++ b/buildSrc/src/main/kotlin/org.tree.kotlin-application-conventions.gradle.kts @@ -0,0 +1,11 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + // Apply the common convention plugin for shared build configuration between library and application projects. + id("org.tree.kotlin-common-conventions") + + // Apply the application plugin to add support for building a CLI application in Java. + application +} diff --git a/buildSrc/src/main/kotlin/org.tree.kotlin-common-conventions.gradle.kts b/buildSrc/src/main/kotlin/org.tree.kotlin-common-conventions.gradle.kts new file mode 100644 index 00000000..b171874a --- /dev/null +++ b/buildSrc/src/main/kotlin/org.tree.kotlin-common-conventions.gradle.kts @@ -0,0 +1,55 @@ +import org.gradle.api.tasks.testing.logging.TestLogEvent + +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + // Apply the org.jetbrains.kotlin.jvm Plugin to add support for Kotlin. + id("org.jetbrains.kotlin.jvm") + + id("jacoco") +} + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + constraints { + // Define dependency versions as constraints + implementation("org.apache.commons:commons-text:1.9") + } + + // Use JUnit Jupiter for testing. + testImplementation("org.junit.jupiter:junit-jupiter:5.9.1") + // Use hamcrest for convenient testing + testImplementation("org.hamcrest:hamcrest:2.2") +} + +tasks.named("test") { + // Use JUnit Platform for unit tests. + useJUnitPlatform() +} + +tasks { + test { + testLogging { + showStandardStreams=true + events.add(TestLogEvent.FAILED) + events.add(TestLogEvent.PASSED) + events.add(TestLogEvent.SKIPPED) + } + } +} + +//Jacoco +tasks{ + test { + finalizedBy(jacocoTestReport) + } + jacocoTestReport { + dependsOn(test) + } +} diff --git a/buildSrc/src/main/kotlin/org.tree.kotlin-library-conventions.gradle.kts b/buildSrc/src/main/kotlin/org.tree.kotlin-library-conventions.gradle.kts new file mode 100644 index 00000000..765f7be8 --- /dev/null +++ b/buildSrc/src/main/kotlin/org.tree.kotlin-library-conventions.gradle.kts @@ -0,0 +1,11 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +plugins { + // Apply the common convention plugin for shared build configuration between library and application projects. + id("org.tree.kotlin-common-conventions") + + // Apply the java-library plugin for API and implementation separation. + `java-library` +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..ccebba77 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 00000000..bdc9a83b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..79a61d42 --- /dev/null +++ b/gradlew @@ -0,0 +1,244 @@ +#!/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/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +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 + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# 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*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + 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 100644 index 00000000..93e3f59f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@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=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-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/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..0f02415c --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,11 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user manual at https://docs.gradle.org/8.0.2/userguide/multi_project_builds.html + */ + +rootProject.name = "trees-2" +include("app", "binaryTree")