diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..00a51af --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/.github/.keep b/.github/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..cb2d165 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/mergeable.yml b/.github/mergeable.yml new file mode 100644 index 0000000..27ceb8f --- /dev/null +++ b/.github/mergeable.yml @@ -0,0 +1,30 @@ +version: 2 +mergeable: + - when: pull_request.*, pull_request_review.* + filter: + # ignore 'Feedback' PR + - do: payload + pull_request: + title: + must_exclude: + regex: ^Feedback$ + regex_flag: none + validate: + - do: description + no_empty: + enabled: true + message: "Description matter and should not be empty. Provide detail with **what** was changed, **why** it was changed, and **how** it was changed." + - do: approvals + min: + count: 1 + required: + assignees: true + + - when: pull_request.opened + name: "Remind about contributing guidelines" + validate: [ ] + pass: + - do: comment + payload: + body: > + Thanks for creating a pull request! Please, check that your pull request meets the [CONTRIBUTING](./CONTRIBUTING.md) requirements. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..da1d660 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: Run full build + +on: + push: +jobs: + build-gradle-project: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ macos-latest, ubuntu-latest, windows-latest ] + + steps: + - name: Checkout project sources + uses: actions/checkout@v3 + - name: Setup Java 11 + uses: actions/setup-java@v2 + with: + distribution: adopt + java-version: 11 + - name: Setup Gradle + uses: gradle/gradle-build-action@v2 + - name: Run build with Gradle Wrapper + run: ./gradlew build + + - uses: actions/upload-artifact@v3 + if: ${{ github.ref == 'refs/heads/main' }} + with: + name: Upload coverage report + path: app/build/coverage/test/* \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84d24ee --- /dev/null +++ b/.gitignore @@ -0,0 +1,85 @@ +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +.idea + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9653d28 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,70 @@ +# Внесение правок + +## Основные советы + +1. Никогда не используйте merge, только rebase для сохранения линейной истории коммитов +2. **Осторожно** с изменениями в чужих ветках. Придется больно и мучительно делать rebase. Лучше не трогайте чужие + ветки +3. Перепроверьте историю коммитов перед созданием пулл реквеста +4. Каждый коммит должен быть осознанным и быть одним логическим элементом +5. Каждый пулл реквест должен быть осознанным и быть одним логическим элементом +6. **Перепроверьте, что вы в правильной ветке**, никогда не коммитьте напрямую в main + +## Правила добавления коммитов + +Коммиты добавляются в соответствии с conventional commits. Т.е +`(): `. + +Поле `` должно принимать одно из этих значений: + +* `feat` для добавления новой функциональности +* `fix` для исправления бага в программе +* `refactor` для рефакторинга кода, например, переименования переменной +* `test` для добавления тестов, их рефакторинга +* `struct` для изменений связанных с изменением структуры проекта (НО НЕ КОДА), например изменение + расположения папок +* `ci` для различных задач ci/cd + +Поле `` опционально и показывает к какому модулю, классу, методу функции и т.п применены изменения. + +Поле `` содержит суть изменений в повелительном наклонении настоящего времени на английском языке без точки в +конце, первое слово - глагол с большой буквы. Текст сообщения должен включать мотивацию к изменению и контрасты с +предыдущим поведением. + +Примеры: + +* Хорошо: "Add module for future BST implementations" +* Плохо: "Added module for future BST implementations." +* Плохо: "Adds module". +* Очень плохо: "new bug." + +## Правила именования и создания веток + +Ветка под одно (большое) логическое изменение. Формат для веток `/`. Тип аналогичен тому же в +коммитах, +а `` представляет собой короткое описание назначения ветки в kebab-case стиле. + +Примеры хороших названий: + +* `feat/add-avl-tree` +* `ci/add-klint` + +После одобрения пулл реквеста, ветка удаляется. А новая функциональность разрабатывается в новой ветке. + +## Правила для пулл реквестов + +Пулл реквесты оформляются на любом удобном языке. Но должны четко отвечать на 3 вопроса: + +1. Что? +2. Зачем? +3. Почему? + +Ответ на вопрос что должен содержаться в заголовке пулл реквеста. На остальные вопросы ответ должен быть в описании. + +**НЕ ТЫКАТЬ НА ЗЕЛЕНУЮ КНОПОЧКУ `REBASE AND MERGE` БЕЗ РЕВЬЮ** + +**Запрещено** сливать свой пулл реквест в ветку самостоятельно. + +Если тыкаете на зеленую кнопочку, то **убедитесь**, что на ней написано `REBASE AND MERGE` + +Ревью происходит в виде комметариев к пулл реквестам, обсуждения в чате команды и личном общении. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9771046 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-present 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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff842ac --- /dev/null +++ b/README.md @@ -0,0 +1,175 @@ +# trees + +Наше приложение рисует и управляет тремя типами бинарных деревьев: красно-черное, AVL и обычное дерево поиска. +Оно позволяет добавлять и удалять элементы из деревьев, а также сохранять деревья, над которыми велась работа, что +обеспечивает сохранение данных между сеансами работы приложения. + + +## Графический интерфейс + + + +1. При запуске приложения вы увидите на экране три типа дерева. Нажмите на одно из них, чтобы начать работу с нужным деревом. + + + +2. Для добавления в дерево новой ноды напишите нужное значение в соответсвующем окне и нажмите на кнопку +. + + + +3. Для удаления ноды из дерева, напишите ее значение в соответсвующем окне и нажмите на кнопку -. + + + +4. Для сохранения дерева, нажмите на кнопку "Save tree to...". В появившемся диалоговом окне выберите место для сохранения файла и введите название файла. + + + +5. Для загрузки дерева из файла, нажмите на кнопку "Load tree from...". В появившемся диалоговом окне выберите нужный тип дерева и нужный файл с этим типом дерева. + (при несоответствии типа дерева, указанного в диалоговом окне, с тем, который в файле, ничего не произойдет) + + + +6. Для поиска ноды в дереве, введите ее значение в соответствующем поле и нажмите на лупу. Если нода найдена, будет выведено поддерево, для которого данная нода станет рутом. Если нода не найдена, ничего не произойдет. + (Чтобы вернуть изначальное дерево нажмите на кнопку "Reset tree") + + + +7. Если вам нужно вернуть изначальное положение экрана, нажмите на кнопку "Reset view". + + + +## Основные фичи + +1. Поддержка 3 видов (AVL, RB, BS) деревьев поиска с возможностью добавления собственных реализаций +2. (WIP) Поддержка сохранения состояния дерева и восстановления его из Json, Postgres и Neo4j +3. (WIP) Визуализация различных деревьев поиска + +## Информация про архитектуру проекта + +### Деревья поиска + +Чтобы создать новый вид дерева нужно сделать 3 действия. Написать балансировщик, возможно, особый тип `BinTreeNode` и +создать класс отнаследованный от `BinarySearchTree`. + +```kotlin +class RBTree> : + BinarySearchTree>( + balancer = RBBalancer(), + ) +``` + +Все наследники `BinarySearchTree` автоматически умеют выполнять поиск, вставку, удаление значений из дерева, а так же +итерироваться по нему различными способами — выполнять inorder, preorder, levelorder обходы. + +### Использование + +Создать дерево и использовать доступные для него методы можно следующим образом: + +```kotlin +val tree = RBTree() + +tree.add(2) +tree.add(5) +tree.add(1) + +tree.remove(2) + +1 in tree // true +``` + +search возвращает Boolean + + +Тип создаваемого дерева указывается одним из следующих способов: RBTree<Тип>(), AVLTree<>(), BSTree<>(). + +Для RB, AVL, или BS дерева соответственно. + + +### Cохранение + +Каждое из трёх доступных деревьев можно сохранить одним из следующих способов на выбор: + + - Json + - PostgreSQL + - Neo4j + +Пример Json: +```Kotlin +val strategy = AVLTreeStrategy({ SerializableValue(it.toString()) }, { it.value.toInt() }) +val repo = JsonRepo(strategy) + +val tree = AVLTree() + +repo.save("myTree", tree) + +repo.loadByVerboseName("myTree",::AVLTree) + +repo.deleteByVerboseName("myTree") +``` + +Пример Neo4j: +```Kotlin +val strategy = RBTreeStrategy({ SerializableValue(it.toString()) }, { it.value.toInt() }) +val repo = Neo4jRepo( + strategy, Configuration.Builder() + .uri("bolt://localhost") + .credentials("neo4j", "password") + .build() +) + +val tree = RBTree() + +repo.save("myTree", tree) + +repo.loadByVerboseName("myTree", ::RBTree) + +repo.deleteByVerboseNam("myTree") +``` + +Пример PostgreSQL: +```Kotlin +val database = Database.connect( + "jdbc:postgresql://localhost:5432/", driver = "org.postgresql.Driver", + user = "postgres", password = "password" +) + +val repo = PostgresRepo( + AVLTreeStrategy({ SerializableValue(it.toString()) }, + { it.value.toInt() }), + database +) + +val tree = AVLTree() + +repo.save("myTree", tree) + +repo.loadByVerboseName("myTree", ::AVLTree) + +repo.deleteByVerboseNam("myTree") +``` + +Для корректной работы базы данных приложения, а также правильного отображения деревьев необходимо поднять docker. + +Чтобы это сделать нужно обратиться к [документации](https://docs.docker.com/desktop/). + +Также это сделать можно следующим способом: + +``` +docker compose -f "dev-docker-compose.yml" up +``` + +## Внесение изменений + +Внимательно прочитайте раздел [CONTRIBUTING](./CONTRIBUTING.md). + +Быстрый способ начать вносить правки: + +1. Создате новую ветку (`git checkout -b feat/add-amazing-feature`) +2. Сделайте коммит изменений (`git commit -m "feat: Add some amazing feature"`) +3. Запушьте ветку в origin (`git push origin feat/add-amazing-feature`) +4. Откройте пулл реквест + +## Лицензия + +Этот проект используeт лицензию MIT. Подробнее в [LICENSE](./LICENSE). diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..e24eb5f --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,82 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + + +repositories { + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() +} + +plugins { + kotlin("jvm") version "1.8.0" + id("org.jetbrains.compose") version "1.4.0" + kotlin("plugin.serialization") version "1.8.0" + id("jacoco") + id("org.jetbrains.kotlin.plugin.noarg") version "1.8.0" +} + +dependencies { + testImplementation(kotlin("test")) + testImplementation(platform("org.junit:junit-bom:5.9.2")) + testImplementation("org.junit.jupiter:junit-jupiter") + implementation("org.neo4j:neo4j-ogm-core:4.0.5") + runtimeOnly("org.neo4j:neo4j-ogm-bolt-driver:4.0.5") + implementation("org.postgresql:postgresql:42.5.4") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.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) + implementation(compose.material3) + implementation(compose.materialIconsExtended) +} + +jacoco { + toolVersion = "0.8.7" + reportsDirectory.set(layout.buildDirectory.dir("coverage")) +} + +tasks.withType { + reports { + xml.required.set(true) + csv.required.set(true) + html.required.set(true) + } +} + +tasks.test { + useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) +} + +noArg { + annotation("org.neo4j.ogm.annotation.NodeEntity") + annotation("org.neo4j.ogm.annotation.RelationshipEntity") +} +kotlin { + jvmToolchain(17) +} + +compose.desktop { + application { + mainClass = "app.AppKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "trees-visualizer" + packageVersion = "1.0.0" + licenseFile.set(project.file("../LICENSE")) + + macOS { + iconFile.set(project.file("src/main/resources/icon.icns")) + } + windows { + iconFile.set(project.file("src/main/resources/icon.ico")) + } + linux { + iconFile.set(project.file("src/main/resources/icon.png")) + } + } + + } +} diff --git a/app/src/main/kotlin/app/App.kt b/app/src/main/kotlin/app/App.kt new file mode 100644 index 0000000..bcd49b1 --- /dev/null +++ b/app/src/main/kotlin/app/App.kt @@ -0,0 +1,38 @@ +package app + +import androidx.compose.material3.MaterialTheme +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import app.view.TreeChoiceDialog +import java.awt.Dimension + +fun main() { + application { + Window( + onCloseRequest = ::exitApplication, + title = "Graph visualizer", + state = rememberWindowState( + position = WindowPosition(alignment = Alignment.Center), + size = DpSize(800.dp, 800.dp), + ), + icon = painterResource("icon.png") + ) { + window.minimumSize = Dimension(800, 800) + MaterialTheme( + + colorScheme = MaterialTheme.colorScheme.copy( + surface = Color(red = 235, green = 235, blue = 237) + ) + ) { + TreeChoiceDialog() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/bst/AVLTree.kt b/app/src/main/kotlin/app/model/bst/AVLTree.kt new file mode 100644 index 0000000..cbfbb39 --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/AVLTree.kt @@ -0,0 +1,19 @@ +package app.model.bst + +import app.model.bst.balancer.AVLBalancer +import app.model.bst.node.AVLTreeNode + +/** + * AVLTree is a self-balancing binary search tree + * It uses the AVLBalancer to maintain balance and ensure that the height of the left and right subtrees of any node + * differ by at most one. + * + * @param E the type of elements stored in the tree + * + * @see BinarySearchTree + * @see AVLBalancer + */ +class AVLTree> : + BinarySearchTree>( + balancer = AVLBalancer(), + ) diff --git a/app/src/main/kotlin/app/model/bst/BSTree.kt b/app/src/main/kotlin/app/model/bst/BSTree.kt new file mode 100644 index 0000000..a663992 --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/BSTree.kt @@ -0,0 +1,18 @@ +package app.model.bst + +import app.model.bst.balancer.BSBalancer +import app.model.bst.node.BinSearchTreeNode + +/** + * BSTree is a binary search tree that implements the BinarySearchTree interface. + * It uses the BSBalancer to insert values to the tree. + * + * @param E the type of elements stored in the tree + * + * @see BinarySearchTree + * @see BSBalancer + */ +class BSTree> : + BinarySearchTree>( + balancer = BSBalancer(), + ) diff --git a/app/src/main/kotlin/app/model/bst/BinarySearchTree.kt b/app/src/main/kotlin/app/model/bst/BinarySearchTree.kt new file mode 100644 index 0000000..78cc6c3 --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/BinarySearchTree.kt @@ -0,0 +1,100 @@ +package app.model.bst + +import app.model.bst.balancer.BinTreeBalancer +import app.model.bst.iterator.InOrderIterator +import app.model.bst.iterator.LevelOrderIterator +import app.model.bst.iterator.PreOrderIterator +import app.model.bst.node.BinTreeNode + +/** + * An abstract class representing a binary search tree. + * + * @param the type of elements stored in the tree + * @param the type of node used in the tree + */ +abstract class BinarySearchTree, NodeType : BinTreeNode>( + /** + * The balancer used to perform all magic with balance + */ + protected val balancer: BinTreeBalancer, +) : Iterable { + /** + * The root node of the tree. + */ + var root: NodeType? = null + + /** + * Adds a value to the tree. + * + * @param value the value to add + * @param unique whether to only add unique values (replaces old with new one if unique and found) + */ + open fun add(value: E, unique: Boolean = false) { + root = balancer.add(root, value, unique) + } + + /** + * Removes a value from the tree. + * + * @param value the value to remove + */ + open fun remove(value: E) { + root = balancer.remove(root, value) + } + + /** + * Checks if a value is in the tree. + * + * @param value the value to check for + * @return true if the value is in the tree, false otherwise + */ + operator fun contains(value: E): Boolean { + return search(root, value) + } + + /** + * Returns an iterator over the elements in the tree in in-order traversal order. + * + * @return an iterator over the elements in the tree in in-order traversal order + */ + override operator fun iterator(): Iterator { + return InOrderIterator(root) + } + + /** + * Returns an iterator over the elements in the tree in pre-order traversal order. + * + * @return an iterator over the elements in the tree in pre-order traversal order + */ + fun preOrderIterator(): Iterator { + return PreOrderIterator(root) + } + + /** + * Returns an iterator over the elements in the tree in level-order traversal order. + * + * @return an iterator over the elements in the tree in level-order traversal order + */ + fun levelOrderIterator(): Iterator { + return LevelOrderIterator(root) + } + + /** + * Recursively searches for a value in the tree. + * + * @param node the node to start the search from + * @param value the value to search for + * @return true if the value is in the tree, false otherwise + */ + private tailrec fun search(node: NodeType?, value: E): Boolean { + if (node == null) { + return false + } + + if (value == node.value) { + return true + } + + return search((if (value < node.value) node.left else node.right), value) + } +} diff --git a/app/src/main/kotlin/app/model/bst/RBTree.kt b/app/src/main/kotlin/app/model/bst/RBTree.kt new file mode 100644 index 0000000..7df71b2 --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/RBTree.kt @@ -0,0 +1,24 @@ +package app.model.bst + +import app.model.bst.balancer.RBBalancer +import app.model.bst.node.RedBlackTreeNode + +/** + * RBTree is a self-balancing binary search tree + * It uses the RBBalancer to maintain balance and ensure that the tree satisfies the red-black tree properties. + * + * 1. Every node is either red or black. + * 2. The root node is always black. + * 3. Every leaf node (null node) is black. + * 4. If a node is red, then both its children are black. + * 5. For each node, all simple paths from the node to descendant leaves contain the same number of black nodes. + * + * @param E the type of elements stored in the tree. + * + * @see BinarySearchTree + * @see RBBalancer + */ +class RBTree> : + BinarySearchTree>( + balancer = RBBalancer(), + ) diff --git a/app/src/main/kotlin/app/model/bst/balancer/AVLBalancer.kt b/app/src/main/kotlin/app/model/bst/balancer/AVLBalancer.kt new file mode 100644 index 0000000..05b4464 --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/balancer/AVLBalancer.kt @@ -0,0 +1,120 @@ +package app.model.bst.balancer + +import app.model.bst.node.AVLNode +import app.model.bst.node.AVLTreeNode +import kotlin.math.max + +internal class AVLBalancer> : BinTreeBalancer> { + override fun add(root: AVLTreeNode?, value: E, unique: Boolean): AVLTreeNode { + fun insertToSubtree(subtree: AVLTreeNode?): AVLTreeNode? { + if (subtree == null) { + return AVLNode(value) + } + + if (value < subtree.value) { + subtree.left = insertToSubtree(subtree.left) + } else if (value > subtree.value) { + subtree.right = insertToSubtree(subtree.right) + } else { + if (unique) return subtree + else subtree.right = insertToSubtree(subtree.right) + } + + return balance(subtree) + } + return insertToSubtree(root) ?: throw IllegalStateException("Root after fixup must be not null") + } + + private fun balance(subtree: AVLTreeNode): AVLTreeNode? { + subtree.height = 1 + max(height(subtree.left), height(subtree.right)) + + val balance = getBalance(subtree) + + if (balance > 1 && getBalance(subtree.left) >= 0) { + return rotateRight(subtree) + } + + if (balance > 1 && getBalance(subtree.left) < 0) { + subtree.left = rotateLeft(subtree.left) + return rotateRight(subtree) + } + + if (balance < -1 && getBalance(subtree.right) <= 0) { + return rotateLeft(subtree) + } + + if (balance < -1 && getBalance(subtree.right) > 0) { + subtree.right = rotateRight(subtree.right) + return rotateLeft(subtree) + } + + return subtree + } + + private fun height(node: AVLTreeNode?): Int { + return node?.height ?: 0 + } + + private fun getBalance(node: AVLTreeNode?): Int { + return if (node == null) 0 else height(node.left) - height(node.right) + } + + private fun rotateRight(y: AVLTreeNode?): AVLTreeNode? { + val x = y?.left + val child = x?.right + + x?.right = y + y?.left = child + + y?.height = max(height(y?.left), height(y?.right)) + 1 + x?.height = 1 + max(height(x?.left), height(x?.right)) + + return x + } + + private fun rotateLeft(x: AVLTreeNode?): AVLTreeNode? { + val y = x?.right + val child = y?.left + + y?.left = x + x?.right = child + + x?.height = 1 + max(height(x?.left), height(x?.right)) + y?.height = 1 + max(height(y?.left), height(y?.right)) + + return y + } + + private fun minValueNode(node: AVLTreeNode): AVLTreeNode { + var current = node + while (true) { + current = current.left ?: break + } + return current + } + + override fun remove(root: AVLTreeNode?, value: E): AVLTreeNode? { + if (root == null) { + return null + } + + if (value < root.value) { + root.left = remove(root.left, value) + } else if (value > root.value) { + root.right = remove(root.right, value) + } else { + if (root.left == null || root.right == null) { + return root.left ?: root.right + } else { + val temp = + minValueNode( + root.right + ?: throw IllegalStateException("Impossible to find minValueNode from node without right child") + ) + root.value = temp.value + root.right = remove(root.right, temp.value) + } + } + return balance(root) + } +} diff --git a/app/src/main/kotlin/app/model/bst/balancer/BSBalancer.kt b/app/src/main/kotlin/app/model/bst/balancer/BSBalancer.kt new file mode 100644 index 0000000..19d346e --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/balancer/BSBalancer.kt @@ -0,0 +1,55 @@ +package app.model.bst.balancer + +import app.model.bst.node.BSNode +import app.model.bst.node.BinSearchTreeNode + +internal class BSBalancer> : BinTreeBalancer> { + override fun add(root: BinSearchTreeNode?, value: E, unique: Boolean): BinSearchTreeNode { + if (root == null) { + return BSNode(value) + } + + if (value < root.value) { + root.left = add(root.left, value, unique) + } else if (value > root.value) { + root.right = add(root.right, value, unique) + } else { + if (unique) return root + else root.right = add(root.right, value, false) + } + return root + } + + override fun remove(root: BinSearchTreeNode?, value: E): BinSearchTreeNode? { + if (root == null) { + return null + } + + if (value < root.value) { + root.left = remove(root.left, value) + } else if (value > root.value) { + root.right = remove(root.right, value) + } else { + if (root.left == null || root.right == null) { + return root.left ?: root.right + } else { + val temp = + minValueNode( + root.right + ?: throw IllegalStateException("Impossible to find minValueNode from node without right child") + ) + root.value = temp.value + root.right = remove(root.right, temp.value) + } + } + return root + } + + private fun minValueNode(node: BinSearchTreeNode): BinSearchTreeNode { + var current = node + while (true) { + current = current.left ?: break + } + return current + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/bst/balancer/BinTreeBalancer.kt b/app/src/main/kotlin/app/model/bst/balancer/BinTreeBalancer.kt new file mode 100644 index 0000000..c98e6c6 --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/balancer/BinTreeBalancer.kt @@ -0,0 +1,27 @@ +package app.model.bst.balancer + +import app.model.bst.node.BinTreeNode + +/** + * An interface for a binary tree balancer. + * @param E the type of element stored in the tree, must implement Comparable interface. + * @param NodeType the type of node used in the tree, must implement BinTreeNode interface. + */ +interface BinTreeBalancer, NodeType : BinTreeNode> { + /** + * Adds a new node with a value to the root's subtree, and rebalances it if necessary. + * @param root the root node of the subtree to add the new node to. + * @param value the value to add to the tree. + * @param unique whether to insert only unique values in the tree. + * @return the new root node of the subtree. + */ + fun add(root: NodeType?, value: E, unique: Boolean): NodeType + + /** + * Removes a node with a value from the root's subtree, and rebalances it if necessary. + * @param root the root node of the subtree to remove the node from. + * @param value the value to remove from the tree. + * @return the new root node of the subtree. + */ + fun remove(root: NodeType?, value: E): NodeType? +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/bst/balancer/RBBalancer.kt b/app/src/main/kotlin/app/model/bst/balancer/RBBalancer.kt new file mode 100644 index 0000000..b9390a1 --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/balancer/RBBalancer.kt @@ -0,0 +1,246 @@ +package app.model.bst.balancer + +import app.model.bst.node.RBNode +import app.model.bst.node.RedBlackTreeNode + +internal class RBBalancer> : BinTreeBalancer> { + override fun add(root: RedBlackTreeNode?, value: E, unique: Boolean): RedBlackTreeNode { + val newNode = RBNode(value) + + fun addToSubtree(subtree: RedBlackTreeNode?): RedBlackTreeNode { + val node = subtree ?: return newNode + + if (value < node.value) node.left = addToSubtree(node.left).also { it.parent = node } + else if (value > node.value) node.right = addToSubtree(node.right).also { it.parent = node } + else { + if (unique) node.value = value + else node.right = addToSubtree(node.right).also { it.parent = node } + } + + return node + } + + return fixAfterInsertion(newNode, addToSubtree(root)) + ?: throw IllegalStateException("Root after fixup must be not null") + } + + private fun rotateLeft(node: RedBlackTreeNode?, root: RedBlackTreeNode?): RedBlackTreeNode? { + val rightChild = node?.right ?: throw IllegalStateException("Node to rotate must have a right child") + node.right = rightChild.left + + if (rightChild.left != null) { + rightChild.left?.parent = node + } + + rightChild.parent = node.parent + var newRoot = root + + if (node.parent == null) { + newRoot = rightChild + } else if (node === node.parent?.left) { + node.parent?.left = rightChild + } else { + node.parent?.right = rightChild + } + + rightChild.left = node + node.parent = rightChild + + return newRoot + } + + private fun rotateRight(node: RedBlackTreeNode?, root: RedBlackTreeNode?): RedBlackTreeNode? { + val leftChild = node?.left ?: throw IllegalStateException("Node to rotate must have a left child") + node.left = leftChild.right + + if (leftChild.right != null) { + leftChild.right?.parent = node + } + + leftChild.parent = node.parent + var newRoot = root + + if (node.parent == null) { + newRoot = leftChild + } else if (node === node.parent?.right) { + node.parent?.right = leftChild + } else { + node.parent?.left = leftChild + } + + leftChild.right = node + node.parent = leftChild + + return newRoot + } + + private fun fixAfterInsertion(node: RedBlackTreeNode, root: RedBlackTreeNode): RedBlackTreeNode? { + if (node.parent == null) return root.also { it.color = RedBlackTreeNode.Color.BLACK } + var current: RedBlackTreeNode? = node + var newRoot: RedBlackTreeNode? = root + + while (current?.parent?.color == RedBlackTreeNode.Color.RED) { + if (current.parent === current.parent?.parent?.left) { + val uncle = current.parent?.parent?.right + if (uncle?.color == RedBlackTreeNode.Color.RED) { + current.parent?.color = RedBlackTreeNode.Color.BLACK + uncle.color = RedBlackTreeNode.Color.BLACK + current.parent?.parent?.color = RedBlackTreeNode.Color.RED + current = current.parent?.parent + } else { + if (current == current.parent?.right) { + current = current.parent + newRoot = rotateLeft(current, newRoot) + } + current?.parent?.color = RedBlackTreeNode.Color.BLACK + current?.parent?.parent?.color = RedBlackTreeNode.Color.RED + newRoot = rotateRight(current?.parent?.parent, newRoot) + } + + } else { + val uncle = current.parent?.parent?.left + if (uncle?.color == RedBlackTreeNode.Color.RED) { + current.parent?.color = RedBlackTreeNode.Color.BLACK + uncle.color = RedBlackTreeNode.Color.BLACK + current.parent?.parent?.color = RedBlackTreeNode.Color.RED + current = current.parent?.parent + } else { + if (current == current.parent?.left) { + current = current.parent + newRoot = rotateRight(current, newRoot) + } + current?.parent?.color = RedBlackTreeNode.Color.BLACK + current?.parent?.parent?.color = RedBlackTreeNode.Color.RED + newRoot = rotateLeft(current?.parent?.parent, newRoot) + } + } + } + newRoot?.color = RedBlackTreeNode.Color.BLACK + + return newRoot + } + + override fun remove(root: RedBlackTreeNode?, value: E): RedBlackTreeNode? { + var current = findNode(root, value) ?: return root + var newRoot = root + + if (current.left != null && current.right != null) { + val successor = minValueNode( + current.right + ?: throw IllegalStateException("Impossible to find minValueNode from node without left child") + ) + current.value = successor.value + current = successor + } + + val child = if (current.left != null) current.left else current.right + if (child != null) { + child.parent = current.parent + if (current.parent == null) return child + if (current == current.parent?.left) { + current.parent?.left = child + } else { + current.parent?.right = child + } + + if (current.color == RedBlackTreeNode.Color.BLACK) { + newRoot = fixAfterDeletion(child, newRoot) + } + } else if (current.parent == null) { + return null + } else { + if (current.color == RedBlackTreeNode.Color.BLACK) { + newRoot = fixAfterDeletion(current, newRoot) + } + if (current.parent?.left == current) { + current.parent?.left = null + } else { + current.parent?.right = null + } + } + + return newRoot + } + + private fun takeColor(node: RedBlackTreeNode?): RedBlackTreeNode.Color { + return node?.color ?: RedBlackTreeNode.Color.BLACK + } + + private fun fixAfterDeletion(node: RedBlackTreeNode?, root: RedBlackTreeNode?): RedBlackTreeNode? { + var newRoot = root + var current = node + var sibling: RedBlackTreeNode? + + while (current !== newRoot && takeColor(current) == RedBlackTreeNode.Color.BLACK) { + if (current === current?.parent?.left) { + sibling = current?.parent?.right + if (takeColor(sibling) == RedBlackTreeNode.Color.RED) { + sibling?.color = RedBlackTreeNode.Color.BLACK + current?.parent?.color = RedBlackTreeNode.Color.RED + newRoot = rotateLeft(current?.parent, newRoot) + sibling = current?.parent?.right + } + if (takeColor(sibling?.left) == RedBlackTreeNode.Color.BLACK && takeColor(sibling?.right) == RedBlackTreeNode.Color.BLACK) { + sibling?.color = RedBlackTreeNode.Color.RED + current = current?.parent + } else { + if (takeColor(sibling?.right) == RedBlackTreeNode.Color.BLACK) { + sibling?.left?.color = RedBlackTreeNode.Color.BLACK + sibling?.color = RedBlackTreeNode.Color.RED + newRoot = rotateRight(sibling, newRoot) + sibling = current?.parent?.right + } + sibling?.color = current?.parent?.color ?: RedBlackTreeNode.Color.BLACK + current?.parent?.color = RedBlackTreeNode.Color.BLACK + sibling?.right?.color = RedBlackTreeNode.Color.BLACK + newRoot = rotateLeft(current?.parent, newRoot) + current = newRoot + } + } else { + sibling = current?.parent?.left + if (takeColor(sibling) == RedBlackTreeNode.Color.RED) { + sibling?.color = RedBlackTreeNode.Color.BLACK + current?.parent?.color = RedBlackTreeNode.Color.RED + newRoot = rotateRight(current?.parent, newRoot) + sibling = current?.parent?.left + } + if (takeColor(sibling?.right) == RedBlackTreeNode.Color.BLACK && takeColor(sibling?.left) == RedBlackTreeNode.Color.BLACK) { + sibling?.color = RedBlackTreeNode.Color.RED + current = current?.parent + } else { + if (takeColor(sibling?.left) == RedBlackTreeNode.Color.BLACK) { + sibling?.right?.color = RedBlackTreeNode.Color.BLACK + sibling?.color = RedBlackTreeNode.Color.RED + newRoot = rotateLeft(sibling, newRoot) + sibling = current?.parent?.left + } + sibling?.color = current?.parent?.color ?: RedBlackTreeNode.Color.BLACK + current?.parent?.color = RedBlackTreeNode.Color.BLACK + sibling?.left?.color = RedBlackTreeNode.Color.BLACK + newRoot = rotateRight(current?.parent, newRoot) + current = newRoot + + } + } + } + current?.color = RedBlackTreeNode.Color.BLACK + newRoot?.parent = null + return newRoot + } + + private fun findNode(node: RedBlackTreeNode?, value: E): RedBlackTreeNode? { + node ?: return null + if (value == node.value) { + return node + } + return findNode(if (value < node.value) node.left else node.right, value) + } + + private fun minValueNode(node: RedBlackTreeNode): RedBlackTreeNode { + var current = node + while (true) { + current = current.left ?: break + } + return current + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/bst/iterator/InOrderIterator.kt b/app/src/main/kotlin/app/model/bst/iterator/InOrderIterator.kt new file mode 100644 index 0000000..5e99baf --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/iterator/InOrderIterator.kt @@ -0,0 +1,52 @@ +package app.model.bst.iterator + +import app.model.bst.node.BinTreeNode +import java.util.* + +/** + * InOrderIterator is an internal class that implements the Iterator interface for a binary tree. + * It iterates over the elements of the tree in in-order traversal order. + * The tree stores elements of type E, which must implement the Comparable interface. + * The NodeType parameter specifies the type of nodes in the tree. + * + * @param E the type of elements stored in the tree, which must implement the Comparable interface. + * @param NodeType the type of nodes in the tree. + * @property root the root node of the tree. + * + * @see Iterator + * @see BinTreeNode + */ +internal class InOrderIterator, NodeType : BinTreeNode>(root: NodeType?) : Iterator { + private val stack = LinkedList() + + init { + var node = root + while (node != null) { + stack.push(node) + node = node.left + } + } + + /** + * @return true if there are more elements to iterate over, false otherwise. + */ + override fun hasNext(): Boolean { + return stack.isNotEmpty() + } + + /** + * @return the next element in the iteration. + */ + override fun next(): E { + val node = stack.pop() + var nextNode = node.right + while (nextNode != null) { + stack.push(nextNode) + nextNode = nextNode.left + } + return node.value + } +} + + + diff --git a/app/src/main/kotlin/app/model/bst/iterator/LevelOrderIterator.kt b/app/src/main/kotlin/app/model/bst/iterator/LevelOrderIterator.kt new file mode 100644 index 0000000..60a0205 --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/iterator/LevelOrderIterator.kt @@ -0,0 +1,43 @@ +package app.model.bst.iterator + +import app.model.bst.node.BinTreeNode +import java.util.* + +/** + * The LevelOrderIterator class is an internal implementation of the Iterator interface for a binary tree. + * It traverses the tree in level-order, visiting each node at each level before moving on to the next level. + * The tree stores elements of type E, which must implement the Comparable interface. + * The NodeType parameter specifies the type of nodes in the tree. + * + * @param E the type of elements stored in the tree, which must implement the Comparable interface. + * @param NodeType the type of nodes in the tree. + * @property root the root node of the tree. + * + * @see Iterator + * @see BinTreeNode + */ +internal class LevelOrderIterator, NodeType : BinTreeNode>(root: NodeType?) : + Iterator { + private val queue = LinkedList() + + init { + root?.let { queue.offer(it) } + } + + /** + * @return true if there are more elements to iterate over, false otherwise. + */ + override fun hasNext(): Boolean { + return queue.isNotEmpty() + } + + /** + * @return the next element in the iteration. + */ + override fun next(): E { + val node = queue.poll() + node.left?.let { queue.offer(it) } + node.right?.let { queue.offer(it) } + return node.value + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/bst/iterator/PreOrderIterator.kt b/app/src/main/kotlin/app/model/bst/iterator/PreOrderIterator.kt new file mode 100644 index 0000000..03517d2 --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/iterator/PreOrderIterator.kt @@ -0,0 +1,42 @@ +package app.model.bst.iterator + +import app.model.bst.node.BinTreeNode +import java.util.* + +/** + * The PreOrderIterator class is an internal implementation of the Iterator interface for a binary tree. + * It traverses the tree in pre-order, visiting each node before its children. + * The tree stores elements of type E, which must implement the Comparable interface. + * The NodeType parameter specifies the type of nodes in the tree. + * + * @param E the type of elements stored in the tree, which must implement the Comparable interface. + * @param NodeType the type of nodes in the tree. + * @property root the root node of the tree. + * + * @see Iterator + * @see BinTreeNode + */ +internal class PreOrderIterator, NodeType : BinTreeNode>(root: NodeType?) : Iterator { + private val stack = LinkedList() + + init { + root?.let { stack.push(it) } + } + + /** + * @return true if there are more elements to iterate over, false otherwise. + */ + override fun hasNext(): Boolean { + return stack.isNotEmpty() + } + + /** + * @return the next element in the iteration. + */ + override fun next(): E { + val node = stack.pop() + node.right?.let { stack.push(it) } + node.left?.let { stack.push(it) } + return node.value + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/bst/node/AVLNode.kt b/app/src/main/kotlin/app/model/bst/node/AVLNode.kt new file mode 100644 index 0000000..8a0ce5d --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/node/AVLNode.kt @@ -0,0 +1,17 @@ +package app.model.bst.node + +/** + * Internal class representing a node in an AVL tree. + * @param E the type of element stored in the node, must implement Comparable interface. + * @property value the value stored in the node. + * @property height the height of the node in the tree. + * @property left the left child of the node. + * @property right the right child of the node. + * @constructor creates a new AVLNode with the given value, height, left and right children. + */ +internal class AVLNode>( + override var value: E, + override var height: Int = 1, + override var left: AVLTreeNode? = null, + override var right: AVLTreeNode? = null, +) : AVLTreeNode diff --git a/app/src/main/kotlin/app/model/bst/node/AVLTreeNode.kt b/app/src/main/kotlin/app/model/bst/node/AVLTreeNode.kt new file mode 100644 index 0000000..c7cbda2 --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/node/AVLTreeNode.kt @@ -0,0 +1,14 @@ +package app.model.bst.node + +/** + * Represents a node in an AVL Tree. + * + * @param E the type of element stored in the node + * @see BinTreeNode + */ +interface AVLTreeNode> : BinTreeNode> { + /** + * The height of the node, which is the length of the longest path from the node to a leaf node. + */ + var height: Int +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/bst/node/BSNode.kt b/app/src/main/kotlin/app/model/bst/node/BSNode.kt new file mode 100644 index 0000000..69c2047 --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/node/BSNode.kt @@ -0,0 +1,16 @@ +package app.model.bst.node + +/** + * Represents a node in a Binary Search Tree. + * + * @param E the type of element stored in the node + * @property value the value stored in the node. + * @property left the left child node of this node, or null if this node has no left child. + * @property right the right child node of this node, or null if this node has no right child. + * @see BinSearchTreeNode + */ +internal class BSNode>( + override var value: E, + override var left: BinSearchTreeNode? = null, + override var right: BinSearchTreeNode? = null, +) : BinSearchTreeNode \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/bst/node/BinSearchTreeNode.kt b/app/src/main/kotlin/app/model/bst/node/BinSearchTreeNode.kt new file mode 100644 index 0000000..74a0d29 --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/node/BinSearchTreeNode.kt @@ -0,0 +1,10 @@ +package app.model.bst.node + +/** + * Represents a node in a Binary Search Tree. + * + * @param E the type of element stored in the node + * @see BinTreeNode + */ +interface BinSearchTreeNode> : BinTreeNode> { +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/bst/node/BinTreeNode.kt b/app/src/main/kotlin/app/model/bst/node/BinTreeNode.kt new file mode 100644 index 0000000..26a85ed --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/node/BinTreeNode.kt @@ -0,0 +1,16 @@ +package app.model.bst.node + +/** + * Represents a node in any Binary Tree. + * + * @param E the type of element stored in the node + * @param Subtype the subtype of the node + * @property value the value stored in the node. + * @property left the left child node of this node, or null if this node has no left child. + * @property right the right child node of this node, or null if this node has no right child. + */ +interface BinTreeNode, Subtype : BinTreeNode> { + var value: E + var left: Subtype? + var right: Subtype? +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/bst/node/RBNode.kt b/app/src/main/kotlin/app/model/bst/node/RBNode.kt new file mode 100644 index 0000000..62f5145 --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/node/RBNode.kt @@ -0,0 +1,20 @@ +package app.model.bst.node + +/** + * Represents a node in a Red-Black Tree. + * + * @param E the type of element stored in the node + * @property value the value stored in the node. + * @property parent the parent node of this node, or null if this node is the root of the tree. + * @property color the color of the node, which can be either RED or BLACK. + * @property left the left child node of this node, or null if this node has no left child. + * @property right the right child node of this node, or null if this node has no right child. + * @see RedBlackTreeNode + */ +internal class RBNode>( + override var value: E, + override var parent: RedBlackTreeNode? = null, + override var color: RedBlackTreeNode.Color = RedBlackTreeNode.Color.RED, + override var left: RedBlackTreeNode? = null, + override var right: RedBlackTreeNode? = null, +) : RedBlackTreeNode \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/bst/node/RedBlackTreeNode.kt b/app/src/main/kotlin/app/model/bst/node/RedBlackTreeNode.kt new file mode 100644 index 0000000..8feb05e --- /dev/null +++ b/app/src/main/kotlin/app/model/bst/node/RedBlackTreeNode.kt @@ -0,0 +1,26 @@ +package app.model.bst.node + +/** + * Represents a node in a Red-Black Tree. + * + * @param E the type of element stored in the node + * @see BinTreeNode + */ +interface RedBlackTreeNode> : BinTreeNode> { + /** + * The color of the node, which can be either RED or BLACK. + */ + var color: Color + + /** + * The parent node of this node, or null if this node is the root of the tree. + */ + var parent: RedBlackTreeNode? + + /** + * An enum representing the possible colors of a Red-Black Tree node. + */ + enum class Color { + RED, BLACK + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/repo/JsonRepo.kt b/app/src/main/kotlin/app/model/repo/JsonRepo.kt new file mode 100644 index 0000000..d14acce --- /dev/null +++ b/app/src/main/kotlin/app/model/repo/JsonRepo.kt @@ -0,0 +1,44 @@ +package app.model.repo + +import app.model.bst.BinarySearchTree +import app.model.bst.node.BinTreeNode +import app.model.repo.serialization.SerializableTree +import app.model.repo.serialization.strategy.SerializationStrategy +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import java.io.File + +class JsonRepo, + Node : BinTreeNode, + BST : BinarySearchTree>( + strategy: SerializationStrategy, + private val file: File, +) : Repository(strategy) { + override fun save(verboseName: String, tree: BST) { + try { + val json = Json.encodeToString(tree.toSerializableTree(verboseName)) + file.writeText(json) + } catch (e: Exception) { + throw IllegalArgumentException("Impossible to save tree with provided verbose name") + } + } + + override fun loadByVerboseName(verboseName: String, factory: () -> BST): BST { + try { + val json = file.readText() + val serializableTree = Json.decodeFromString(json) + return factory().apply { root = strategy.buildNode(serializableTree.root) } + } catch (e: Exception) { + throw IllegalArgumentException("Impossible to load tree with provided arguments") + } + } + + override fun deleteByVerboseName(verboseName: String) { + try { + file.delete() + } catch (e: Exception) { + throw IllegalArgumentException("Impossible to delete tree with provided verbose name") + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/repo/Neo4jRepo.kt b/app/src/main/kotlin/app/model/repo/Neo4jRepo.kt new file mode 100644 index 0000000..7624868 --- /dev/null +++ b/app/src/main/kotlin/app/model/repo/Neo4jRepo.kt @@ -0,0 +1,182 @@ +package app.model.repo + +import app.model.bst.BinarySearchTree +import app.model.bst.node.BinTreeNode +import app.model.repo.serialization.* +import app.model.repo.serialization.strategy.SerializationStrategy +import org.neo4j.ogm.annotation.* +import org.neo4j.ogm.config.Configuration +import org.neo4j.ogm.cypher.ComparisonOperator +import org.neo4j.ogm.cypher.Filter +import org.neo4j.ogm.cypher.Filters +import org.neo4j.ogm.session.SessionFactory + +@NodeEntity +class SerializableNodeEntity( + @Property + var value: SerializableValue, + + @Property + var metadata: Metadata, + + @Relationship(type = "LEFT") + var left: SerializableNodeEntity? = null, + + @Relationship(type = "RIGHT") + var right: SerializableNodeEntity? = null, +) { + @Id + @GeneratedValue + var id: Long? = null +} + +@NodeEntity +class SerializableTreeEntity( + @Property + var verboseName: String, + + @Property + var typeOfTree: TypeOfTree, + + @Relationship(type = "ROOT") + var root: SerializableNodeEntity? = null, +) { + + @Id + @GeneratedValue + var id: Long? = null + +} + +/** + * A Neo4j repository for serializable tree data structures. + * + * @param the type of elements stored in the tree + * @param the type of nodes in the tree + * @param the type of binary search tree being serialized or deserialized + * @property strategy the serialization strategy used by the repository + * @property configuration the Neo4j configuration used by the repository + */ +class Neo4jRepo, + Node : BinTreeNode, + BST : BinarySearchTree>( + strategy: SerializationStrategy, + configuration: Configuration +) : Repository(strategy) { + + private val sessionFactory = SessionFactory(configuration, "app.model.repo") + private val session = sessionFactory.openSession() + + /** + * Converts a serializable node entity to a serializable node. + * + * @receiver the serializable node entity to convert + * @return the serializable node that was converted from the Neo4j ogm entity + */ + private fun SerializableNodeEntity.toNode(): SerializableNode { + return SerializableNode( + value, + metadata, + left?.toNode(), + right?.toNode() + ) + } + + /** + * Converts a serializable tree entity to a serializable tree. + * + * @receiver the serializable tree entity to convert + * @return the serializable tree that was converted from the Neo4j ogm entity + */ + private fun SerializableTreeEntity.toTree(): SerializableTree { + return SerializableTree( + verboseName, + typeOfTree, + root?.toNode() + ) + } + + /** + * Converts a serializable node to a serializable node entity. + * + * @receiver the serializable node to convert + * @return the serializable node entity that was converted from the node + */ + private fun SerializableNode.toEntity(): SerializableNodeEntity { + return SerializableNodeEntity( + value, + metadata, + left?.toEntity(), + right?.toEntity() + ) + } + + /** + * Converts a serializable tree to a serializable tree entity. + * + * @receiver the serializable tree to convert + * @return the serializable tree entity that was converted from the tree + */ + private fun SerializableTree.toEntity(): SerializableTreeEntity { + return SerializableTreeEntity( + verboseName, + typeOfTree, + root?.toEntity() + ) + } + + /** + * Saves a binary search tree to the Neo4j database. + * + * @param verboseName the name of the tree being saved + * @param tree the binary search tree to save + */ + override fun save(verboseName: String, tree: BST) { + deleteByVerboseName(verboseName) + val entityTree = tree.toSerializableTree(verboseName).toEntity() + session.save(entityTree) + } + + /** + * Finds a serializable tree entity by its verbose name. + * + * @param verboseName the verbose name of the tree entity to find + * @return a list of serializable tree entities matching the verbose name and type of tree + */ + private fun findByVerboseName(verboseName: String) = session.loadAll( + SerializableTreeEntity::class.java, + Filters().and( + Filter("verboseName", ComparisonOperator.EQUALS, verboseName) + ).and( + Filter("typeOfTree", ComparisonOperator.EQUALS, strategy.typeOfTree) + ), + -1 + ) + + /** + * Loads a binary search tree from the Neo4j database by its verbose name. + * + * @param verboseName the verbose name of the tree to load + * @param factory a function that creates a new instance of the binary search tree being loaded + * @return the binary search tree loaded from the database + */ + override fun loadByVerboseName(verboseName: String, factory: () -> BST): BST { + val treeEntity = findByVerboseName(verboseName).singleOrNull() + + return factory().apply { + root = strategy.buildNode(treeEntity?.toTree()?.root) + } + } + + /** + * Deletes a serializable tree entity and all of its related nodes and relationships from the Neo4j database. + * + * @param verboseName the verbose name of the tree entity to delete + */ + override fun deleteByVerboseName(verboseName: String) { + session.query( + "MATCH toDelete=(t:SerializableTreeEntity {typeOfTree: \$typeOfTree, verboseName : \$verboseName})-[*0..]->() DETACH DELETE toDelete", + mapOf("typeOfTree" to strategy.typeOfTree, "verboseName" to verboseName) + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/repo/PostgresRepo.kt b/app/src/main/kotlin/app/model/repo/PostgresRepo.kt new file mode 100644 index 0000000..a9bd29e --- /dev/null +++ b/app/src/main/kotlin/app/model/repo/PostgresRepo.kt @@ -0,0 +1,126 @@ +package app.model.repo + +import app.model.bst.BinarySearchTree +import app.model.bst.node.BinTreeNode +import app.model.repo.serialization.Metadata +import app.model.repo.serialization.SerializableNode +import app.model.repo.serialization.SerializableTree +import app.model.repo.serialization.SerializableValue +import app.model.repo.serialization.strategy.SerializationStrategy +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.sql.Database +import org.jetbrains.exposed.sql.ReferenceOption +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.transactions.transaction + +private object NodesTable : IntIdTable("nodes") { + val value = text("data") + val metadata = text("metadata") + val left = reference("left_id", NodesTable).nullable() + val right = reference("right_id", NodesTable).nullable() + val tree = reference("tree_id", TreesTable, onDelete = ReferenceOption.CASCADE) +} + +private object TreesTable : IntIdTable("trees") { + val verboseName = text("name") + val typeOfTree = text("type") + val root = reference("root_node_id", NodesTable).nullable() + + init { + uniqueIndex(verboseName, typeOfTree) + } +} + +class DatabaseNodeEntity(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(NodesTable) + + var value by NodesTable.value + var metadata by NodesTable.metadata + var left by DatabaseNodeEntity optionalReferencedOn NodesTable.left + var right by DatabaseNodeEntity optionalReferencedOn NodesTable.right + var tree by DatabaseTreeEntity referencedOn NodesTable.tree +} + +class DatabaseTreeEntity(id: EntityID) : IntEntity(id) { + companion object : IntEntityClass(TreesTable) + + var verboseName by TreesTable.verboseName + var typeOfTree by TreesTable.typeOfTree + var root by DatabaseNodeEntity optionalReferencedOn TreesTable.root +} + + +class PostgresRepo, + Node : BinTreeNode, + BST : BinarySearchTree>( + strategy: SerializationStrategy, + private val db: Database +) : Repository(strategy) { + init { + transaction(db) { + SchemaUtils.create(TreesTable) + SchemaUtils.create(NodesTable) + } + } + + private fun SerializableNode.toEntity(tree: DatabaseTreeEntity): DatabaseNodeEntity { + return DatabaseNodeEntity.new { + this@new.value = this@toEntity.value.value + this@new.metadata = this@toEntity.metadata.value + this@new.left = this@toEntity.left?.toEntity(tree) + this@new.right = this@toEntity.right?.toEntity(tree) + this@new.tree = tree + } + } + + private fun SerializableTree.toEntity(): DatabaseTreeEntity { + return DatabaseTreeEntity.new { + this@new.verboseName = this@toEntity.verboseName + this@new.typeOfTree = strategy.typeOfTree.toString() + }.also { it.root = root?.toEntity(it) } + } + + private fun DatabaseTreeEntity.toTree(): SerializableTree { + return SerializableTree( + verboseName = verboseName, + typeOfTree = strategy.typeOfTree, + root = root?.toNode(), + ) + } + + private fun DatabaseNodeEntity.toNode(): SerializableNode { + return SerializableNode( + SerializableValue(value), + Metadata(metadata), + left?.toNode(), + right?.toNode() + ) + } + + + override fun save(verboseName: String, tree: BST): Unit = transaction(db) { + deleteByVerboseName(verboseName) + tree.toSerializableTree(verboseName).toEntity() + } + + + override fun loadByVerboseName(verboseName: String, factory: () -> BST): BST = + transaction(db) { + val entity = DatabaseTreeEntity.find( + TreesTable.typeOfTree eq strategy.typeOfTree.toString() and (TreesTable.verboseName eq verboseName) + ).firstOrNull() + + factory().apply { root = strategy.buildNode(entity?.root?.toNode()) } + } + + override fun deleteByVerboseName(verboseName: String): Unit = transaction(db) { + DatabaseTreeEntity.find( + TreesTable.typeOfTree eq strategy.typeOfTree.toString() and (TreesTable.verboseName eq verboseName) + ).firstOrNull()?.delete() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/repo/Repository.kt b/app/src/main/kotlin/app/model/repo/Repository.kt new file mode 100644 index 0000000..5fa24f7 --- /dev/null +++ b/app/src/main/kotlin/app/model/repo/Repository.kt @@ -0,0 +1,69 @@ +package app.model.repo + +import app.model.bst.BinarySearchTree +import app.model.bst.node.BinTreeNode +import app.model.repo.serialization.SerializableNode +import app.model.repo.serialization.SerializableTree +import app.model.repo.serialization.strategy.SerializationStrategy + +/** + * An abstract class representing a repository for binary search trees. + * @param E the type of element stored in the tree + * @param Node the type of node used in the tree + * @param BST the type of binary search tree stored in the repository + * @property strategy the serialization strategy used to serialize and deserialize the tree. + * @constructor creates a new Repository with the given serialization strategy. + */ +abstract class Repository, + Node : BinTreeNode, + BST : BinarySearchTree>( + protected val strategy: SerializationStrategy +) { + /** + * Converts a node to a serializable node using the serialization strategy. + * @param Node the node to convert. + * @return the serializable node. + */ + protected fun Node.toSerializableNode(): SerializableNode { + return SerializableNode( + strategy.serializeValue(this.value), + strategy.serializeMetadata(this), + left?.toSerializableNode(), + right?.toSerializableNode() + ) + } + + /** + * Converts a binary search tree to a serializable tree using the serialization strategy. + * @param verboseName the verbose name of the tree. + * @return the serializable tree. + */ + protected fun BST.toSerializableTree(verboseName: String): SerializableTree { + return SerializableTree( + verboseName = verboseName, + typeOfTree = strategy.typeOfTree, + root = this.root?.toSerializableNode() + ) + } + + /** + * Saves a binary search tree to the repository. + * @param verboseName the verbose name of the tree. + * @param tree the tree to save. + */ + abstract fun save(verboseName: String, tree: BST) + + /** + * Loads a binary search tree from the repository by its verbose name. + * @param verboseName the verbose name of the tree. + * @param factory a factory function to create a new instance of the tree. + * @return the loaded tree. + */ + abstract fun loadByVerboseName(verboseName: String, factory: () -> BST): BST + + /** + * Deletes a binary search tree from the repository by its verbose name. + * @param verboseName the verbose name of the tree to delete. + */ + abstract fun deleteByVerboseName(verboseName: String) +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/repo/serialization/Serializable.kt b/app/src/main/kotlin/app/model/repo/serialization/Serializable.kt new file mode 100644 index 0000000..902d026 --- /dev/null +++ b/app/src/main/kotlin/app/model/repo/serialization/Serializable.kt @@ -0,0 +1,63 @@ +package app.model.repo.serialization + +import kotlinx.serialization.Serializable + +/** + * An enum representing the type of tree being serialized or deserialized. + * + * @property savingName the name of the tree type used for serialization + */ +@Serializable +enum class TypeOfTree(val savingName: String) { + BINARY_SEARCH_TREE("BINARY_SEARCH_TREE"), + RED_BLACK_TREE("RED_BLACK_TREE"), + AVL_TREE("AVL_TREE") +} + +/** + * A serializable node in a tree data structure. + * + * @property value the value stored in the node + * @property metadata the metadata associated with the node + * @property left the left child of the node + * @property right the right child of the node + */ +@Serializable +class SerializableNode( + val value: SerializableValue, + val metadata: Metadata, + val left: SerializableNode? = null, + val right: SerializableNode? = null, +) + +/** + * A serializable tree data structure. + * + * @property verboseName the verbose name of the tree + * @property typeOfTree the type of tree being serialized or deserialized + * @property root the root node of the tree + */ +@Serializable +class SerializableTree( + val verboseName: String, + val typeOfTree: TypeOfTree, + val root: SerializableNode? +) + +/** + * A metadata value (actually a string) for a serializable node. + * + * @property value the value of the metadata + */ +@Serializable +@JvmInline +value class Metadata(val value: String) + +/** + * A serializable value (actually a string) stored in a tree node. + * + * @property value the value of the serializable value + */ +@Serializable +@JvmInline +value class SerializableValue(val value: String) \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/repo/serialization/strategy/AVLTreeStrategy.kt b/app/src/main/kotlin/app/model/repo/serialization/strategy/AVLTreeStrategy.kt new file mode 100644 index 0000000..11277c5 --- /dev/null +++ b/app/src/main/kotlin/app/model/repo/serialization/strategy/AVLTreeStrategy.kt @@ -0,0 +1,57 @@ +package app.model.repo.serialization.strategy + +import app.model.bst.node.AVLNode +import app.model.bst.node.AVLTreeNode +import app.model.repo.serialization.Metadata +import app.model.repo.serialization.SerializableNode +import app.model.repo.serialization.SerializableValue +import app.model.repo.serialization.TypeOfTree + +/** + * A serialization strategy for AVL tree data structures. + * + * @param the type of elements stored in the AVL tree + * @param serializeValue a function that serializes an element of type E to a SerializableValue + * @param deserializeValue a function that deserializes a SerializableValue to an element of type E + */ +class AVLTreeStrategy>( + serializeValue: (E) -> SerializableValue, + deserializeValue: (SerializableValue) -> E +) : SerializationStrategy, Int>(serializeValue, deserializeValue) { + + /** + * The type of tree that this strategy serializes and deserializes. + */ + override val typeOfTree: TypeOfTree = TypeOfTree.AVL_TREE + + /** + * Builds an AVL tree node from a serializable node. + * + * @param node the serializable node to build from + * @return the AVL tree node that was built + */ + override fun buildNode(node: SerializableNode?): AVLTreeNode? = node?.let { + AVLNode( + value = deserializeValue(node.value), + left = buildNode(node.left), + right = buildNode(node.right), + height = deserializeMetadata(node.metadata) + ) + } + + /** + * Deserializes the metadata for an AVL tree node. + * + * @param metadata the metadata to deserialize + * @return the height of the AVL tree node + */ + override fun deserializeMetadata(metadata: Metadata) = metadata.value.toInt() + + /** + * Serializes the metadata for an AVL tree node. + * + * @param node the AVL tree node to serialize metadata for + * @return a metadata object containing the height of the AVL tree node + */ + override fun serializeMetadata(node: AVLTreeNode) = Metadata(node.height.toString()) +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/repo/serialization/strategy/BSTreeStrategy.kt b/app/src/main/kotlin/app/model/repo/serialization/strategy/BSTreeStrategy.kt new file mode 100644 index 0000000..af928e4 --- /dev/null +++ b/app/src/main/kotlin/app/model/repo/serialization/strategy/BSTreeStrategy.kt @@ -0,0 +1,56 @@ +package app.model.repo.serialization.strategy + +import app.model.bst.node.BSNode +import app.model.bst.node.BinSearchTreeNode +import app.model.repo.serialization.Metadata +import app.model.repo.serialization.SerializableNode +import app.model.repo.serialization.SerializableValue +import app.model.repo.serialization.TypeOfTree + +/** + * A serialization strategy for binary search tree data structures. + * + * @param the type of elements stored in the binary search tree + * @param serializeValue a function that serializes an element of type E to a SerializableValue + * @param deserializeValue a function that deserializes a SerializableValue to an element of type E + */ +class BSTreeStrategy>( + serializeValue: (E) -> SerializableValue, + deserializeValue: (SerializableValue) -> E +) : SerializationStrategy, String>(serializeValue, deserializeValue) { + + /** + * The type of tree that this strategy serializes and deserializes. + */ + override val typeOfTree: TypeOfTree = TypeOfTree.BINARY_SEARCH_TREE + + /** + * Builds a binary search tree node from a serializable node. + * + * @param node the serializable node to build from + * @return the binary search tree node that was built + */ + override fun buildNode(node: SerializableNode?): BinSearchTreeNode? = node?.let { + BSNode( + value = deserializeValue(node.value), + left = buildNode(node.left), + right = buildNode(node.right) + ) + } + + /** + * Deserializes the metadata for a binary search tree node. + * + * @param metadata the metadata to deserialize + * @return an empty string, since binary search tree nodes do not have metadata + */ + override fun deserializeMetadata(metadata: Metadata) = "" + + /** + * Serializes the metadata for a binary search tree node. + * + * @param node the binary search tree node to serialize metadata for + * @return an empty metadata object, since binary search tree nodes do not have metadata + */ + override fun serializeMetadata(node: BinSearchTreeNode) = Metadata("") +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/repo/serialization/strategy/RBTreeStrategy.kt b/app/src/main/kotlin/app/model/repo/serialization/strategy/RBTreeStrategy.kt new file mode 100644 index 0000000..669269a --- /dev/null +++ b/app/src/main/kotlin/app/model/repo/serialization/strategy/RBTreeStrategy.kt @@ -0,0 +1,80 @@ +package app.model.repo.serialization.strategy + +import app.model.bst.node.RBNode +import app.model.bst.node.RedBlackTreeNode +import app.model.repo.serialization.Metadata +import app.model.repo.serialization.SerializableNode +import app.model.repo.serialization.SerializableValue +import app.model.repo.serialization.TypeOfTree + +/** + * A class representing a strategy for serializing and deserializing red-black tree data structures. + * + * @param E the type of element stored in the red-black tree + * @property serializeValue a function that serializes an element of type E into a [SerializableValue] + * @property deserializeValue a function that deserializes a [SerializableValue] into an element of type E + * @constructor creates a new [RBTreeStrategy] with the given serialization and deserialization functions + */ +class RBTreeStrategy>( + serializeValue: (E) -> SerializableValue, + deserializeValue: (SerializableValue) -> E +) : SerializationStrategy, RedBlackTreeNode.Color>(serializeValue, deserializeValue) { + + /** + * The type of tree being serialized or deserialized. + */ + override val typeOfTree: TypeOfTree = TypeOfTree.RED_BLACK_TREE + + /** + * Builds a [RedBlackTreeNode] from a [SerializableNode]. + * + * @param node the [SerializableNode] to build the [RedBlackTreeNode] from + * @return the [RedBlackTreeNode] built from the [SerializableNode], or null if the [SerializableNode] is null + */ + override fun buildNode(node: SerializableNode?): RedBlackTreeNode? { + fun buildNodeWithParent(node: SerializableNode?, parent: RedBlackTreeNode?): RedBlackTreeNode? { + node ?: return null + val rbNode = RBNode( + value = deserializeValue(node.value), + color = deserializeMetadata(node.metadata) + ) + rbNode.parent = parent + rbNode.left = buildNodeWithParent(node.left, rbNode) + rbNode.right = buildNodeWithParent(node.right, rbNode) + return rbNode + } + + return buildNodeWithParent(node, null) + } + + /** + * Deserializes metadata of type [RedBlackTreeNode.Color] from a [Metadata] object. + * + * @param metadata the [Metadata] object to deserialize the metadata from + * @return the deserialized metadata of type [RedBlackTreeNode.Color] + * @throws IllegalArgumentException if the metadata value is not "RED" or "BLACK" + */ + override fun deserializeMetadata(metadata: Metadata): RedBlackTreeNode.Color { + return when (metadata.value) { + "RED" -> RedBlackTreeNode.Color.RED + "BLACK" -> RedBlackTreeNode.Color.BLACK + else -> throw IllegalArgumentException("Can deserialize 'RED', 'BLACK' metadata value strings only") + } + } + + /** + * Serializes metadata of type [RedBlackTreeNode.Color] into a [Metadata] object. + * + * @param node the node to serialize the metadata from + * @return the serialized metadata as a [Metadata] object + */ + override fun serializeMetadata(node: RedBlackTreeNode): Metadata { + return Metadata( + when (node.color) { + RedBlackTreeNode.Color.RED -> "RED" + else -> "BLACK" + } + ) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/model/repo/serialization/strategy/SerializationStrategy.kt b/app/src/main/kotlin/app/model/repo/serialization/strategy/SerializationStrategy.kt new file mode 100644 index 0000000..7a57c66 --- /dev/null +++ b/app/src/main/kotlin/app/model/repo/serialization/strategy/SerializationStrategy.kt @@ -0,0 +1,48 @@ +package app.model.repo.serialization.strategy + +import app.model.bst.node.BinTreeNode +import app.model.repo.serialization.Metadata +import app.model.repo.serialization.SerializableNode +import app.model.repo.serialization.SerializableValue +import app.model.repo.serialization.TypeOfTree + +/** + * An abstract class representing a strategy for serializing and deserializing binary tree data structures. + * + * @param E the type of element stored in the binary tree + * @param Node the type of node used in the binary tree + * @param M the type of metadata associated with each node in the binary tree + * @property serializeValue a function that serializes an element of type E into a [SerializableValue] + * @property deserializeValue a function that deserializes a [SerializableValue] into an element of type E + * @property typeOfTree an abstract property representing the type of binary tree being serialized or deserialized + */ +abstract class SerializationStrategy, Node : BinTreeNode, M>( + val serializeValue: (E) -> SerializableValue, + val deserializeValue: (SerializableValue) -> E +) { + abstract val typeOfTree: TypeOfTree + + /** + * Builds a node of type [Node] from a [SerializableNode]. + * + * @param node the [SerializableNode] to build the [Node] from + * @return the [Node] built from the [SerializableNode], or null if the [SerializableNode] is null + */ + abstract fun buildNode(node: SerializableNode?): Node? + + /** + * Deserializes metadata of type [M] from a [Metadata] object. + * + * @param metadata the [Metadata] object to deserialize the metadata from + * @return the deserialized metadata of type [M] + */ + abstract fun deserializeMetadata(metadata: Metadata): M + + /** + * Serializes metadata of type [M] into a [Metadata] object. + * + * @param node the node to serialize the metadata from + * @return the serialized metadata as a [Metadata] object + */ + abstract fun serializeMetadata(node: Node): Metadata +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/Constants.kt b/app/src/main/kotlin/app/view/Constants.kt new file mode 100644 index 0000000..3374a81 --- /dev/null +++ b/app/src/main/kotlin/app/view/Constants.kt @@ -0,0 +1,5 @@ +package app.view + +import androidx.compose.ui.unit.dp + +val defaultNodeSize = 50.dp \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/DrawableNode.kt b/app/src/main/kotlin/app/view/DrawableNode.kt new file mode 100644 index 0000000..a3c11ca --- /dev/null +++ b/app/src/main/kotlin/app/view/DrawableNode.kt @@ -0,0 +1,23 @@ +package app.view + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + + +class DrawableNode( + val value: String, + x: Dp = 0.dp, + y: Dp = 0.dp, + left: DrawableNode? = null, + right: DrawableNode? = null, + val color: Color = Color.DarkGray +) { + var x by mutableStateOf(x) + var y by mutableStateOf(y) + var left by mutableStateOf(left) + var right by mutableStateOf(right) +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/DrawableTree.kt b/app/src/main/kotlin/app/view/DrawableTree.kt new file mode 100644 index 0000000..5a820d6 --- /dev/null +++ b/app/src/main/kotlin/app/view/DrawableTree.kt @@ -0,0 +1,11 @@ +package app.view + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue + +class DrawableTree( + root: DrawableNode? +) { + var root by mutableStateOf(root) +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/HiddenSettings.kt b/app/src/main/kotlin/app/view/HiddenSettings.kt new file mode 100644 index 0000000..a39c59a --- /dev/null +++ b/app/src/main/kotlin/app/view/HiddenSettings.kt @@ -0,0 +1,24 @@ +package app.view + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun HiddenSettings(text: String, hidden: Boolean = true, content: @Composable () -> Unit) { + var expanded by remember { mutableStateOf(!hidden) } + + Button(modifier = Modifier.fillMaxWidth(), onClick = { expanded = !expanded }) { + Text(text) + } + + Row(verticalAlignment = Alignment.CenterVertically) { + if (expanded) { + content() + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/InputField.kt b/app/src/main/kotlin/app/view/InputField.kt new file mode 100644 index 0000000..750e2ce --- /dev/null +++ b/app/src/main/kotlin/app/view/InputField.kt @@ -0,0 +1,36 @@ +package app.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun InputField(action: (String) -> Unit, icon: ImageVector) { + var text by remember { mutableStateOf("") } + + Row(Modifier.padding(5.dp)) { + OutlinedTextField( + value = text, + onValueChange = { it -> + text = it.slice(0..Integer.min(10, it.length - 1)) + }, + modifier = Modifier.width(150.dp).height(50.dp).padding(end = 5.dp), + maxLines = 1 + ) + IconButton(onClick = { action(text) }, modifier = Modifier.background( + color = MaterialTheme.colorScheme.tertiary, shape = RoundedCornerShape(8.dp) + ).size(50.dp), content = { + Icon( + imageVector = icon, contentDescription = null, tint = Color.White + ) + }) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/Transformation.kt b/app/src/main/kotlin/app/view/Transformation.kt new file mode 100644 index 0000000..6f2689e --- /dev/null +++ b/app/src/main/kotlin/app/view/Transformation.kt @@ -0,0 +1,22 @@ +package app.view + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.geometry.Offset + +class ScreenDrag( + x: Float, + y: Float +) { + var x by mutableStateOf(x) + var y by mutableStateOf(y) +} + +class ScreenScale( + scale: Float, + posRelScale: Offset +) { + var scale by mutableStateOf(scale) + var posRelXYScale by mutableStateOf(posRelScale) +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/TreeChoiceDialog.kt b/app/src/main/kotlin/app/view/TreeChoiceDialog.kt new file mode 100644 index 0000000..7ede8d5 --- /dev/null +++ b/app/src/main/kotlin/app/view/TreeChoiceDialog.kt @@ -0,0 +1,69 @@ +package app.view + +import androidx.compose.foundation.layout.* +import androidx.compose.material.Button +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import app.view.model.AVLTreeEditor +import app.view.model.BSTreeEditor +import app.view.model.RBTreeEditor +import app.view.model.TreeEditor + + +@Composable +fun TreeChoiceDialog() { + var showDialog by remember { mutableStateOf(true) } + var chosenTreeEditor: TreeEditor<*, *> by remember { mutableStateOf(BSTreeEditor()) } + if (showDialog) { + Dialog( + onCloseRequest = { showDialog = false }, + title = "Choose tree" + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "Choose tree") + Spacer(modifier = Modifier.height(16.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + Button( + modifier = Modifier.padding(horizontal = 8.dp), + onClick = { + showDialog = false + chosenTreeEditor = BSTreeEditor() + } + ) { + Text(text = "BSTree") + } + Button( + modifier = Modifier.padding(horizontal = 8.dp), + onClick = { + showDialog = false + chosenTreeEditor = AVLTreeEditor() + } + ) { + Text(text = "AVLTree") + } + Button( + modifier = Modifier.padding(horizontal = 8.dp), + onClick = { + showDialog = false + chosenTreeEditor = RBTreeEditor() + } + ) { + Text(text = "RBTree") + } + } + } + } + } + + TreeEditorView(chosenTreeEditor) +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/TreeEditorView.kt b/app/src/main/kotlin/app/view/TreeEditorView.kt new file mode 100644 index 0000000..cea3d05 --- /dev/null +++ b/app/src/main/kotlin/app/view/TreeEditorView.kt @@ -0,0 +1,45 @@ +package app.view + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import app.view.graph.Graph +import app.view.graph.GraphControls +import app.view.model.TreeEditor + + +@Composable +fun TreeEditorView(editor: TreeEditor<*, *>) { + val drawableTree by remember { + mutableStateOf( + DrawableTree( + null + ) + ) + } + + val screenDrag = remember { ScreenDrag(0f, 0f) } + val screenScale = remember { ScreenScale(1f, Offset(0f, 0f)) } + + Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surface)) { + Graph(drawableTree, screenDrag, screenScale) + GraphControls( + editor, drawableTree, Modifier.padding(10.dp).align(Alignment.TopEnd).clip( + RoundedCornerShape(10.dp) + ).background(Color.LightGray), screenDrag, screenScale + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/graph/Graph.kt b/app/src/main/kotlin/app/view/graph/Graph.kt new file mode 100644 index 0000000..c413a13 --- /dev/null +++ b/app/src/main/kotlin/app/view/graph/Graph.kt @@ -0,0 +1,53 @@ +package app.view.graph + +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerEventType +import androidx.compose.ui.input.pointer.onPointerEvent +import androidx.compose.ui.input.pointer.pointerInput +import app.view.DrawableTree +import app.view.ScreenDrag +import app.view.ScreenScale +import kotlin.math.max +import kotlin.math.min + + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun Graph(root: DrawableTree?, screenDrag: ScreenDrag, screenScale: ScreenScale) { + + + Box(modifier = Modifier.fillMaxSize().onPointerEvent(PointerEventType.Scroll) { + screenDrag.x -= it.changes.first().scrollDelta.x * 50 // let's scroll faster brbrbr + + val scrollPos = it.changes.first().position + val prevScale = screenScale.scale + screenScale.scale = min( + max( + screenScale.scale - it.changes.first().scrollDelta.y / 15, // let's zoom slower + 0.5f // min scale + ), 2f // max scale + ) + + val relScale = screenScale.scale / prevScale + screenScale.posRelXYScale = Offset( + screenScale.posRelXYScale.x * relScale + scrollPos.x * (1 - relScale), + screenScale.posRelXYScale.y * relScale + scrollPos.y * (1 - relScale) + ) + }.pointerInput(Unit) { + detectDragGestures { change, dragAmount -> + change.consume() + screenDrag.x += dragAmount.x / screenScale.scale + screenDrag.y += dragAmount.y / screenScale.scale + } + }) { + Tree( + rootNode = root?.root, screenDrag = screenDrag, screenScale = screenScale + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/graph/GraphControls.kt b/app/src/main/kotlin/app/view/graph/GraphControls.kt new file mode 100644 index 0000000..7483717 --- /dev/null +++ b/app/src/main/kotlin/app/view/graph/GraphControls.kt @@ -0,0 +1,113 @@ +package app.view.graph + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import app.model.bst.BinarySearchTree +import app.model.bst.node.BinTreeNode +import app.view.* +import app.view.model.TreeEditor +import app.view.model.exportTreeToJson +import app.view.model.importTreeFromJson + + +@Composable +fun , BST : BinarySearchTree> GraphControls( + editor: TreeEditor, + tree: DrawableTree?, + modifier: Modifier, + screenDrag: ScreenDrag, + screenScale: ScreenScale, +) { + + val currentDensity = LocalDensity.current + Box(modifier) { + Column(Modifier.padding(10.dp).width(210.dp).verticalScroll(rememberScrollState())) { + HiddenSettings( + "Tree operations", hidden = false + ) { + Column { + InputField({ tree?.root = editor.addToTree(it) }, Icons.Default.Add) + InputField({ tree?.root = editor.removeFromTree(it) }, Icons.Default.Remove) + InputField({ + tree?.root = editor.findNodeInTree(it) + editor.resetCoordinates(tree?.root) + }, Icons.Default.Search) + Row { + Button( + { + currentDensity.run { + screenScale.scale = 1f + screenDrag.x = 0f + screenDrag.y = 0f + } + + }, + Modifier.weight(1f).fillMaxWidth(), + colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.tertiary) + ) { + Text("Reset view") + } + Button( + { + tree?.root = editor.toDrawableNode(editor.tree.root) + editor.resetCoordinates(tree?.root) + }, + Modifier.weight(1f).fillMaxWidth(), + colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.tertiary) + ) { + Text("Reset tree") + } + } + } + } + + HiddenSettings( + "Save tree to...", hidden = true + ) { + Column { + Button( + { + exportTreeToJson(editor.tree, editor.typeOfTree) + }, + Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.tertiary) + ) { + Text("Json") + } + } + } + + HiddenSettings( + "Load tree from...", hidden = true + ) { + Column { + Button( + { + val t = importTreeFromJson(editor.typeOfTree) ?: return@Button + editor.tree = t + tree?.root = editor.toDrawableNode(editor.tree.root) + editor.resetCoordinates(tree?.root) + }, + Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors(MaterialTheme.colorScheme.tertiary) + ) { + Text("Json") + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/graph/GraphLine.kt b/app/src/main/kotlin/app/view/graph/GraphLine.kt new file mode 100644 index 0000000..1b9e177 --- /dev/null +++ b/app/src/main/kotlin/app/view/graph/GraphLine.kt @@ -0,0 +1,36 @@ +package app.view.graph + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import app.view.DrawableNode +import app.view.ScreenDrag +import app.view.ScreenScale +import app.view.defaultNodeSize + +@Composable +fun GraphLine( + modifier: Modifier = Modifier, + start: DrawableNode, + end: DrawableNode, + screenDrag: ScreenDrag, + screenScale: ScreenScale +) { + Canvas(modifier = modifier.fillMaxSize()) { + drawLine( + start = Offset( + ((start.x + defaultNodeSize / 2).toPx() + screenDrag.x) * screenScale.scale + screenScale.posRelXYScale.x, + ((start.y + defaultNodeSize / 2).toPx() + screenDrag.y) * screenScale.scale + screenScale.posRelXYScale.y, + ), + end = Offset( + ((end.x + defaultNodeSize / 2).toPx() + screenDrag.x) * screenScale.scale + screenScale.posRelXYScale.x, + ((end.y + defaultNodeSize / 2).toPx() + screenDrag.y) * screenScale.scale + screenScale.posRelXYScale.y, + ), + strokeWidth = 2f * screenScale.scale, + color = Color.Black + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/graph/GraphNode.kt b/app/src/main/kotlin/app/view/graph/GraphNode.kt new file mode 100644 index 0000000..a47689c --- /dev/null +++ b/app/src/main/kotlin/app/view/graph/GraphNode.kt @@ -0,0 +1,64 @@ +package app.view.graph + +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.layout +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.zIndex +import app.view.DrawableNode +import app.view.ScreenDrag +import app.view.ScreenScale +import app.view.defaultNodeSize +import kotlin.math.roundToInt + +@Composable +fun GraphNode( + modifier: Modifier = Modifier, + node: DrawableNode, + screenDrag: ScreenDrag, + screenScale: ScreenScale +) { + Box(modifier = modifier + .zIndex(2f) + .layout { measurable: Measurable, _: Constraints -> + val placeable = measurable.measure( + Constraints.fixed( + (defaultNodeSize * screenScale.scale).roundToPx(), + (defaultNodeSize * screenScale.scale).roundToPx() + ) + ) + + layout(placeable.width, placeable.height) { + placeable.placeRelative( + ((node.x.toPx() + screenDrag.x) * screenScale.scale + screenScale.posRelXYScale.x).roundToInt(), + ((node.y.toPx() + screenDrag.y) * screenScale.scale + screenScale.posRelXYScale.y).roundToInt(), + ) + } + } + .background( + color = node.color, + shape = CircleShape + ) + + .pointerInput(node, screenScale) { + detectDragGestures { change, dragAmount -> + change.consume() + node.x += (dragAmount.x / screenScale.scale).toDp() + node.y += (dragAmount.y / screenScale.scale).toDp() + } + } + ) { + GraphNodeText( + modifier = Modifier.align(Alignment.Center), + text = node.value, + scaleProvider = { screenScale.scale } + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/graph/GraphNodeText.kt b/app/src/main/kotlin/app/view/graph/GraphNodeText.kt new file mode 100644 index 0000000..58db307 --- /dev/null +++ b/app/src/main/kotlin/app/view/graph/GraphNodeText.kt @@ -0,0 +1,25 @@ +package app.view.graph + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle + +@Composable +fun GraphNodeText( + modifier: Modifier = Modifier, + text: String, + scaleProvider: () -> Float, + style: TextStyle = MaterialTheme.typography.bodyMedium, +) { + Text( + modifier = modifier, + text = if (text.length > 4) text.substring(0, 4) + ".." else text, + color = MaterialTheme.colorScheme.onPrimary, + style = style.copy( + fontSize = style.fontSize * scaleProvider(), + lineHeight = style.lineHeight * scaleProvider() + ) + ) +} diff --git a/app/src/main/kotlin/app/view/graph/GraphTree.kt b/app/src/main/kotlin/app/view/graph/GraphTree.kt new file mode 100644 index 0000000..0dfbc97 --- /dev/null +++ b/app/src/main/kotlin/app/view/graph/GraphTree.kt @@ -0,0 +1,31 @@ +package app.view.graph + +import androidx.compose.runtime.Composable +import app.view.DrawableNode +import app.view.ScreenDrag +import app.view.ScreenScale + + +@Composable +fun Tree( + rootNode: DrawableNode?, + screenDrag: ScreenDrag, + screenScale: ScreenScale, + parent: DrawableNode? = null +) { + rootNode?.let { + parent?.let { parent -> + GraphLine( + start = parent, + end = it, + screenDrag = screenDrag, + screenScale = screenScale + ) + } + + Tree(rootNode.left, screenDrag, screenScale, it) + Tree(rootNode.right, screenDrag, screenScale, it) + + GraphNode(node = rootNode, screenDrag = screenDrag, screenScale = screenScale) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/model/FileIOHandler.kt b/app/src/main/kotlin/app/view/model/FileIOHandler.kt new file mode 100644 index 0000000..d573171 --- /dev/null +++ b/app/src/main/kotlin/app/view/model/FileIOHandler.kt @@ -0,0 +1,115 @@ +package app.view.model + +import androidx.compose.ui.awt.ComposeWindow +import app.model.bst.AVLTree +import app.model.bst.BSTree +import app.model.bst.BinarySearchTree +import app.model.bst.RBTree +import app.model.bst.node.AVLTreeNode +import app.model.bst.node.BinSearchTreeNode +import app.model.bst.node.BinTreeNode +import app.model.bst.node.RedBlackTreeNode +import app.model.repo.JsonRepo +import app.model.repo.serialization.SerializableTree +import app.model.repo.serialization.SerializableValue +import app.model.repo.serialization.TypeOfTree +import app.model.repo.serialization.strategy.AVLTreeStrategy +import app.model.repo.serialization.strategy.BSTreeStrategy +import app.model.repo.serialization.strategy.RBTreeStrategy +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import java.awt.FileDialog +import java.io.File + +@Suppress("UNCHECKED_CAST") +fun , BST : BinarySearchTree> exportTreeToJson( + tree: BST, + typeOfTree: TypeOfTree, +) { + val file = selectJsonFile(Mode.EXPORT) ?: return + when (typeOfTree) { + TypeOfTree.BINARY_SEARCH_TREE -> { + val strategy = BSTreeStrategy({ SerializableValue(it) }, { it.value }) + JsonRepo( + strategy, + file + ).save( + file.name, tree as + BinarySearchTree> + ) + } + + TypeOfTree.RED_BLACK_TREE -> { + val strategy = RBTreeStrategy({ SerializableValue(it) }, { it.value }) + JsonRepo( + strategy, + file + ).save( + file.name, tree as + BinarySearchTree> + ) + } + + TypeOfTree.AVL_TREE -> { + val strategy = AVLTreeStrategy({ SerializableValue(it) }, { it.value }) + JsonRepo( + strategy, + file + ).save( + file.name, tree as + BinarySearchTree> + ) + } + } +} + +enum class Mode { + IMPORT, + EXPORT +} + +@Suppress("UNCHECKED_CAST") +fun , BST : BinarySearchTree> importTreeFromJson(currentEditorType: TypeOfTree): BST? { + try { + val file = selectJsonFile(Mode.IMPORT) ?: return null + val serializableTree = Json.decodeFromString(file.readText()) + if (currentEditorType != serializableTree.typeOfTree) return null //can load only same types of trees + return when (serializableTree.typeOfTree) { + TypeOfTree.BINARY_SEARCH_TREE -> { + val factory = { BSTree() } + val strategy = BSTreeStrategy({ SerializableValue(it) }, { it.value }) + JsonRepo(strategy, file).loadByVerboseName(file.name, factory) as BST + } + + TypeOfTree.RED_BLACK_TREE -> { + val factory = { RBTree() } + val strategy = RBTreeStrategy({ SerializableValue(it) }, { it.value }) + JsonRepo(strategy, file).loadByVerboseName(file.name, factory) as BST + } + + TypeOfTree.AVL_TREE -> { + val factory = { AVLTree() } + val strategy = AVLTreeStrategy({ SerializableValue(it) }, { it.value }) + JsonRepo(strategy, file).loadByVerboseName(file.name, factory) as BST + } + } + } catch (e: Exception){ + return null + } +} + + +fun selectJsonFile(mode: Mode): File? { + val fd = if (mode == Mode.IMPORT) { + FileDialog(ComposeWindow(), "Choose file to import", FileDialog.LOAD) + } else { + FileDialog(ComposeWindow(), "Choose file to export", FileDialog.SAVE) + } + fd.file = "tree.json" + fd.isVisible = true + val fileString = fd.directory + fd.file + if (fileString != "nullnull") { + return File(fileString) + } + return null +} \ No newline at end of file diff --git a/app/src/main/kotlin/app/view/model/TreeEditor.kt b/app/src/main/kotlin/app/view/model/TreeEditor.kt new file mode 100644 index 0000000..ee70ef0 --- /dev/null +++ b/app/src/main/kotlin/app/view/model/TreeEditor.kt @@ -0,0 +1,140 @@ +package app.view.model + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import app.model.bst.AVLTree +import app.model.bst.BSTree +import app.model.bst.BinarySearchTree +import app.model.bst.RBTree +import app.model.bst.node.AVLTreeNode +import app.model.bst.node.BinSearchTreeNode +import app.model.bst.node.BinTreeNode +import app.model.bst.node.RedBlackTreeNode +import app.model.repo.serialization.TypeOfTree +import app.view.DrawableNode +import app.view.defaultNodeSize + + +abstract class TreeEditor, BST : BinarySearchTree> { + abstract var tree: BST + abstract val typeOfTree: TypeOfTree + fun resetCoordinates(node: DrawableNode?) { + node?.let { + calcLeft(node, 0.dp, 0.dp) + calcRight(node, 0.dp, 0.dp) + } + } + + private fun countWidth(node: DrawableNode?): Pair { + var leftWidth = 0 + var rightWidth = 0 + if (node?.left != null) { + leftWidth = countWidth(node.left).toList().sum() + 1 + } + if (node?.right != null) { + rightWidth = countWidth(node.right).toList().sum() + 1 + } + return Pair(leftWidth, rightWidth) + } + + private fun calcLeft(node: DrawableNode?, parentX: Dp, parentY: Dp) { + var count = 0 + if (node?.right != null) { + count = countWidth(node).second + } + val x = parentX - defaultNodeSize - (defaultNodeSize * count) + val y = parentY + defaultNodeSize + node?.x = x + node?.y = y + if (node?.left != null) calcLeft(node.left, x, y) + if (node?.right != null) calcRight(node.right, x, y) + } + + + private fun calcRight(node: DrawableNode?, parentX: Dp, parentY: Dp) { + var count = 0 + if (node?.left != null) { + count = countWidth(node).first + } + val x = parentX + defaultNodeSize + (defaultNodeSize * count) + val y = parentY + defaultNodeSize + node?.x = x + node?.y = y + if (node?.left != null) calcLeft(node.left, x, y) + if (node?.right != null) calcRight(node.right, x, y) + } + + fun addToTree(value: String): DrawableNode? { + this.tree.add(value) + return toDrawableNode(this.tree.root).also { resetCoordinates(it) } + } + + fun removeFromTree(value: String): DrawableNode? { + this.tree.remove(value) + return toDrawableNode(this.tree.root).also { resetCoordinates(it) } + } + + fun findNodeInTree(value: String): DrawableNode? { + fun findNode(node: N?): N? { + node ?: return null + if (value == node.value) { + return node + } + return findNode(if (value < node.value) node.left else node.right) + } + return toDrawableNode(findNode(tree.root)) + } + + abstract fun toDrawableNode(node: N?): DrawableNode? +} + +class BSTreeEditor : TreeEditor, BinarySearchTree>>() { + override val typeOfTree = TypeOfTree.BINARY_SEARCH_TREE + override fun toDrawableNode(node: BinSearchTreeNode?): DrawableNode? { + return node?.let { + DrawableNode( + node.value, + left = toDrawableNode(node.left), + right = toDrawableNode(node.right), + ) + } + } + + override var tree: BinarySearchTree> = BSTree() +} + + +class RBTreeEditor : TreeEditor, BinarySearchTree>>() { + override val typeOfTree = TypeOfTree.RED_BLACK_TREE + + override fun toDrawableNode(node: RedBlackTreeNode?): DrawableNode? { + return node?.let { + DrawableNode( + node.value, + left = toDrawableNode(node.left), + right = toDrawableNode(node.right), + color = if (it.color == RedBlackTreeNode.Color.RED) Color.Red else Color.DarkGray + ) + } + } + + override var tree: BinarySearchTree> = RBTree() +} + + +class AVLTreeEditor : TreeEditor, BinarySearchTree>>() { + override val typeOfTree = TypeOfTree.AVL_TREE + + override fun toDrawableNode(node: AVLTreeNode?): DrawableNode? { + return node?.let { + DrawableNode( + node.value, + left = toDrawableNode(node.left), + right = toDrawableNode(node.right), + ) + } + } + + override var tree: BinarySearchTree> = AVLTree() +} diff --git a/app/src/main/resources/add.png b/app/src/main/resources/add.png new file mode 100644 index 0000000..ac11b6e Binary files /dev/null and b/app/src/main/resources/add.png differ diff --git a/app/src/main/resources/icon.icns b/app/src/main/resources/icon.icns new file mode 100644 index 0000000..388deea Binary files /dev/null and b/app/src/main/resources/icon.icns differ diff --git a/app/src/main/resources/icon.ico b/app/src/main/resources/icon.ico new file mode 100644 index 0000000..18fcecc Binary files /dev/null and b/app/src/main/resources/icon.ico differ diff --git a/app/src/main/resources/icon.png b/app/src/main/resources/icon.png new file mode 100644 index 0000000..50b5d53 Binary files /dev/null and b/app/src/main/resources/icon.png differ diff --git a/app/src/main/resources/loadTree.png b/app/src/main/resources/loadTree.png new file mode 100644 index 0000000..e836f1b Binary files /dev/null and b/app/src/main/resources/loadTree.png differ diff --git a/app/src/main/resources/remove.png b/app/src/main/resources/remove.png new file mode 100644 index 0000000..6baf65c Binary files /dev/null and b/app/src/main/resources/remove.png differ diff --git a/app/src/main/resources/resetView.png b/app/src/main/resources/resetView.png new file mode 100644 index 0000000..0fd79ca Binary files /dev/null and b/app/src/main/resources/resetView.png differ diff --git a/app/src/main/resources/saveTree.png b/app/src/main/resources/saveTree.png new file mode 100644 index 0000000..2e32cd8 Binary files /dev/null and b/app/src/main/resources/saveTree.png differ diff --git a/app/src/main/resources/search.png b/app/src/main/resources/search.png new file mode 100644 index 0000000..7fb3a74 Binary files /dev/null and b/app/src/main/resources/search.png differ diff --git a/app/src/main/resources/tree.png b/app/src/main/resources/tree.png new file mode 100644 index 0000000..1355fc0 Binary files /dev/null and b/app/src/main/resources/tree.png differ diff --git a/app/src/main/resources/view.png b/app/src/main/resources/view.png new file mode 100644 index 0000000..82abcdd Binary files /dev/null and b/app/src/main/resources/view.png differ diff --git a/app/src/test/kotlin/app/model/bst/AVLTreeTest.kt b/app/src/test/kotlin/app/model/bst/AVLTreeTest.kt new file mode 100644 index 0000000..872094e --- /dev/null +++ b/app/src/test/kotlin/app/model/bst/AVLTreeTest.kt @@ -0,0 +1,74 @@ +package app.model.bst + +import app.model.bst.utils.InvariantChecker +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import kotlin.random.Random +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class AVLTreeTest { + companion object { + const val defaultSeed = 42 + } + + private lateinit var tree: AVLTree + private lateinit var values: Array + private val randomizer = Random(defaultSeed) + + @BeforeTest + fun init() { + values = Array(1000) { randomizer.nextInt(10) } + tree = AVLTree() + } + + @ParameterizedTest(name = "Are unique elements added: {0}") + @ValueSource(booleans = [true, false]) + fun `test values addition invariant`(unique: Boolean) { + for (v in values) { + tree.add(v, unique) + assertTrue(InvariantChecker.isBinarySearchTree(tree)) + assertTrue(InvariantChecker.checkNeighborHeights(tree)) + } + } + + @Test + fun `test all added values are unique`() { + for (v in values) { + tree.add(v, unique = true) + } + assertEquals(values.sorted().distinct(), tree.iterator().asSequence().toList()) + // Don't use toSet in `actual` part of assertEquals because of false-positive effect + } + + @ParameterizedTest(name = "Are unique elements added: {0}") + @ValueSource(booleans = [true, false]) + fun `test values deletion invariant`(unique: Boolean) { + values.forEach { tree.add(it, unique) } + values.shuffle() + for (v in values) { + tree.remove(v) + assertTrue(InvariantChecker.isBinarySearchTree(tree)) + assertTrue(InvariantChecker.checkNeighborHeights(tree)) + } + + assertEquals(null, tree.root) // Tree is empty + } + + @Test + fun `test values are being removed gradually`() { + val uniqueValues = values.toSet() + var countOfUniqueValues = uniqueValues.size + values.forEach { tree.add(it, unique = true) } + + for (v in uniqueValues) { + assertEquals(countOfUniqueValues, tree.iterator().asSequence().toList().size) + tree.remove(v) + countOfUniqueValues-- + } + + assertEquals(0, tree.iterator().asSequence().toList().size) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/app/model/bst/BSTreeTest.kt b/app/src/test/kotlin/app/model/bst/BSTreeTest.kt new file mode 100644 index 0000000..e246077 --- /dev/null +++ b/app/src/test/kotlin/app/model/bst/BSTreeTest.kt @@ -0,0 +1,72 @@ +package app.model.bst + +import app.model.bst.utils.InvariantChecker +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import kotlin.random.Random +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertTrue + +class BSTreeTest { + companion object { + const val defaultSeed = 42 + } + + private lateinit var tree: BSTree + private lateinit var values: Array + private val randomizer = Random(defaultSeed) + + @BeforeTest + fun init() { + values = Array(1000) { randomizer.nextInt(10) } + tree = BSTree() + } + + @ParameterizedTest(name = "Are unique elements added: {0}") + @ValueSource(booleans = [true, false]) + fun `test values addition invariant`(unique: Boolean) { + for (v in values) { + tree.add(v, unique) + assertTrue(InvariantChecker.isBinarySearchTree(tree)) + } + } + + @Test + fun `test all added values are unique`() { + for (v in values) { + tree.add(v, unique = true) + } + assertEquals(values.sorted().distinct(), tree.iterator().asSequence().toList()) + // Don't use toSet in `actual` part of assertEquals because of false-positive effect + } + + @ParameterizedTest(name = "Are unique elements added: {0}") + @ValueSource(booleans = [true, false]) + fun `test values deletion invariant`(unique: Boolean) { + values.forEach { tree.add(it, unique) } + values.shuffle() + for (v in values) { + tree.remove(v) + assertTrue(InvariantChecker.isBinarySearchTree(tree)) + } + + assertEquals(null, tree.root) // Tree is empty + } + + @Test + fun `test values are being removed gradually`() { + val uniqueValues = values.toSet() + var countOfUniqueValues = uniqueValues.size + values.forEach { tree.add(it, unique = true) } + + for (v in uniqueValues) { + assertEquals(countOfUniqueValues, tree.iterator().asSequence().toList().size) + tree.remove(v) + countOfUniqueValues-- + } + + assertEquals(0, tree.iterator().asSequence().toList().size) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/app/model/bst/RBTreeTest.kt b/app/src/test/kotlin/app/model/bst/RBTreeTest.kt new file mode 100644 index 0000000..34e89cf --- /dev/null +++ b/app/src/test/kotlin/app/model/bst/RBTreeTest.kt @@ -0,0 +1,77 @@ +package app.model.bst + +import app.model.bst.utils.InvariantChecker +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import kotlin.random.Random +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class RBTreeTest { + companion object { + const val defaultSeed = 42 + } + + private lateinit var tree: RBTree + private lateinit var values: Array + private val randomizer = Random(defaultSeed) + + @BeforeTest + fun init() { + values = Array(1000) { randomizer.nextInt(10) } + tree = RBTree() + } + + @ParameterizedTest(name = "Are unique elements added: {0}") + @ValueSource(booleans = [true, false]) + fun `test values addition invariant`(unique: Boolean) { + for (v in values) { + tree.add(v, unique) + assertTrue(InvariantChecker.isBinarySearchTree(tree)) + assertTrue(InvariantChecker.isBlackHeightBalanced(tree)) + assertTrue(InvariantChecker.isParentLinkedRight(tree)) + } + } + + @Test + fun `test all added values are unique`() { + for (v in values) { + tree.add(v, unique = true) + } + Assertions.assertEquals(values.sorted().distinct(), tree.iterator().asSequence().toList()) + // Don't use toSet in `actual` part of assertEquals because of false-positive effect + } + + @ParameterizedTest(name = "Are unique elements added: {0}") + @ValueSource(booleans = [true, false]) + fun `test values deletion invariant`(unique: Boolean) { + values.forEach { tree.add(it, unique) } + values.shuffle() + for (v in values) { + tree.remove(v) + assert(InvariantChecker.isBinarySearchTree(tree)) + assert(InvariantChecker.isBlackHeightBalanced(tree)) + assert(InvariantChecker.isParentLinkedRight(tree)) + } + + Assertions.assertEquals(null, tree.root) // Tree is empty + } + + @Test + fun `test values are being removed gradually`() { + val uniqueValues = values.toSet() + var countOfUniqueValues = uniqueValues.size + values.forEach { tree.add(it, unique = true) } + + for (v in uniqueValues) { + assertEquals(countOfUniqueValues, tree.iterator().asSequence().toList().size) + tree.remove(v) + countOfUniqueValues-- + } + + Assertions.assertEquals(0, tree.iterator().asSequence().toList().size) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/app/model/bst/balancer/AVLBalancerTest.kt b/app/src/test/kotlin/app/model/bst/balancer/AVLBalancerTest.kt new file mode 100644 index 0000000..4471e50 --- /dev/null +++ b/app/src/test/kotlin/app/model/bst/balancer/AVLBalancerTest.kt @@ -0,0 +1,243 @@ +package app.model.bst.balancer + +import app.model.bst.node.AVLTreeNode +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class AVLBalancerTest { + private val balancer = AVLBalancer() + + @Test + fun `test left turn when 'add'`() { + var root = balancer.add(null, 8, true) + root = balancer.add(root, 7, true) + + /* + constructed tree: + 8 + / + 7 + */ + + assertEquals(8, root.value) + assertEquals(7, root.left?.value) + + root = balancer.add(root, 6, true) + + /* + constructed tree: + 7 + / \ + 6 8 + */ + + assertEquals(7, root.value) + assertEquals(6, root.left?.value) + assertEquals(8, root.right?.value) + + root = balancer.add(root, 5, true) + root = balancer.add(root, 4, true) + + /* + constructed tree: + 7 + / \ + 5 8 + / \ + 4 6 + */ + + assertEquals(7, root.value) + assertEquals(5, root.left?.value) + assertEquals(8, root.right?.value) + assertEquals(4, root.left?.left?.value) + assertEquals(6, root.left?.right?.value) + + root = balancer.add(root, 3, true) + + /* + constructed tree: + 5 + / \ + 4 7 + / / \ + 3 6 8 + */ + + assertEquals(5, root.value) + assertEquals(4, root.left?.value) + assertEquals(7, root.right?.value) + assertEquals(3, root.left?.left?.value) + assertEquals(6, root.right?.left?.value) + assertEquals(8, root.right?.right?.value) + + root = balancer.add(root, 2, true) + + /* + constructed tree: + 5 + / \ + 3 7 + / \ / \ + 2 4 6 8 + */ + + assertEquals(3, root.left?.value) + assertEquals(2, root.left?.left?.value) + assertEquals(4, root.left?.right?.value) + + root = balancer.add(root, 1, true) + root = balancer.add(root, 0, true) + + /* + constructed tree: + 5 + / \ + 3 7 + / \ / \ + 1 4 6 8 + / \ + 0 2 + */ + + assertEquals(1, root.left?.left?.value) + assertEquals(0, root.left?.left?.left?.value) + assertEquals(2, root.left?.left?.right?.value) + } + + @Test + fun `test right turn when 'add'`() { + var root = balancer.add(null, 2, true) + root = balancer.add(root, 3, true) + + /* + constructed tree: + 2 + \ + 3 + */ + + assertEquals(2, root.value) + assertEquals(3, root.right?.value) + + root = balancer.add(root, 4, true) + + /* + constructed tree: + 3 + / \ + 2 4 + */ + + assertEquals(3, root.value) + assertEquals(2, root.left?.value) + assertEquals(4, root.right?.value) + + root = balancer.add(root, 5, true) + root = balancer.add(root, 6, true) + + /* + constructed tree: + 3 + / \ + 2 5 + / \ + 4 6 + */ + + assertEquals(5, root.right?.value) + assertEquals(4, root.right?.left?.value) + assertEquals(6, root.right?.right?.value) + + root = balancer.add(root, 7, true) + + /* + constructed tree: + 5 + / \ + 3 6 + / \ \ + 2 4 7 + */ + + assertEquals(5, root.value) + assertEquals(3, root.left?.value) + assertEquals(2, root.left?.left?.value) + assertEquals(4, root.left?.right?.value) + assertEquals(6, root.right?.value) + assertEquals(7, root.right?.right?.value) + + root = balancer.add(root, 8, true) + + /* + constructed tree: + 5 + / \ + 3 7 + / \ / \ + 2 4 6 8 + */ + + assertEquals(7, root.right?.value) + assertEquals(8, root.right?.right?.value) + assertEquals(6, root.right?.left?.value) + } + + @Test + fun `test method 'remove' in avl tree balancer`() { + var root: AVLTreeNode? = balancer.add(null, 2, true) + root = balancer.add(root, 3, true) + root = balancer.add(root, 4, true) + root = balancer.add(root, 5, true) + root = balancer.add(root, 6, true) + root = balancer.add(root, 7, true) + root = balancer.add(root, 8, true) + + /* + constructed tree: + 5 + / \ + 3 7 + / \ / \ + 2 4 6 8 + */ + + root = balancer.remove(root, 5) + + /* + constructed tree: + 6 + / \ + 3 7 + / \ \ + 2 4 8 + */ + + assertEquals(6, root?.value) + assertEquals(3, root?.left?.value) + assertEquals(2, root?.left?.left?.value) + assertEquals(4, root?.left?.right?.value) + assertEquals(7, root?.right?.value) + assertNull(root?.right?.left?.value) + assertEquals(8, root?.right?.right?.value) + + root = balancer.remove(root, 3) + + /* + constructed tree: + 6 + / \ + 4 7 + / \ + 2 8 + */ + + assertEquals(6, root?.value) + assertEquals(4, root?.left?.value) + assertEquals(2, root?.left?.left?.value) + assertNull(root?.left?.right?.value) + assertEquals(7, root?.right?.value) + assertEquals(8, root?.right?.right?.value) + } +} diff --git a/app/src/test/kotlin/app/model/bst/balancer/BSTBalancerTest.kt b/app/src/test/kotlin/app/model/bst/balancer/BSTBalancerTest.kt new file mode 100644 index 0000000..a42e0fd --- /dev/null +++ b/app/src/test/kotlin/app/model/bst/balancer/BSTBalancerTest.kt @@ -0,0 +1,81 @@ +package app.model.bst.balancer + +import app.model.bst.node.BinSearchTreeNode +import kotlin.test.Test +import kotlin.test.assertEquals + +class BSTBalancerTest { + private val balancer = BSBalancer() + + @Test + fun `test method 'add' in bs balancer`() { + var root = balancer.add(null, 5, true) + + root = balancer.add(root, 1, true) + root = balancer.add(root, 7, true) + root = balancer.add(root, 4, true) + root = balancer.add(root, 6, true) + root = balancer.add(root, 3, true) + + /* + constructed tree: + 5 + / \ + 1 7 + \ / + 4 6 + / + 3 + */ + + assertEquals(5, root.value) + assertEquals(1, root.left?.value) + assertEquals(4, root.left?.right?.value) + assertEquals(3, root.left?.right?.left?.value) + assertEquals(7, root.right?.value) + assertEquals(6, root.right?.left?.value) + } + + @Test + fun `test method 'remove' in bs balancer`() { + var root: BinSearchTreeNode? = balancer.add(null, 5, true) + + root = balancer.add(root, 1, true) + root = balancer.add(root, 7, true) + root = balancer.add(root, 4, true) + root = balancer.add(root, 6, true) + root = balancer.add(root, 3, true) + + + /* + constructed tree: + 5 + / \ + 1 7 + \ / + 4 6 + / + 3 + */ + + root = balancer.remove(root, 5) + + /* + constructed tree: + 6 + / \ + 1 7 + \ + 4 + / + 3 + */ + + + assertEquals(6, root?.value) + assertEquals(1, root?.left?.value) + assertEquals(4, root?.left?.right?.value) + assertEquals(7, root?.right?.value) + assertEquals(3, root?.left?.right?.left?.value) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/app/model/bst/balancer/RBBalancerTest.kt b/app/src/test/kotlin/app/model/bst/balancer/RBBalancerTest.kt new file mode 100644 index 0000000..7fb60bb --- /dev/null +++ b/app/src/test/kotlin/app/model/bst/balancer/RBBalancerTest.kt @@ -0,0 +1,247 @@ +package app.model.bst.balancer + +import app.model.bst.node.RedBlackTreeNode +import kotlin.test.Test +import kotlin.test.assertEquals + +class RBBalancerTest { + private val balancer = RBBalancer() + + @Test + fun `test method 'add' in rb balancer`() { + var root: RedBlackTreeNode? = balancer.add(null, 1, true) + + root = balancer.add(root, 2, true) + root = balancer.add(root, 3, true) + root = balancer.add(root, 4, true) + root = balancer.add(root, 5, true) + root = balancer.add(root, 6, true) + root = balancer.add(root, 7, true) + root = balancer.add(root, 8, true) + root = balancer.add(root, 9, true) + + /* + constructed tree: + 4(BLACK) + / \ + (RED)2 6(RED) + / \ / \ + (BLACK)1 3(BLACK) (BLACK)5 8(BLACK) + / \ + 7(RED) 9(RED) + + */ + + + assertEquals(4, root.value) + assertEquals(2, root.left?.value) + assertEquals(6, root.right?.value) + assertEquals(8, root.right?.right?.value) + assertEquals(5, root.right?.left?.value) + assertEquals(1, root.left?.left?.value) + assertEquals(3, root.left?.right?.value) + assertEquals(7, root.right?.right?.left?.value) + assertEquals(9, root.right?.right?.right?.value) + assertEquals(RedBlackTreeNode.Color.BLACK, root.color) + assertEquals(RedBlackTreeNode.Color.RED, root.left?.color) + assertEquals(RedBlackTreeNode.Color.RED, root.right?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root.right?.right?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root.right?.left?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root.left?.left?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root.left?.right?.color) + assertEquals(RedBlackTreeNode.Color.RED, root.right?.right?.left?.color) + assertEquals(RedBlackTreeNode.Color.RED, root.right?.right?.right?.color) + + + } + + @Test + fun `test method 'remove' in rb balancer, remove root`() { + var root: RedBlackTreeNode? = balancer.add(null, 1, true) + + root = balancer.add(root, 2, true) + root = balancer.add(root, 3, true) + root = balancer.add(root, 4, true) + root = balancer.add(root, 5, true) + + + /* + constructed tree: + 2(BLACK) + / \ + (BLACK)1 4(BLACK) + / \ + (RED)3 5(RED) + */ + + + root = balancer.remove(root, 2) + + + /* + constructed tree: + 3(BLACK) + / \ + 1(BLACK) 4(BLACK) + \ + 5(RED) + */ + + + assertEquals(3, root?.value) + assertEquals(1, root?.left?.value) + assertEquals(4, root?.right?.value) + assertEquals(5, root?.right?.right?.value) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.left?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.right?.color) + assertEquals(RedBlackTreeNode.Color.RED, root?.right?.right?.color) + } + + @Test + fun `test method 'remove' in rb balancer, remove black node near the root`() { + var root: RedBlackTreeNode? = balancer.add(null, 1, true) + + root = balancer.add(root, 2, true) + root = balancer.add(root, 3, true) + root = balancer.add(root, 4, true) + root = balancer.add(root, 5, true) + + + /* + constructed tree: + 2(BLACK) + / \ + (BLACK)1 4(BLACK) + / \ + (RED)3 5(RED) + */ + + + root = balancer.remove(root, 1) + + + /* + constructed tree: + 4(BLACK) + / \ + 2(BLACK) 5(BLACK) + \ + 3(RED) + */ + + + assertEquals(4, root?.value) + assertEquals(2, root?.left?.value) + assertEquals(5, root?.right?.value) + assertEquals(3, root?.left?.right?.value) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.left?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.right?.color) + assertEquals(RedBlackTreeNode.Color.RED, root?.left?.right?.color) + } + + @Test + fun `test method 'remove' in rb balancer, remove red node near the root`() { + var root: RedBlackTreeNode? = balancer.add(null, 1, true) + + root = balancer.add(root, 2, true) + root = balancer.add(root, 3, true) + root = balancer.add(root, 4, true) + root = balancer.add(root, 5, true) + + + /* + constructed tree: + 2(BLACK) + / \ + (BLACK)1 4(BLACK) + / \ + (RED)3 5(RED) + */ + + + root = balancer.remove(root, 5) + + + /* + constructed tree: + 2(BLACK) + / \ + (BLACK)1 4(BLACK) + / + (RED)3 + */ + + + assertEquals(2, root?.value) + assertEquals(1, root?.left?.value) + assertEquals(4, root?.right?.value) + assertEquals(3, root?.right?.left?.value) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.left?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.right?.color) + assertEquals(RedBlackTreeNode.Color.RED, root?.right?.left?.color) + } + + @Test + fun `test method 'remove' in rb balancer, remove node, with many descendants`() { + var root: RedBlackTreeNode? = balancer.add(null, 1, true) + + root = balancer.add(root, 2, true) + root = balancer.add(root, 3, true) + root = balancer.add(root, 4, true) + root = balancer.add(root, 5, true) + root = balancer.add(root, 6, true) + root = balancer.add(root, 7, true) + root = balancer.add(root, 8, true) + root = balancer.add(root, 9, true) + + /* + constructed tree: + 4(BLACK) + / \ + (RED)2 6(RED) + / \ / \ + (BLACK)1 3(BLACK) (BLACK)5 8(BLACK) + / \ + 7(RED) 9(RED) + + */ + + + root = balancer.remove(root, 6) + + + /* + constructed tree: + 4(BLACK) + / \ + (RED)2 7(RED) + / \ / \ + (BLACK)1 3(BLACK) (BLACK)5 8(BLACK) + \ + 9(RED) + */ + + + + assertEquals(4, root?.value) + assertEquals(2, root?.left?.value) + assertEquals(7, root?.right?.value) + assertEquals(8, root?.right?.right?.value) + assertEquals(5, root?.right?.left?.value) + assertEquals(1, root?.left?.left?.value) + assertEquals(3, root?.left?.right?.value) + assertEquals(9, root?.right?.right?.right?.value) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.color) + assertEquals(RedBlackTreeNode.Color.RED, root?.left?.color) + assertEquals(RedBlackTreeNode.Color.RED, root?.right?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.right?.right?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.right?.left?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.left?.left?.color) + assertEquals(RedBlackTreeNode.Color.BLACK, root?.left?.right?.color) + assertEquals(RedBlackTreeNode.Color.RED, root?.right?.right?.right?.color) + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/app/model/bst/iterator/InOrderIteratorTest.kt b/app/src/test/kotlin/app/model/bst/iterator/InOrderIteratorTest.kt new file mode 100644 index 0000000..19f6d1b --- /dev/null +++ b/app/src/test/kotlin/app/model/bst/iterator/InOrderIteratorTest.kt @@ -0,0 +1,167 @@ +package app.model.bst.iterator + +import app.model.bst.AVLTree +import app.model.bst.BSTree +import app.model.bst.RBTree +import kotlin.test.Test +import kotlin.test.assertEquals + +class InOrderIteratorTest { + @Test + fun `test InlOrderIterator to BSTree`() { + val tree = BSTree() + tree.add(8) + tree.add(4) + tree.add(10) + tree.add(3) + tree.add(1) + tree.add(5) + tree.add(9) + tree.add(7) + tree.add(6) + tree.add(2) + /* 8 + / \ + 4 10 + / \ / \ + 3 5 9 null + / \ / \ + 1 null null 7 + / \ / \ + null 2 6 null + */ + val iterator = tree.iterator() + + var res = iterator.next() + assertEquals(res, tree.root?.left?.left?.left?.value) //1 + + res = iterator.next() + assertEquals(res, tree.root?.left?.left?.left?.right?.value) //2 + + res = iterator.next() + assertEquals(res, tree.root?.left?.left?.value) //3 + + res = iterator.next() + assertEquals(res, tree.root?.left?.value) //4 + + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.value) //5 + + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.right?.left?.value) //6 + + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.right?.value) //7 + + res = iterator.next() + assertEquals(res, tree.root?.value) //8 + + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.value) //9 + + res = iterator.next() + assertEquals(res, tree.root?.right?.value) //10 + } + + @Test + fun `test InlOrderIterator to AVLTree`() { + val tree = AVLTree() + for (i in 0..10) { + tree.add(i) + } + /* 3 + / \ + 1 7 + / \ / \ + 0 2 5 9 + / \ / \ + 4 6 8 10 + */ + val iterator = tree.iterator() + + var res = iterator.next() + assertEquals(res, tree.root?.left?.left?.value) //0 + + res = iterator.next() + assertEquals(res, tree.root?.left?.value) //1 + + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.value) //2 + + res = iterator.next() + assertEquals(res, tree.root?.value) //3 + + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.left?.value) //4 + + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.value) //5 + + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.right?.value) //6 + + res = iterator.next() + assertEquals(res, tree.root?.right?.value) //7 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.left?.value) //8 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.value) //9 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.right?.value) //10 + } + + @Test + fun `test InOrderIterator to RBTree`() { + val tree = RBTree() + for (i in 0..10) { + tree.add(i) + } + /* 3 + / \ + 1 5 + / \ / \ + 0 2 4 7 + / \ + 6 9 + / \ + 8 10 + */ + val iterator = tree.iterator() + + var res = iterator.next() + assertEquals(res, tree.root?.left?.left?.value) //0 + + res = iterator.next() + assertEquals(res, tree.root?.left?.value) //1 + + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.value) //2 + + res = iterator.next() + assertEquals(res, tree.root?.value) //3 + + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.value) //4 + + res = iterator.next() + assertEquals(res, tree.root?.right?.value) //5 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.left?.value)//6 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.value) //7 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.right?.left?.value) //8 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.right?.value) //9 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.right?.right?.value) //10 + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/app/model/bst/iterator/LevelOrderIteratorTest.kt b/app/src/test/kotlin/app/model/bst/iterator/LevelOrderIteratorTest.kt new file mode 100644 index 0000000..f3dd072 --- /dev/null +++ b/app/src/test/kotlin/app/model/bst/iterator/LevelOrderIteratorTest.kt @@ -0,0 +1,174 @@ +package app.model.bst.iterator + +import app.model.bst.AVLTree +import app.model.bst.BSTree +import app.model.bst.RBTree +import kotlin.test.Test +import kotlin.test.assertEquals + +class LevelOrderIteratorTest { + @Test + fun `test levelOrderIterator to BSTree`() { + val tree = BSTree() + tree.add(8) + tree.add(4) + tree.add(10) + tree.add(3) + tree.add(1) + tree.add(5) + tree.add(9) + tree.add(7) + tree.add(6) + tree.add(2) + /* 8 + / \ + 4 10 + / \ / \ + 3 5 9 null + / \ / \ + 1 null null 7 + / \ / \ + null 2 6 null + */ + val iterator = tree.levelOrderIterator() + + // 8 + var res = iterator.next() + assertEquals(res, tree.root?.value) + + // / \ + // 4 10 + res = iterator.next() + assertEquals(res, tree.root?.left?.value) + res = iterator.next() + assertEquals(res, tree.root?.right?.value) + + // / \ / \ + // 3 5 9 null + res = iterator.next() + assertEquals(res, tree.root?.left?.left?.value) + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.value) + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.value) + + // / \ / \ + // 1 null null 7 + res = iterator.next() + assertEquals(res, tree.root?.left?.left?.left?.value) + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.right?.value) + + // / \ / \ + // null 2 6 null + res = iterator.next() + assertEquals(res, tree.root?.left?.left?.left?.right?.value) + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.right?.left?.value) + } + + @Test + fun `test levelOrderIterator to AVLTree`() { + val tree = AVLTree() + for (i in 0..10) { + tree.add(i) + } + /* 3 + / \ + 1 7 + / \ / \ + 0 2 5 9 + / \ / \ + 4 6 8 10 + */ + val iterator = tree.levelOrderIterator() + + // 3 + var res = iterator.next() + assertEquals(res, tree.root?.value) + + // / \ + // 1 7 + res = iterator.next() + assertEquals(res, tree.root?.left?.value) + res = iterator.next() + assertEquals(res, tree.root?.right?.value) + + // / \ / \ + // 0 2 5 9 + res = iterator.next() + assertEquals(res, tree.root?.left?.left?.value) + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.value) + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.value) + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.value) + + // / \ / \ + // 4 6 8 10 + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.left?.value) + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.right?.value) + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.left?.value) + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.right?.value) + } + + @Test + fun `test levelOrderIterator to RBTree`() { + val tree = RBTree() + for (i in 0..10) { + tree.add(i) + } + /* 3 + / \ + 1 5 + / \ / \ + 0 2 4 7 + / \ + 6 9 + / \ + 8 10 + */ + val iterator = tree.levelOrderIterator() + + // 3 + var res = iterator.next() + assertEquals(res, tree.root?.value) + + // / \ + // 1 5 + res = iterator.next() + assertEquals(res, tree.root?.left?.value) + res = iterator.next() + assertEquals(res, tree.root?.right?.value) + + // / \ / \ + // 0 2 4 7 + res = iterator.next() + assertEquals(res, tree.root?.left?.left?.value) + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.value) + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.value) + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.value) + + // / \ + // 6 9 + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.left?.value) + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.right?.value) + + // / \ + // 8 10 + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.right?.left?.value) + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.right?.right?.value) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/app/model/bst/iterator/PreOrderIteratorTest.kt b/app/src/test/kotlin/app/model/bst/iterator/PreOrderIteratorTest.kt new file mode 100644 index 0000000..46fb9b4 --- /dev/null +++ b/app/src/test/kotlin/app/model/bst/iterator/PreOrderIteratorTest.kt @@ -0,0 +1,167 @@ +package app.model.bst.iterator + +import app.model.bst.AVLTree +import app.model.bst.BSTree +import app.model.bst.RBTree +import kotlin.test.Test +import kotlin.test.assertEquals + +class PreOrderIteratorTest { + @Test + fun `test PreOrderIterator to BSTree`() { + val tree = BSTree() + tree.add(8) + tree.add(4) + tree.add(10) + tree.add(3) + tree.add(1) + tree.add(5) + tree.add(9) + tree.add(7) + tree.add(6) + tree.add(2) + /* 8 + / \ + 4 10 + / \ / \ + 3 5 9 null + / \ / \ + 1 null null 7 + / \ / \ + null 2 6 null + */ + val iterator = tree.preOrderIterator() + + var res = iterator.next() + assertEquals(res, tree.root?.value) //8 + + res = iterator.next() + assertEquals(res, tree.root?.left?.value) //4 + + res = iterator.next() + assertEquals(res, tree.root?.left?.left?.value) //3 + + res = iterator.next() + assertEquals(res, tree.root?.left?.left?.left?.value) //1 + + res = iterator.next() + assertEquals(res, tree.root?.left?.left?.left?.right?.value) //2 + + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.value) //5 + + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.right?.value) //7 + + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.right?.left?.value) //6 + + res = iterator.next() + assertEquals(res, tree.root?.right?.value) //10 + + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.value) //9 + } + + @Test + fun `test levelOrderIterator to AVLTree`() { + val tree = AVLTree() + for (i in 0..10) { + tree.add(i) + } + /* 3 + / \ + 1 7 + / \ / \ + 0 2 5 9 + / \ / \ + 4 6 8 10 + */ + val iterator = tree.preOrderIterator() + + var res = iterator.next() + assertEquals(res, tree.root?.value) //3 + + res = iterator.next() + assertEquals(res, tree.root?.left?.value) //1 + + res = iterator.next() + assertEquals(res, tree.root?.left?.left?.value) //0 + + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.value) //2 + + res = iterator.next() + assertEquals(res, tree.root?.right?.value) //7 + + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.value) //5 + + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.left?.value) //4 + + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.right?.value) //6 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.value) //9 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.left?.value) //8 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.right?.value) //10 + } + + @Test + fun `test levelOrderIterator to RBTree`() { + val tree = RBTree() + for (i in 0..10) { + tree.add(i) + } + /* 3 + / \ + 1 5 + / \ / \ + 0 2 4 7 + / \ + 6 9 + / \ + 8 10 + */ + val iterator = tree.preOrderIterator() + + var res = iterator.next() + assertEquals(res, tree.root?.value) //3 + + res = iterator.next() + assertEquals(res, tree.root?.left?.value) //1 + + res = iterator.next() + assertEquals(res, tree.root?.left?.left?.value) //0 + + res = iterator.next() + assertEquals(res, tree.root?.left?.right?.value) //2 + + res = iterator.next() + assertEquals(res, tree.root?.right?.value) //5 + + res = iterator.next() + assertEquals(res, tree.root?.right?.left?.value) //4 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.value) //7 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.left?.value) //6 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.right?.value) //9 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.right?.left?.value) //8 + + res = iterator.next() + assertEquals(res, tree.root?.right?.right?.right?.right?.value) //10 + } +} diff --git a/app/src/test/kotlin/app/model/bst/utils/InvariantChecker.kt b/app/src/test/kotlin/app/model/bst/utils/InvariantChecker.kt new file mode 100644 index 0000000..d0dec5e --- /dev/null +++ b/app/src/test/kotlin/app/model/bst/utils/InvariantChecker.kt @@ -0,0 +1,112 @@ +package app.model.bst.utils + +import app.model.bst.AVLTree +import app.model.bst.BinarySearchTree +import app.model.bst.RBTree +import app.model.bst.node.AVLTreeNode +import app.model.bst.node.BinTreeNode +import app.model.bst.node.RedBlackTreeNode +import java.util.* +import kotlin.math.abs + +object InvariantChecker { + fun , NodeType : BinTreeNode> + isBinarySearchTree( + bst: BinarySearchTree + ): Boolean { + var currentNode = bst.root + var prevNode: NodeType? = null + val stack = Stack() + + while (currentNode != null || stack.isNotEmpty()) { + while (currentNode != null) { + stack.push(currentNode) + currentNode = currentNode.left + } + + currentNode = stack.pop() + if (prevNode != null && currentNode.value < prevNode.value) { + return false + } + prevNode = currentNode + + currentNode = currentNode.right + } + + return true + } + + fun > checkNeighborHeights(tree: AVLTree?): Boolean { + fun helper(node: AVLTreeNode?): Boolean { + if (node == null) { + return true + } + + val leftHeight = calculateHeight(node.left) + val rightHeight = calculateHeight(node.right) + + if (abs(leftHeight - rightHeight) > 1) { + return false + } + + return helper(node.left) && helper(node.right) + } + return helper(tree?.root) + } + + private fun > calculateHeight(node: AVLTreeNode?): Int { + if (node == null) { + return 0 + } + val leftHeight = calculateHeight(node.left) + val rightHeight = calculateHeight(node.right) + return 1 + maxOf(leftHeight, rightHeight) + } + + + fun > isBlackHeightBalanced(tree: RBTree?): Boolean { + fun helper(root: RedBlackTreeNode?): Boolean { + root ?: return true + + val leftHeight = blackHeight(root.left) + val rightHeight = blackHeight(root.right) + if (leftHeight != rightHeight) { + return false + } + + return helper(root.left) && helper(root.right) + } + return helper(tree?.root) + } + + private fun > blackHeight(node: RedBlackTreeNode?): Int { + if (node == null) { + return 0 + } + + val leftHeight = blackHeight(node.left) + val rightHeight = blackHeight(node.right) + + return if (node.color == RedBlackTreeNode.Color.BLACK) { + maxOf(leftHeight, rightHeight) + 1 + } else { + maxOf(leftHeight, rightHeight) + } + } + + /** + * Checks that all nodes linked correctly. + * For example, that node.left.parent is the same as node. + **/ + + fun > isParentLinkedRight(tree: RBTree?): Boolean { + fun helper(root: RedBlackTreeNode?): Boolean { + root ?: return true + if (root.parent != null && (root !== root.parent?.left && root !== root.parent?.right)) return false + + return helper(root.left) && helper(root.right) + } + + return helper(tree?.root) + } +} diff --git a/dev-docker-compose.yml b/dev-docker-compose.yml new file mode 100644 index 0000000..ba83097 --- /dev/null +++ b/dev-docker-compose.yml @@ -0,0 +1,21 @@ +version: '3.9' + +services: + neo4j: + image: neo4j:latest + container_name: neo4j + ports: + - "7474:7474" + - "7687:7687" + environment: + - NEO4J_AUTH=neo4j/password + + postgres: + image: postgres:latest + container_name: postgres + ports: + - "5432:5432" + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + - POSTGRES_DB=trees \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ffed3a2 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100755 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "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%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..42b3f29 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + } +} + +rootProject.name = "trees-4" + +include("app") \ No newline at end of file