diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 0000000..44b952f --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,35 @@ +name: Build and test + +on: + workflow_dispatch: + + push: + branches: [ "main", "dev" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Build source code + run: ./gradlew build -x test + + - name: Run tests + run: ./gradlew test >./test-res-out.log 2>./test-res-err.log + continue-on-error: true + + - name: Display test results + run: python3 ./scripts/test-result-printer.py --dir ./lib/build/test-results/test --all-failures + + - name: Run jacoco coverage report + run: ./gradlew jacocoTestReport >./test-res-out.log 2>./test-res-err.log + + - name: Display info about coverage + run: python3 ./scripts/csv-reports-printer.py --input ./lib/build/reports/jacoco/info.csv --lib lib + + - name: Clear tmpfiles of runnig tests + run: rm ./test-res-out.log && rm ./test-res-err.log diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05ede4d --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +### Gradle console-app generated template +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +*/build/* + +### VSCode Template +.vscode/ + +### IntelliJ IDEA Template +.idea/ +*.iml + +### Apple MacOS folder attributes +*.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..22f3e1d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Ilia Suponev, Maxim Rodionov, Vladimir Zaikin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 0000000..406ed99 --- /dev/null +++ b/ReadMe.md @@ -0,0 +1,270 @@ +[//]: # (Project readme template from https://github.com/othneildrew/Best-README-Template/) + + +[![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/) + + +

TreeTripper

+ +## Description + +Library `TreeTripper`: provides implementations of the following binary search tree data structures: +- [x] [`Binary Search Tree`](lib/src/main/kotlin/tree_tripper/binary_trees/BSTree.kt), see more [information](https://en.wikipedia.org/wiki/Binary_search_tree) +- [x] [`AVL tree`](lib/src/main/kotlin/tree_tripper/binary_trees/AVLTree.kt), see more [information](https://en.wikipedia.org/wiki/AVL_tree) +- [x] [`Red-black tree`](lib/src/main/kotlin/tree_tripper/binary_trees/RBTree.kt), + see more [information](https://en.wikipedia.org/wiki/Left-leaning_red%E2%80%93black_tree) + +> [!IMPORTANT] +> The red-black tree is implemented based on the algorithm +> of left-linear red-black trees described by Robert Sedgewick +> on his [website](https://sedgewick.io/) and [presentation](https://sedgewick.io/wp-content/uploads/2022/03/2008-09LLRB.pdf) about it + +The library supports the extension both internally (future library updates) and externally (implemented by the user). + +## Getting started +To run building library execute command: +```bash +./gradlew build -x test +``` + +To run tests of library execute command: +```bash +./gradlew test +``` + +## Using library + +### Basic operations ++ `insert`, see [docs](lib/src/main/kotlin/tree_tripper/SearchTree.kt#L17) ++ `search`, see [docs](lib/src/main/kotlin/tree_tripper/SearchTree.kt#L50) ++ `remove`, see [docs](lib/src/main/kotlin/tree_tripper/SearchTree.kt#L36) + +### Examples + +##### Example 1 (importing) +```kotlin +import tree_tripper.binary_trees.BSTree +import tree_tripper.binary_trees.AVLTree +import tree_tripper.binary_trees.RBTree + + +val simpleTree = BSTree() // initialization of empty simple binary search tree +val avlTree = AVLTree() // initialization of empty AVL tree +val rbTree = RBTree>() // initialization of empty Red-Black tree +``` + +##### Example 2 (inserting) +Code: +```kotlin +import tree_tripper.binary_trees.BSTree + +fun main() { + val tree = BSTree() + + tree.insert(key = 1, value = 1) + tree.insert(key = 2, value = 2) + tree.insert(key = 3, value = 3) + + /* The words `key` and `value` are optional */ + tree.insert(4, 4) + + /* Alternative insert method */ + tree[5] = 5 + + println(tree) +} +``` +Output: +```text +BSTree(1: 1, 2: 2, 3: 3, 4: 4, 5: 5) +``` + +##### Example 3 (searching) +Code: +```kotlin +import tree_tripper.binary_trees.BSTree + +fun main() { + val tree = BSTree() + /* + ... + inserting from `example 2` + ... + */ + + /* Existing element in tree */ + println(tree.search(key = 1)) + println(tree.search(key = 3)) + println(tree.search(key = 5)) + + /* Unexciting element in tree */ + println(tree.search(key = -2)) + println(tree.searchOrDefault(7, "Element not found")) + + /* Alternative search method */ + println(tree[2]) + println(tree[0]) +} +``` +Output: +```text +1 +3 +5 +null +Element not found +2 +null +``` + +##### Example 4 (removing) +Code: +```kotlin +import tree_tripper.binary_trees.BSTree + +fun main() { + val tree = BSTree() + /* + ... + inserting from `example 2` + ... + */ + + /* Existing element in tree */ + println(tree.remove(key = 1)) + println(tree.remove(key = 3)) + println(tree.remove(key = 5)) + + /* Unexciting element in tree */ + println(tree.remove(key = -2)) + println(tree.removeOrDefault(key = 7, "Element not found")) + + println(tree) +} +``` +Output: +```text +1 +3 +5 +null +Element not found +BSTree(2: 2, 4: 4) +``` + +##### Example 5 (tree-like printing) +Code: +```kotlin +import tree_tripper.binary_trees.BSTree +import tree_tripper.binary_trees.AVLTree +import tree_tripper.binary_trees.RBTree + +fun main() { + val simpleTree = BSTree() + val avlTree = AVLTree() + val rbTree = RBTree() + /* + ... + inserting similar to `example 2` for each tree + ... + */ + + println("${simpleTree.toStringWithTreeView()}\n") + println("${avlTree.toStringWithTreeView()}\n") + println("${rbTree.toStringWithTreeView()}\n") +} +``` +Output: +```text +BSTree( + (5: 5) + (4: 4) + (3: 3) + (2: 2) +(1: 1) +) + +AVLTree( + (5: 5) + (4: 4) + (3: 3) +(2: 2) + (1: 1) +) + +RBTree( + (5: 5) - BLACK +(4: 4) - BLACK + (3: 3) - BLACK + (2: 2) - RED + (1: 1) - BLACK +) +``` + +##### Example 6 (iterators) +Code: +```kotlin +import tree_tripper.binary_trees.AVLTree + +fun main() { + val tree = AVLTree() + /* + ... + inserting from `example 2` + ... + */ + + println("WIDTH ORDER:") + tree.forEach(IterationOrders.WIDTH_ORDER) { + println(it) + } + println() + println("INCREASING ORDER:") + tree.forEach(IterationOrders.INCREASING_ORDER) { + println(it) + } + println() + println("DECREASING ORDER:") + tree.forEach(IterationOrders.DECREASING_ORDER) { + println(it) + } +} +``` +Output: +```text +WIDTH ORDER: +(2: 2) +(1: 1) +(4: 4) +(3: 3) +(5: 5) + +INCREASING ORDER: +(1: 1) +(2: 2) +(3: 3) +(4: 4) +(5: 5) + +DECREASING ORDER: +(5: 5) +(4: 4) +(3: 3) +(2: 2) +(1: 1) +``` + +## Documentation +See more [_**documentation**_](lib/src/main/kotlin/tree_tripper/SearchTree.kt) of library `TreeTripper` to learn more about it. + +## Authors + +- [@IliaSuponeff](https://github.com/IliaSuponeff) +- [@RodionovMaxim05](https://github.com/RodionovMaxim05) +- [@Friend-zva](https://github.com/Friend-zva), sometimes in commits his name is _**Vladimir Zaikin**_ + +## License + +Distributed under the [MIT License](https://choosealicense.com/licenses/mit/). See [`LICENSE`](LICENSE) for more information. + +

(back to top)

diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..18f452c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,6 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.parallel=true +org.gradle.caching=true + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..4f41755 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,13 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[versions] +commons-math3 = "3.6.1" +guava = "32.1.3-jre" + +[libraries] +commons-math3 = { module = "org.apache.commons:commons-math3", version.ref = "commons-math3" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } + +[plugins] +jvm = { id = "org.jetbrains.kotlin.jvm", version = "1.9.20" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..d64cd49 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..a80b22c --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +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 + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# 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 + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts new file mode 100644 index 0000000..62c75da --- /dev/null +++ b/lib/build.gradle.kts @@ -0,0 +1,51 @@ +plugins { + // Apply the org.jetbrains.kotlin.jvm Plugin to add support for Kotlin. + alias(libs.plugins.jvm) + + // Code coverage plugin + jacoco +} + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + // This dependency is exported to consumers, that is to say found on their compile classpath. + api(libs.commons.math3) + // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + // https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params + testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.2") + // This dependency is used internally, and not exposed to consumers on their own compile classpath. + implementation(libs.guava) +} + +// Apply a specific Java toolchain to ease working on different environments. +java { + toolchain { + languageVersion = JavaLanguageVersion.of(19) + } +} + +tasks.named("test") { + useJUnitPlatform() + testLogging { + showCauses = false + showStackTraces = false + showExceptions = false + } +} + +tasks.named("jacocoTestReport") { + val sep = File.separator + val jacocoReportsDirName = "reports${sep}jacoco" + reports { + csv.required = true + xml.required = false + html.required = true + csv.outputLocation = layout.buildDirectory.file("${jacocoReportsDirName}${sep}info.csv") + html.outputLocation = layout.buildDirectory.dir("${jacocoReportsDirName}${sep}html") + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/tree_tripper/SearchTree.kt b/lib/src/main/kotlin/tree_tripper/SearchTree.kt new file mode 100644 index 0000000..e7773ee --- /dev/null +++ b/lib/src/main/kotlin/tree_tripper/SearchTree.kt @@ -0,0 +1,107 @@ +package tree_tripper + +import tree_tripper.iterators.IterationOrders + + +/** + * The interface represents a search tree, that inherits from [Iterable] interface. + * + * @param K the key type in a tree, supporting the [Comparable] interface + * @param V the value type in a tree + */ +public interface SearchTree, V>: Iterable> { + + /** + * Inserts or updates the specified [value] with the specified [key] in a tree. + */ + public fun insert(key: K, value: V) + + /** + * Inserts the specified [value] with the specified [key] in a tree. + * @return true if the specified [value] with the specified [key] was inserted in a tree, + * false if a tree was not modified. + */ + public fun insertIfAbsent(key: K, value: V): Boolean + + /** + * Associates the specified [value] with the specified [key] in a tree. + */ + public operator fun set(key: K, value: V) + + /** + * Removes a given [key] and its corresponding value from a tree. + * @return the removed value associated with a given [key], or + * null if such a [key] does not present in a tree. + */ + public fun remove(key: K): V? + + /** + * Removes a given [key] and its corresponding value from a tree. + * @return the removed value associated with a given [key], or + * the [defaultValue] if such a [key] does not present in a tree. + */ + public fun removeOrDefault(key: K, defaultValue: V): V + + /** + * Searches the value associated with a given [key]. + * @return the value associated with a given [key], or + * null if such a [key] does not present in a tree. + */ + public fun search(key: K): V? + + /** + * Searches the value associated with a given [key]. + * @return the value associated with a given [key], or + * the [defaultValue] if such a [key] does not present in a tree. + */ + public fun searchOrDefault(key: K, defaultValue: V): V + + /** + * Checks if a given [key] is contained in a tree. + */ + public fun contains(key: K): Boolean + + /** + * Returns the value associated with a given [key], or + * null if such a [key] does not present in a tree. + */ + public operator fun get(key: K): V? + + /** + * Returns the key/value pair with the max key, or null if a tree is empty. + */ + public fun getMax(): Pair? + + /** + * Returns the key/value pair with the max key in subtree with a given [key] in root, or + * null if such a [key] does not present in a tree. + */ + public fun getMaxInSubtree(key: K): Pair? + + /** + * Returns the key/value pair with the min key, or null if a tree is empty. + */ + public fun getMin(): Pair? + + /** + * Returns the key/value pair with the min key in subtree with a given [key] in root, or + * null if such a [key] does not present in a tree. + */ + public fun getMinInSubtree(key: K): Pair? + + // Iterator + public fun iterator(order: IterationOrders): Iterator> + + public fun forEach(order: IterationOrders, action: (Pair) -> Unit) + + /** + * Returns a string with a transformed tree according to the [order]. + */ + public fun toString(order: IterationOrders): String + + /** + * Returns a string with a transformed tree according to the tree structure. + */ + public fun toStringWithTreeView(): String + +} diff --git a/lib/src/main/kotlin/tree_tripper/binary_trees/AVLTree.kt b/lib/src/main/kotlin/tree_tripper/binary_trees/AVLTree.kt new file mode 100644 index 0000000..0ea16fe --- /dev/null +++ b/lib/src/main/kotlin/tree_tripper/binary_trees/AVLTree.kt @@ -0,0 +1,89 @@ +package tree_tripper.binary_trees + +import tree_tripper.nodes.binary_nodes.AVLTreeNode + + +/** + * AVLTree class represents a self-balancing binary search tree that maintains + * the property of AVL tree, ensuring that the heights + * of the two child subtrees of any node differ by at most one. + * + * @param K the type of the keys in the tree + * @param V the value type associated with the key + */ +public open class AVLTree, V>: AbstractBSTree>() { + + override fun createNode(key: K, value: V): AVLTreeNode { + return AVLTreeNode(key, value) + } + + override fun balanceTree(node: AVLTreeNode): AVLTreeNode { + node.updateHeight() + return balance(node) + } + + /** + * @param node the AVLTreeNode for which to calculate the balance factor + * @return balance factor (height difference of his children) of the [node] + */ + protected fun balanceFactor(node: AVLTreeNode?): Int { + return (node?.rightChild?.height ?: 0) - (node?.leftChild?.height ?: 0) + } + + /** + * Defines and calls the method(s) for balancing the current [node]. + * + * @param node the node to balance in the AVL tree + * @return the root of the rebalanced subtree or a [node] if balance is not required + */ + protected fun balance(node: AVLTreeNode): AVLTreeNode { + when (balanceFactor(node)) { + -2 -> { + if (balanceFactor(node.leftChild) == 1) + node.leftChild = rotateLeft(node.leftChild as AVLTreeNode) + return rotateRight(node) + } + 2 -> { + if (balanceFactor(node.rightChild) == -1) + node.rightChild = rotateRight(node.rightChild as AVLTreeNode) + return rotateLeft(node) + } + else -> return node + } + } + + /** + * Performs a left rotation (counterclockwise) of the tree with the [node] as the root. + * Updates the heights of the affected nodes. + * + * @param node the node to perform a left rotation on + * @returns the root of the rotated subtree + */ + protected fun rotateLeft(node: AVLTreeNode): AVLTreeNode { + val nodeSwapped = node.rightChild ?: return node + node.rightChild = nodeSwapped.leftChild + nodeSwapped.leftChild = node + + node.updateHeight() + nodeSwapped.updateHeight() + return nodeSwapped + } + + /** + * Performs a right rotation (clockwise) of the tree with the [node] as the root. + * Updates the heights of the affected nodes. + * + * @param node the node to perform a right rotation on + * @returns the root of the rotated subtree + */ + protected fun rotateRight(node: AVLTreeNode): AVLTreeNode { + val nodeSwapped = node.leftChild ?: return node + node.leftChild = nodeSwapped.rightChild + nodeSwapped.rightChild = node + + node.updateHeight() + nodeSwapped.updateHeight() + return nodeSwapped + } + +} diff --git a/lib/src/main/kotlin/tree_tripper/binary_trees/AbstractBSTree.kt b/lib/src/main/kotlin/tree_tripper/binary_trees/AbstractBSTree.kt new file mode 100644 index 0000000..e507d30 --- /dev/null +++ b/lib/src/main/kotlin/tree_tripper/binary_trees/AbstractBSTree.kt @@ -0,0 +1,247 @@ +package tree_tripper.binary_trees + +import tree_tripper.SearchTree +import tree_tripper.iterators.BinarySearchTreeIterator +import tree_tripper.iterators.IterationOrders +import tree_tripper.nodes.binary_nodes.AbstractBSTreeNode +import tree_tripper.nodes.notNullNodeAction + + +/** + * The class represents an abstract binary search tree, + * from which binary search trees are inherited. + * + * @param K the key type in a tree, supporting the [Comparable] interface + * @param V the value type in a tree + * @param N the node type in a tree + */ +public abstract class AbstractBSTree, V, N: AbstractBSTreeNode>: SearchTree { + protected var root: N? = null + private set + public var size: Int = 0 + private set + + override fun insert(key: K, value: V) { + insert(key, value, permissionUpdate = true) + } + + override fun insertIfAbsent(key: K, value: V): Boolean { + return insert(key, value, permissionUpdate = false) + } + + override fun set(key: K, value: V) { + insert(key, value) + } + + override fun remove(key: K): V? { + val resultRemove = removeNode(root, key) + updateRoot(resultRemove.first) + if (resultRemove.second != null) size-- + return resultRemove.second + } + + override fun removeOrDefault(key: K, defaultValue: V): V { + return remove(key) ?: defaultValue + } + + override fun search(key: K): V? { + return searchNode(key)?.value + } + + override fun searchOrDefault(key: K, defaultValue: V): V { + return search(key) ?: defaultValue + } + + override fun contains(key: K): Boolean { + return search(key) != null + } + + override fun get(key: K): V? { + return search(key) + } + + override fun getMaxInSubtree(key: K): Pair? { + val resultSearch = getMaxNodeInSubtree(searchNode(key)) ?: return null + return Pair(resultSearch.key, resultSearch.value) + } + + override fun getMax(): Pair? { + return notNullNodeAction(root, null) { node -> getMaxInSubtree(node.key) } + } + + override fun getMinInSubtree(key: K): Pair? { + val resultSearch = getMinNodeInSubtree(searchNode(key)) ?: return null + return Pair(resultSearch.key, resultSearch.value) + } + + override fun getMin(): Pair? { + return notNullNodeAction(root, null) { node -> getMinInSubtree(node.key) } + } + + override fun iterator(): BinarySearchTreeIterator { + return iterator(IterationOrders.WIDTH_ORDER) + } + + override fun iterator(order: IterationOrders): BinarySearchTreeIterator { + return BinarySearchTreeIterator(root, order) + } + + override fun forEach(order: IterationOrders, action: (Pair) -> Unit) { + val treeIterator: BinarySearchTreeIterator = iterator(order) + while (treeIterator.hasNext()) + action(treeIterator.next()) + } + + override fun toString(): String { + return toString(IterationOrders.WIDTH_ORDER) + } + + override fun toString(order: IterationOrders): String { + val builder = StringBuilder() + this.forEach(order) { pair: Pair -> builder.append("${pair.first}: ${pair.second}, ") } + if (builder.isNotEmpty()) + repeat(2) { builder.deleteCharAt(builder.length - 1) } // remove ", " in the last pair + return "${this.javaClass.simpleName}($builder)" + } + + override fun toStringWithTreeView(): String { + val builder = StringBuilder() + notNullNodeAction(root, Unit) { node -> node.toStringWithSubtreeView(0, builder) } + return "${this.javaClass.simpleName}(\n$builder)" + } + + /** + * Returns a new [N] node with the specified [value] with the specified [key]. + */ + protected abstract fun createNode(key: K, value: V): N + + /** + * Changes the root to a given [node]. + */ + protected open fun updateRoot(node: N?) { + root = node + } + + /** + * Balances subtree with a given [node] at the top. + */ + protected open fun balanceTree(node: N): N { + return node + } + + /** + * Inserts the specified [value] with the specified [key] in a tree or + * updates [value] if [permissionUpdate] is true + * @return true if the specified [value] with the specified [key] was inserted in a tree, + * false if a tree was not modified. + */ + private fun insert(key: K, value: V, permissionUpdate: Boolean): Boolean { + val insertResult: Pair = insertNode(root, key, value, permissionUpdate) + updateRoot(insertResult.first) + if (insertResult.second) size++ + return insertResult.second + } + + /** + * Add recursively the node with the specified [value] with the specified [key] in a tree or + * updates [value] if [permissionUpdate] is true and balances tree on every call. + * @return a pair of a new or balanced [N] node, and true if the specified [value] with + * the specified [key] was inserted in a tree, false if not. + */ + private fun insertNode(node: N?, key: K, value: V, permissionUpdate: Boolean): Pair { + if (node == null) return Pair(createNode(key, value), true) + + val resultInsert: Pair + val resultCompare: Int = key.compareTo(node.key) + if (resultCompare < 0) { + resultInsert = insertNode(node.leftChild, key, value, permissionUpdate) + node.leftChild = resultInsert.first + } else if (resultCompare > 0) { + resultInsert = insertNode(node.rightChild, key, value, permissionUpdate) + node.rightChild = resultInsert.first + } else { + if (permissionUpdate) node.value = value + return Pair(node, false) + } + + return Pair(balanceTree(node), resultInsert.second) + } + + /** + * Removes recursively the node with a given [key] and balances tree on every call. + * @return a pair of a balanced [N] node or null, and [V] value corresponding the given [key] + * if a node was removed, null if not. + */ + protected open fun removeNode(node: N?, key: K): Pair { + if (node == null) return Pair(null, null) + + val resultRemove: Pair + val resultCompare: Int = key.compareTo(node.key) + if (resultCompare < 0) { + resultRemove = removeNode(node.leftChild, key) + node.leftChild = resultRemove.first + } else if (resultCompare > 0) { + resultRemove = removeNode(node.rightChild, key) + node.rightChild = resultRemove.first + } else { + val nodeSubstitutive: N? + if (node.leftChild == null || node.rightChild == null) { + nodeSubstitutive = node.leftChild ?: node.rightChild + return Pair(nodeSubstitutive, node.value) + } + nodeSubstitutive = getMaxNodeInSubtree(node.leftChild) as N + node.leftChild = removeNode(node.leftChild, nodeSubstitutive.key).first + nodeSubstitutive.rightChild = node.rightChild + nodeSubstitutive.leftChild = node.leftChild + return Pair(balanceTree(nodeSubstitutive), node.value) + } + + return Pair(balanceTree(node), resultRemove.second) + } + + /** + * Searches the node with a given key. + * @return the found node or null if the node is not contained in a tree. + */ + private fun searchNode(key: K): N? { + var nodeCurrent: N? = root ?: return null + + while (nodeCurrent != null) { + val resultCompare: Int = key.compareTo(nodeCurrent.key) + if (resultCompare < 0) + nodeCurrent = nodeCurrent.leftChild + else if (resultCompare > 0) + nodeCurrent = nodeCurrent.rightChild + else + return nodeCurrent + } + return null + } + + /** + * Returns the [N] node with the max key in subtree with a given [node] in root, or null + * if such a [node] is not contained in a tree. + */ + protected fun getMaxNodeInSubtree(node: N?): N? { + if (node == null) return null + + var nodeCurrent: N = node + while (true) + nodeCurrent = nodeCurrent.rightChild ?: break + return nodeCurrent + } + + /** + * Returns the [N] node with the min key in subtree with a given [node] in root, or null + * if such a [node] is not contained in a tree. + */ + protected fun getMinNodeInSubtree(node: N?): N? { + if (node == null) return null + + var nodeCurrent: N = node + while (true) + nodeCurrent = nodeCurrent.leftChild ?: break + return nodeCurrent + } + +} diff --git a/lib/src/main/kotlin/tree_tripper/binary_trees/BSTree.kt b/lib/src/main/kotlin/tree_tripper/binary_trees/BSTree.kt new file mode 100644 index 0000000..5f790c6 --- /dev/null +++ b/lib/src/main/kotlin/tree_tripper/binary_trees/BSTree.kt @@ -0,0 +1,18 @@ +package tree_tripper.binary_trees + +import tree_tripper.nodes.binary_nodes.BSTreeNode + + +/** + * The class represents the binary search tree. + * + * @param K the key type in the tree, supporting the [Comparable] interface + * @param V the value type in the tree + */ +public open class BSTree, V>: AbstractBSTree>() { + + override fun createNode(key: K, value: V): BSTreeNode { + return BSTreeNode(key, value) + } + +} diff --git a/lib/src/main/kotlin/tree_tripper/binary_trees/RBTree.kt b/lib/src/main/kotlin/tree_tripper/binary_trees/RBTree.kt new file mode 100644 index 0000000..12aa1ac --- /dev/null +++ b/lib/src/main/kotlin/tree_tripper/binary_trees/RBTree.kt @@ -0,0 +1,205 @@ +package tree_tripper.binary_trees + +import tree_tripper.nodes.binary_nodes.RBTreeNode +import tree_tripper.nodes.notNullNodeAction +import tree_tripper.nodes.notNullNodeUpdate + + +/** + * A class that represents a red-black tree data structure. + * + * @param K the type of the keys in the tree + * @param V the type of the values in the tree + */ +public open class RBTree, V>: AbstractBSTree>() { + + override fun createNode(key: K, value: V): RBTreeNode { + return RBTreeNode(key, value, isRed = true, leftChild = null, rightChild = null) + } + + override fun updateRoot(node: RBTreeNode?) { + notNullNodeUpdate(node) { it.isRed = false } + super.updateRoot(node) + } + + override fun balanceTree(node: RBTreeNode): RBTreeNode { + var nodeCurrent = node + if (isRedColor(nodeCurrent.rightChild) && !isRedColor(nodeCurrent.leftChild)) { + nodeCurrent = rotateLeft(nodeCurrent) + } + if (isRedColor(nodeCurrent.leftChild) && isRedLeftChild(nodeCurrent.leftChild)) { + nodeCurrent = rotateRight(nodeCurrent) + } + if (isRedColor(nodeCurrent.leftChild) && isRedColor(nodeCurrent.rightChild)) { + flipColors(nodeCurrent) + } + return nodeCurrent + } + + override fun removeNode(node: RBTreeNode?, key: K): Pair?, V?> { + if (node == null) return Pair(null, null) + + val removeResult: Pair?, V?> + var resultCompare: Int = key.compareTo(node.key) + var nodeCurrent = node + if (resultCompare < 0) { + if (!isRedColor(nodeCurrent.leftChild) && !isRedLeftChild(nodeCurrent.leftChild)) + nodeCurrent = moveRedLeft(nodeCurrent) + + removeResult = removeNode(nodeCurrent.leftChild, key) + nodeCurrent.leftChild = removeResult.first + } else { + if (isRedColor(nodeCurrent.leftChild)) { + nodeCurrent = rotateRight(nodeCurrent) + resultCompare = key.compareTo(nodeCurrent.key) + } + if (resultCompare == 0 && nodeCurrent.rightChild == null) + return Pair(null, nodeCurrent.value) + if (!isRedColor(nodeCurrent.rightChild) && !isRedLeftChild(nodeCurrent.rightChild)) { + nodeCurrent = moveRedRight(nodeCurrent) + resultCompare = key.compareTo(nodeCurrent.key) + } + if (resultCompare == 0) { + val nodeWithMinimalKey = getMinNodeInSubtree(nodeCurrent.rightChild) as RBTreeNode + val nodeSubstitutive: RBTreeNode = createNode(nodeWithMinimalKey.key, nodeWithMinimalKey.value) + nodeSubstitutive.isRed = nodeCurrent.isRed + nodeSubstitutive.leftChild = nodeCurrent.leftChild + nodeSubstitutive.rightChild = removeMinNode(nodeCurrent.rightChild) + return Pair(balanceTree(nodeSubstitutive), nodeCurrent.value) + } else { + removeResult = removeNode(nodeCurrent.rightChild, key) + nodeCurrent.rightChild = removeResult.first + } + } + + return Pair(balanceTree(nodeCurrent), removeResult.second) + } + + /** + * Returns whether the specified node is red or not. + * + * @param node the node to check + * @return `true` if the node is red, `false` otherwise + */ + protected fun isRedColor(node: RBTreeNode?): Boolean { + if (node == null) return false + return node.isRed + } + + /** + * Returns whether the specified node is red or not. + * + * @param node the node to check color its left child + * @return `true` if left child of `node` is red, `false` otherwise + */ + protected fun isRedLeftChild(node: RBTreeNode?): Boolean { + if (node == null) return false + return isRedColor(node.leftChild) + } + + /** + * Rotates the binary tree node with the given root to the left. + * + * @param node the root of the binary tree node to rotate left + * @return if `node.rightChild` is null, returns `node`, + * otherwise `node` switches places with the right child + */ + protected fun rotateLeft(node: RBTreeNode): RBTreeNode { + val nodeSwapped: RBTreeNode = node.rightChild ?: return node + node.rightChild = nodeSwapped.leftChild + nodeSwapped.leftChild = node + + nodeSwapped.isRed = node.isRed + node.isRed = true + return nodeSwapped + } + + /** + * Rotates the binary tree node with the given root to the right. + * + * @param node the binary tree node to rotate right + * @return if `node.leftChild` is null, returns `node`, + * otherwise `node` switches places with the left child + */ + protected fun rotateRight(node: RBTreeNode): RBTreeNode { + val nodeSwapped: RBTreeNode = node.leftChild ?: return node + node.leftChild = nodeSwapped.rightChild + nodeSwapped.rightChild = node + + nodeSwapped.isRed = node.isRed + node.isRed = true + return nodeSwapped + } + + /** + * Flips the colors of the specified node and its children. + * + * @param node needed to flip the colors + */ + protected fun flipColors(node: RBTreeNode): Unit { + node.isRed = !node.isRed + notNullNodeUpdate(node.leftChild) { child -> child.isRed = !child.isRed } + notNullNodeUpdate(node.rightChild) { child -> child.isRed = !child.isRed } + } + + /** + * This function is used to move a red node to the right, if it has a red left child of its [node] left child. + * It first flips the colors of the node and its children, then rotates the tree if the left child is also red. + * + * @param node the node to move + * @return the new root of the tree, which is balanced node subtree + */ + protected fun moveRedRight(node: RBTreeNode): RBTreeNode { + if (node.rightChild == null) return node + var nodeCurrent = node + + flipColors(nodeCurrent) + if (isRedLeftChild(nodeCurrent.leftChild)) { + nodeCurrent = rotateRight(nodeCurrent) + flipColors(nodeCurrent) + } + return nodeCurrent + } + + /** + * This function is used to move a red node to the left, if it has a red right child of its [node] left child. + * It first flips the colors of the node and its children, then rotates the tree if the left child is also red. + * + * @param node the node to move + * @return the new root of the tree, which is balanced node subtree + */ + private fun moveRedLeft(node: RBTreeNode): RBTreeNode { + if (node.leftChild == null) return node + var nodeCurrent = node + + flipColors(nodeCurrent) + if (isRedLeftChild(nodeCurrent.rightChild)) { + nodeCurrent.rightChild = notNullNodeAction( + node.rightChild, null + ) { rightChild -> rotateRight(rightChild) } + nodeCurrent = rotateLeft(nodeCurrent) + flipColors(nodeCurrent) + } + return nodeCurrent + } + + /** + * Removes the node with the minimum key from the binary search tree. + * + * @param node the root of the binary search tree + * @return the root of the binary search tree with the node removed, or `null` if the tree is empty + */ + protected fun removeMinNode(node: RBTreeNode?): RBTreeNode? { + if (node == null) return null + val leftChild = node.leftChild ?: return node.rightChild + + var nodeCurrent = node + if (!isRedColor(leftChild) && !isRedLeftChild(leftChild)) + nodeCurrent = moveRedLeft(nodeCurrent) + + nodeCurrent.leftChild = removeMinNode(leftChild) + + return balanceTree(nodeCurrent) + } + +} \ No newline at end of file diff --git a/lib/src/main/kotlin/tree_tripper/iterators/BinarySearchTreeIterator.kt b/lib/src/main/kotlin/tree_tripper/iterators/BinarySearchTreeIterator.kt new file mode 100644 index 0000000..8fc15da --- /dev/null +++ b/lib/src/main/kotlin/tree_tripper/iterators/BinarySearchTreeIterator.kt @@ -0,0 +1,182 @@ +package tree_tripper.iterators + +import tree_tripper.nodes.binary_nodes.AbstractBSTreeNode +import java.util.LinkedList +import java.util.Queue + + +/** + * A generic binary search tree iterator that can iterate over a binary search tree in different orders. + * + * @param K the type of the keys in the binary search tree + * @param V the type of the values in the binary search tree + * @param N the type of the nodes in the binary search tree + */ +class BinarySearchTreeIterator, V, N : AbstractBSTreeNode>( + root: N?, + order: IterationOrders = IterationOrders.WIDTH_ORDER +) : Iterator> { + private val iterationState: IterationState + + init { + iterationState = when (order) { + IterationOrders.WIDTH_ORDER -> WidthIterationState(root) + IterationOrders.INCREASING_ORDER -> IncreasingIterationState(root) + IterationOrders.DECREASING_ORDER -> DecreasingIterationState(root) + } + } + + override fun hasNext(): Boolean { + return iterationState.hasNext() + } + + override fun next(): Pair { + return iterationState.next() + } + + /** + * An interface for the different iteration states of a binary search tree iterator. + */ + private interface IterationState, V, N : AbstractBSTreeNode> { + + /** + * Returns `true` if the iteration has more elements. + */ + abstract fun hasNext(): Boolean + + /** + * Returns `true` if the iteration has more elements. + * @throws: NoSuchElementException - if the iteration state has no next element. + */ + abstract fun next(): Pair + + } + + /** + * A concrete iteration state for a binary search tree iterator with the width iteration order. + */ + private class WidthIterationState, V, N : AbstractBSTreeNode>( + root: N? + ) : IterationState { + private val queue: Queue = LinkedList() + + init { + if (root != null) queue.add(root) + } + + override fun hasNext(): Boolean { + return (queue.size > 0) + } + + override fun next(): Pair { + if (!hasNext()) throw NoSuchElementException("Try get next element from the end of iterator state") + val nodeCurrent: N = queue.poll() + nodeCurrent.getChildren().forEach() { child -> + queue.add(child) + } + return Pair(nodeCurrent.key, nodeCurrent.value) + } + + } + + /** + * A concrete iteration state for a binary search tree iterator with the increasing iteration order. + */ + private class IncreasingIterationState, V, N : AbstractBSTreeNode>( + root: N? + ) : IterationState { + private val unprocessedNodesStack = LinkedList() + private val semiProcessedNodesStack = LinkedList() + + init { + if (root != null) unprocessedNodesStack.add(root) + } + + override fun hasNext(): Boolean { + return (hasUnprocessedNodes() || hasSemiProcessedNodes()) + } + + private fun hasUnprocessedNodes(): Boolean { + return unprocessedNodesStack.isNotEmpty() + } + + private fun hasSemiProcessedNodes(): Boolean { + return semiProcessedNodesStack.isNotEmpty() + } + + override fun next(): Pair { + if (!hasNext()) throw NoSuchElementException("Try get next element from the end of iterator state") + var nodeCurrent: N + + while (hasUnprocessedNodes()) { + nodeCurrent = unprocessedNodesStack.pollFirst() + semiProcessedNodesStack.addFirst(nodeCurrent) + if (nodeCurrent.leftChild != null) + unprocessedNodesStack.addFirst(nodeCurrent.leftChild) + else { + semiProcessedNodesStack.pollFirst() + if (nodeCurrent.rightChild != null) + unprocessedNodesStack.addFirst(nodeCurrent.rightChild) + return Pair(nodeCurrent.key, nodeCurrent.value) + } + } + + nodeCurrent = semiProcessedNodesStack.pollFirst() + if (nodeCurrent.rightChild != null) + unprocessedNodesStack.addFirst(nodeCurrent.rightChild) + return Pair(nodeCurrent.key, nodeCurrent.value) + } + + } + + /** + * A concrete iteration state for a binary search tree iterator with the decreasing iteration order. + */ + private class DecreasingIterationState, V, N : AbstractBSTreeNode>( + root: N? + ) : IterationState { + private val unprocessedNodesStack = LinkedList() + private val semiProcessedNodesStack = LinkedList() + + init { + if (root != null) unprocessedNodesStack.add(root) + } + + override fun hasNext(): Boolean { + return (hasUnprocessedNodes() || hasSemiProcessedNodes()) + } + + private fun hasSemiProcessedNodes(): Boolean { + return semiProcessedNodesStack.isNotEmpty() + } + + private fun hasUnprocessedNodes(): Boolean { + return unprocessedNodesStack.isNotEmpty() + } + + override fun next(): Pair { + if (!hasNext()) throw NoSuchElementException("Try get next element from the end of iterator state") + var nodeCurrent: N + + while (hasUnprocessedNodes()) { + nodeCurrent = unprocessedNodesStack.pollFirst() + semiProcessedNodesStack.addFirst(nodeCurrent) + if (nodeCurrent.rightChild != null) + unprocessedNodesStack.addFirst(nodeCurrent.rightChild) + else { + semiProcessedNodesStack.pollFirst() + if (nodeCurrent.leftChild != null) + unprocessedNodesStack.addFirst(nodeCurrent.leftChild) + return Pair(nodeCurrent.key, nodeCurrent.value) + } + } + + nodeCurrent = semiProcessedNodesStack.pollFirst() + if (nodeCurrent.leftChild != null) + unprocessedNodesStack.addFirst(nodeCurrent.leftChild) + return Pair(nodeCurrent.key, nodeCurrent.value) + } + + } + +} diff --git a/lib/src/main/kotlin/tree_tripper/iterators/IterationOrders.kt b/lib/src/main/kotlin/tree_tripper/iterators/IterationOrders.kt new file mode 100644 index 0000000..7bcec24 --- /dev/null +++ b/lib/src/main/kotlin/tree_tripper/iterators/IterationOrders.kt @@ -0,0 +1,15 @@ +package tree_tripper.iterators + + +/** + * An enumeration of the possible orders in which to iterate over the elements of a tree. + * + * @property WIDTH_ORDER iterate over the elements in order of their width in the tree + * @property INCREASING_ORDER iterate over the elements in order of their increasing depth in the tree + * @property DECREASING_ORDER iterate over the elements in order of their decreasing depth in the tree + */ +enum class IterationOrders { + WIDTH_ORDER, + INCREASING_ORDER, + DECREASING_ORDER, +} diff --git a/lib/src/main/kotlin/tree_tripper/nodes/SearchTreeNode.kt b/lib/src/main/kotlin/tree_tripper/nodes/SearchTreeNode.kt new file mode 100644 index 0000000..78fb845 --- /dev/null +++ b/lib/src/main/kotlin/tree_tripper/nodes/SearchTreeNode.kt @@ -0,0 +1,28 @@ +package tree_tripper.nodes + + +/** + * The interface represents a node of a search tree. + * + * @param K the key type of node, supporting the [Comparable] interface + * @param V the value type of node + * @param N the node type + */ +public interface SearchTreeNode, V, N: SearchTreeNode> { + + /** + * Returns a [N] list of not null children of a node. + */ + public fun getChildren(): List + + /** + * Returns a string with a transformed to the simple view node. + */ + public fun toStringSimpleView(): String + + /** + * Transforms a node to the [builder] of the subtree structure with a some [indent]. + */ + public fun toStringWithSubtreeView(indent: Int, builder: StringBuilder): Unit + +} diff --git a/lib/src/main/kotlin/tree_tripper/nodes/Utils.kt b/lib/src/main/kotlin/tree_tripper/nodes/Utils.kt new file mode 100644 index 0000000..2fe421f --- /dev/null +++ b/lib/src/main/kotlin/tree_tripper/nodes/Utils.kt @@ -0,0 +1,20 @@ +package tree_tripper.nodes + + +/** + * Checks the search tree [node] for null and if it is not null then executes the [action] + * + * @return the result of applying the given [action] function to the given [node], or the [nullNodeResult] if the [node] is null. + */ +public fun , R> notNullNodeAction(node: N?, nullNodeResult: R, action: (N) -> (R)): R { + if (node == null) return nullNodeResult + return action(node) +} + +/** + * Checks the search tree [node] for null and if it is not null then executes the [updateAction] + */ +public fun > notNullNodeUpdate(node: N?, updateAction: (N) -> (Unit)): Unit { + if (node == null) return + return updateAction(node) +} diff --git a/lib/src/main/kotlin/tree_tripper/nodes/binary_nodes/AVLTreeNode.kt b/lib/src/main/kotlin/tree_tripper/nodes/binary_nodes/AVLTreeNode.kt new file mode 100644 index 0000000..7e70f93 --- /dev/null +++ b/lib/src/main/kotlin/tree_tripper/nodes/binary_nodes/AVLTreeNode.kt @@ -0,0 +1,39 @@ +package tree_tripper.nodes.binary_nodes + +import kotlin.math.max + +/** + * AVLTreeNode class represents a node in an AVL tree, containing a key-value pair and additional + * information for maintaining the AVL property such as the height of the node. + * + * @param K the key type that implements the Comparable interface + * @param V the value type associated with the key + * @property height the height of the [AVLTreeNode] in the AVL tree + */ +public class AVLTreeNode, V>( + key: K, + value: V +): AbstractBSTreeNode>(key, value) { + + public constructor( + key: K, value: V, height: Int, + leftChild: AVLTreeNode?, + rightChild: AVLTreeNode? + ) : this(key, value) { + this.height = height + this.leftChild = leftChild + this.rightChild = rightChild + } + + /** The height of the node in the AVL tree, initialized to 1. */ + public var height: Int = 1 + private set + + /** Updates height of the node in AVL tree based on the heights of its left and right child subtrees. */ + public fun updateHeight() { + val leftHeight = this.leftChild?.height ?: 0 + val rightHeight = this.rightChild?.height ?: 0 + height = (max(leftHeight, rightHeight) + 1) + } + +} diff --git a/lib/src/main/kotlin/tree_tripper/nodes/binary_nodes/AbstractBSTreeNode.kt b/lib/src/main/kotlin/tree_tripper/nodes/binary_nodes/AbstractBSTreeNode.kt new file mode 100644 index 0000000..8707bef --- /dev/null +++ b/lib/src/main/kotlin/tree_tripper/nodes/binary_nodes/AbstractBSTreeNode.kt @@ -0,0 +1,40 @@ +package tree_tripper.nodes.binary_nodes + +import tree_tripper.nodes.SearchTreeNode +import tree_tripper.nodes.notNullNodeAction + + +/** + * The class represents a node of the abstract binary search tree, + * from which nodes of binary search trees are inherited. + * + * @param K the [key] type of node, supporting the [Comparable] interface + * @param V the [value] type of node + * @param N the node type + */ +public abstract class AbstractBSTreeNode, V, N: AbstractBSTreeNode>( + public val key: K, + public var value: V +): SearchTreeNode { + public var leftChild: N? = null + public var rightChild: N? = null + + override fun getChildren(): List { + return listOfNotNull(leftChild, rightChild) + } + + override fun toString(): String { + return "${this.javaClass.simpleName}(key=$key, value=$value)" + } + + override fun toStringSimpleView(): String { + return "($key: $value)" + } + + override fun toStringWithSubtreeView(indent: Int, builder: StringBuilder) { + notNullNodeAction(this.rightChild, Unit) {node -> node.toStringWithSubtreeView(indent + 1, builder)} + builder.append("\t".repeat(indent)).append(this.toStringSimpleView()).append("\n") + notNullNodeAction(this.leftChild, Unit) {node -> node.toStringWithSubtreeView(indent + 1, builder)} + } + +} diff --git a/lib/src/main/kotlin/tree_tripper/nodes/binary_nodes/BSTreeNode.kt b/lib/src/main/kotlin/tree_tripper/nodes/binary_nodes/BSTreeNode.kt new file mode 100644 index 0000000..601102b --- /dev/null +++ b/lib/src/main/kotlin/tree_tripper/nodes/binary_nodes/BSTreeNode.kt @@ -0,0 +1,22 @@ +package tree_tripper.nodes.binary_nodes + + +/** + * The class represents a node of the binary search tree. + * + * @param K the key type of the node, supporting the [Comparable] interface + * @param V the value type of the node + */ +public class BSTreeNode, V>( + key: K, + value: V +) : AbstractBSTreeNode>(key, value) { + + public constructor(key: K, value: V, leftChild: BSTreeNode?, rightChild: BSTreeNode?) : this( + key, + value + ) { + this.leftChild = leftChild + this.rightChild = rightChild + } +} diff --git a/lib/src/main/kotlin/tree_tripper/nodes/binary_nodes/RBTreeNode.kt b/lib/src/main/kotlin/tree_tripper/nodes/binary_nodes/RBTreeNode.kt new file mode 100644 index 0000000..f76dba0 --- /dev/null +++ b/lib/src/main/kotlin/tree_tripper/nodes/binary_nodes/RBTreeNode.kt @@ -0,0 +1,40 @@ +package tree_tripper.nodes.binary_nodes + + +/** + * A red-black tree node. + * + * @param K the key type + * @param V the value type + */ +public class RBTreeNode, V>( + key: K, + value: V +) : AbstractBSTreeNode>(key, value) { + var isRed: Boolean = true + + public constructor(key: K, value: V, isRed: Boolean) : this(key, value) { + this.isRed = isRed + } + + public constructor( + key: K, value: V, isRed: Boolean, + leftChild: RBTreeNode?, + rightChild: RBTreeNode? + ) : this(key, value, isRed) { + this.leftChild = leftChild + this.rightChild = rightChild + } + + override fun toStringSimpleView(): String { + return "${super.toStringSimpleView()} - ${colorName()}" + } + + /** + * Returns the color name of this node. + */ + private fun colorName(): String { + return if (isRed) "RED" else "BLACK" + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/AssertionUtils.kt b/lib/src/test/kotlin/AssertionUtils.kt new file mode 100644 index 0000000..2109199 --- /dev/null +++ b/lib/src/test/kotlin/AssertionUtils.kt @@ -0,0 +1,37 @@ +import org.junit.jupiter.api.Assertions +import tree_tripper.nodes.binary_nodes.AbstractBSTreeNode + + +public fun > assertBinaryNodeDataEquals(nodeFirst: N?, nodeSecond: N?): Unit { + assertBinaryNodeDataEquals(nodeFirst, nodeSecond) { _, _: N -> true } +} + +public fun > assertBinaryNodeDataEquals( + nodeFirst: N?, + nodeSecond: N?, + assertAction: (N, N) -> (Boolean) +): Unit { + if (nodeFirst == null) return Assertions.assertNull(nodeSecond) { "Second node is not null." } + if (nodeSecond == null) return Assertions.assertNull(nodeFirst) { "First node is not null." } + + Assertions.assertEquals(nodeFirst.key, nodeSecond.key) { "Keys are not equal." } + Assertions.assertEquals(nodeFirst.value, nodeSecond.value) { "Values are not equal." } + Assertions.assertTrue(assertAction(nodeFirst, nodeSecond)) { "Action assertion is invalid equals" } +} + +public fun > assertBinaryNodeDeepEquals(nodeFirst: N?, nodeSecond: N?): Unit { + assertBinaryNodeDeepEquals(nodeFirst, nodeSecond) { _, _: N? -> true } +} + +public fun > assertBinaryNodeDeepEquals( + nodeFirst: N?, + nodeSecond: N?, + assertAction: (N, N) -> (Boolean) +): Unit { + if (nodeFirst == null) return Assertions.assertNull(nodeSecond) { "Second node is not null." } + if (nodeSecond == null) return Assertions.assertNull(nodeFirst) { "First node is not null." } + + assertBinaryNodeDataEquals(nodeFirst, nodeSecond, assertAction) + assertBinaryNodeDeepEquals(nodeFirst.leftChild, nodeSecond.leftChild, assertAction) + assertBinaryNodeDeepEquals(nodeFirst.rightChild, nodeSecond.rightChild, assertAction) +} diff --git a/lib/src/test/kotlin/tree_tripper/binary_trees/AVLTreeTest.kt b/lib/src/test/kotlin/tree_tripper/binary_trees/AVLTreeTest.kt new file mode 100644 index 0000000..5296d7c --- /dev/null +++ b/lib/src/test/kotlin/tree_tripper/binary_trees/AVLTreeTest.kt @@ -0,0 +1,408 @@ +package tree_tripper.binary_trees + +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import tree_tripper.binary_trees.assistants.AVLTreeTestAssistant +import tree_tripper.nodes.binary_nodes.AVLTreeNode + + +class AVLTreeTest { + private lateinit var tree: AVLTreeTestAssistant + + @BeforeEach + public fun setup() { + tree = AVLTreeTestAssistant() + } + + @Test + @DisplayName("tree initialization") + public fun testTreeInitialization() { + tree.assertRoot(null) { "Root of AVLTree is not null by standard initialize." } + Assertions.assertEquals(0, tree.size) + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testNodeCreationCases") + @DisplayName("node creation") + public fun testNodeCreation(key: Int, value: Int) { + tree.assertNodeCreation(key, value) + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testBalanceTreeCases") + @DisplayName("check balance tree") + public fun testBalanceTree(expected: AVLTreeNode, node: AVLTreeNode) { + tree.assertBalanceTree(expected, node) + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("checkBalanceFactor") + @DisplayName("balance factor") + public fun checkBalanceFactor(expected: Int, node: AVLTreeNode?) { + tree.assertBalanceFactor(expected, node) + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testBalanceCases") + @DisplayName("balance case") + public fun testBalanceCase(expected: AVLTreeNode, node: AVLTreeNode) { + tree.assertBalance(expected, node) + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testNodeRotateLeftCases") + @DisplayName("node rotate left case") + public fun testNodeRotateLeftCases(expected: AVLTreeNode, node: AVLTreeNode) { + tree.assertNodeLeftRotation(expected, node) + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testNodeRotateRightCases") + @DisplayName("node rotate right case") + public fun testNodeRotateRightCase(expected: AVLTreeNode, node: AVLTreeNode) { + tree.assertNodeRightRotation(expected, node) + } + + companion object { + + @JvmStatic + public fun testNodeCreationCases(): List = listOf( + Arguments.of(0, 0), + Arguments.of(1, 1), + Arguments.of(-1, -1) + ) + + @JvmStatic + public fun testBalanceTreeCases(): List = listOf( + + //Does not require balance + Arguments.of( + //Expected + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ), + //Testing + AVLTreeNode( + 1, 1, 1, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ) + ), + + //Simple left rotation + Arguments.of( + //Expected + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ), + //Testing + AVLTreeNode( + 0, 0, 1, + null, + AVLTreeNode( + 1, 1, 2, + null, + AVLTreeNode(2, 2, 1, null, null) + ) + ) + ), + + //Simple right rotation + Arguments.of( + //Expected + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ), + //Testing + AVLTreeNode( + 2, 2, 1, + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + null + ), + null + ) + ) + + ) + + @JvmStatic + public fun checkBalanceFactor(): List = listOf( + + Arguments.of(0, null), + + Arguments.of(0, + AVLTreeNode(0, 0, 1, null, null) + ), + + Arguments.of(1, + AVLTreeNode( + 0, 0, 2, + null, + AVLTreeNode(0, 0, 1, null, null) + ) + ), + + Arguments.of(-1, + AVLTreeNode( + 0, 0, 2, + AVLTreeNode(0, 0, 1, null, null), + null + ) + ), + + Arguments.of(0, + AVLTreeNode( + 0, 0, 2, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(0, 0, 1, null, null) + ) + ) + + ) + + @JvmStatic + public fun testBalanceCases(): List = listOf( + + //Does not require balance + Arguments.of( + //Expected + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ), + //Testing + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ) + ), + + //Simple left rotation + Arguments.of( + //Expected + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ), + //Testing + AVLTreeNode( + 0, 0, 3, + null, + AVLTreeNode( + 1, 1, 2, + null, + AVLTreeNode(2, 2, 1, null, null) + ) + ) + ), + + //Simple right rotation + Arguments.of( + //Expected + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ), + //Testing + AVLTreeNode( + 2, 2, 3, + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + null + ), + null + ) + ), + + //Simple left right rotation + Arguments.of( + //Expected + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ), + //Testing + AVLTreeNode( + 2, 2, 3, + AVLTreeNode( + 0, 0, 2, + null, + AVLTreeNode(1, 1, 1, null, null) + ), + null + ) + ), + + //Simple right left rotation + Arguments.of( + //Expected + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ), + //Testing + AVLTreeNode( + 0, 0, 3, + null, + AVLTreeNode( + 2, 2, 2, + AVLTreeNode(1, 1, 1, null, null), + null + ) + ) + ) + + ) + + @JvmStatic + public fun testNodeRotateLeftCases(): List = listOf( + + //Null check + Arguments.of( + //Expected + AVLTreeNode(0, 0, 1, null, null), + //Testing + AVLTreeNode(0, 0, 1, null, null) + ), + + //Simple left rotation + Arguments.of( + //Expected + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ), + //Testing + AVLTreeNode( + 0, 0, 3, + null, + AVLTreeNode( + 1, 1, 2, + null, + AVLTreeNode(2, 2, 1, null, null) + ) + ) + ), + + //Left rotation with children + Arguments.of( + //Expected + AVLTreeNode( + 3, 3, 3, + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ), + AVLTreeNode( + 4, 4, 2, + null, + AVLTreeNode(5, 5, 1, null, null) + ) + ), + //Testing + AVLTreeNode( + 1, 1, 4, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode( + 3, 3, 3, + AVLTreeNode(2, 2, 1, null, null), + AVLTreeNode( + 4, 4, 2, + null, + AVLTreeNode(5, 5, 1, null, null) + ) + ) + ) + ) + + ) + + @JvmStatic + public fun testNodeRotateRightCases(): List = listOf( + + //Null check + Arguments.of( + //Expected + AVLTreeNode(0, 0, 1, null, null), + //Testing + AVLTreeNode(0, 0, 1, null, null) + ), + + //Simple right rotation + Arguments.of( + //Expected + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ), + //Testing + AVLTreeNode( + 2, 2, 3, + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + null + ), + null + ) + ), + + //Right rotation with children + Arguments.of( + //Expected + AVLTreeNode( + 2, 2, 3, + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + null + ), + AVLTreeNode( + 4, 4, 2, + AVLTreeNode(3, 3, 1, null, null), + AVLTreeNode(5, 5, 1, null, null) + ) + ), + //Testing + AVLTreeNode( + 4, 4, 4, + AVLTreeNode( + 2, 2, 3, + AVLTreeNode( + 1, 1, 2, + AVLTreeNode(0, 0, 1, null, null), + null + ), + AVLTreeNode(3, 3, 1, null, null) + ), + AVLTreeNode(5, 5, 1, null, null) + ) + ) + + ) + + } +} diff --git a/lib/src/test/kotlin/tree_tripper/binary_trees/BSTreeTest.kt b/lib/src/test/kotlin/tree_tripper/binary_trees/BSTreeTest.kt new file mode 100644 index 0000000..226b109 --- /dev/null +++ b/lib/src/test/kotlin/tree_tripper/binary_trees/BSTreeTest.kt @@ -0,0 +1,388 @@ +package tree_tripper.binary_trees + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.assertTimeout +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import tree_tripper.binary_trees.assistants.BSTreeTestAssistant +import tree_tripper.iterators.IterationOrders +import java.time.Duration +import kotlin.random.Random +import kotlin.test.Test +import kotlin.test.assertEquals + + +public class BSTreeTest { + private lateinit var tree: BSTreeTestAssistant + + @BeforeEach + public fun setup() { + tree = BSTreeTestAssistant() + } + + @Test + @DisplayName("tree initialization") + public fun testTreeInitialization() { + tree.assertNullRoot() + Assertions.assertEquals(tree.size, 0, "Incorrect a tree initialization.") + } + + @Test + @DisplayName("create node") + public fun testCreateNode() { + tree.assertWasCreatedNode(1, -1) + } + + @Test + @DisplayName("update root") + public fun testUpdateRoot() { + tree.assertWasUpdatedRoot(1, -1) + } + + @Test + @DisplayName("balance tree") + public fun testBalanceTree() { + tree.assertWasBalancedTree(1, -1) + } + + @Test + @DisplayName("insert root") + public fun testInsertRoot() { + tree.insert(1, -1) + Assertions.assertEquals(tree.size, 1, "Incorrect resizing tree size.") + Assertions.assertEquals(tree.getRoot(), Pair(1, -1), "Incorrect insert root") + + tree.insert(1, 0) + Assertions.assertEquals(tree.size, 1, "Incorrect resizing tree size.") + Assertions.assertEquals(tree.getRoot(), Pair(1, 0), "Incorrect change root") + } + + @Test() + @DisplayName("insert children root") + public fun testInsertChildrenRoot() { + tree.insert(2, -2) + tree.insert(1, -1) + tree.insert(3, -3) + tree.assertIsBSTree() + Assertions.assertEquals(tree.size, 3, "Incorrect resizing tree size.") + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("getSizeAndTimeArguments") + @DisplayName("insert with size and time") + public fun testInsertWithSizeAndTime(size: Int, seconds: Long) { + assertTimeout(Duration.ofSeconds(seconds)) { + repeat(size) { + val keyRandom = Random.nextInt(-1000, 1000) + tree.insert(keyRandom, keyRandom) + } + } + + tree.assertIsBSTree() + } + + @Test + @DisplayName("if absent insert root") + public fun testInsertIfAbsentRoot() { + Assertions.assertEquals(tree.insertIfAbsent(1, -1), true) + Assertions.assertEquals(tree.size, 1, "Incorrect resizing tree size.") + Assertions.assertEquals(tree.getRoot(), Pair(1, -1), "Incorrect insert root") + + Assertions.assertEquals(tree.insertIfAbsent(1, 1), false) + Assertions.assertEquals(tree.size, 1, "Incorrect resizing tree size.") + Assertions.assertEquals(tree.getRoot(), Pair(1, -1), "Incorrect change root") + } + + @Test + @DisplayName("search root") + public fun testSearchNode() { + Assertions.assertEquals(tree.search(0), null, "Incorrect search in a empty tree.") + + tree.insert(1, -1) + Assertions.assertEquals(tree.search(1), -1, "Incorrect search an existent root.") + } + + @Test + @DisplayName("search children root") + public fun testSearchChildrenNode() { + tree.insert(2, -2) + tree.insert(1, -1) + tree.insert(3, -3) + Assertions.assertEquals(tree.search(1), -1, "Incorrect search an existent child root.") + Assertions.assertEquals(tree.search(3), -3, "Incorrect search an existent child root.") + Assertions.assertEquals(tree.search(0), null, "Incorrect search a non-existent child root.") + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("getSizeAndTimeArguments") + @DisplayName("search with size and time") + public fun testSearchWithSizeAndTime(size: Int, seconds: Long) { + val arrayKeys = IntArray(size) + var index = 0 + repeat(size) { + val keyRandom = Random.nextInt(-1000, 1000) + arrayKeys[index++] = keyRandom + tree.insert(keyRandom, keyRandom * (-1)) + } + + assertTimeout(Duration.ofSeconds(seconds)) { + repeat(10) { + val keyRandom = arrayKeys[Random.nextInt(0, size - 1)] + Assertions.assertEquals(tree.search(keyRandom), (-1) * keyRandom, + "Incorrect search an existent node.") + + if ((keyRandom - 10) !in arrayKeys) + Assertions.assertEquals(tree.search(keyRandom - 10), null, + "Incorrect search a non-existent node.") + } + } + + tree.assertIsBSTree() + } + + @Test + @DisplayName("search of default root") + public fun testSearchOrDefault() { + Assertions.assertEquals(tree.searchOrDefault(1, 0), 0, + "Incorrect return of search a non-existent child root.") + + tree.insert(1, -1) + Assertions.assertEquals(tree.searchOrDefault(1, 0), -1, + "Incorrect return of search an existent child root.") + } + + @Test + @DisplayName("contains") + public fun testContains() { + Assertions.assertEquals(tree.contains(1), false, "Incorrect return of search a non-existent node.") + + tree.insert(1, -1) + Assertions.assertEquals(tree.contains(1), true, "Incorrect return of search an existent node.") + } + + @Test + @DisplayName("set with brackets") + public fun testSet() { + tree[1] = -1 + Assertions.assertEquals(tree.getRoot(), Pair(1, -1), "Incorrect set.") + + tree[1] = 0 + Assertions.assertEquals(tree.getRoot(), Pair(1, 0), "Incorrect change of the value.") + } + + @Test + @DisplayName("get with brackets") + public fun testGet() { + Assertions.assertEquals(tree[1], null, "Incorrect get a non-existent node.") + + tree[1] = -1 + Assertions.assertEquals(tree[1], -1, "Incorrect get an existent node.") + } + + @Test + @DisplayName("get maximum in subtree without children") + public fun testGetMaxInSubtree() { + Assertions.assertEquals(tree.getMaxInSubtree(0), null, + "Incorrect search a maximum key in a empty tree.") + + tree[1] = -1 + Assertions.assertEquals(tree.getMaxInSubtree(1), Pair(1, -1), + "Incorrect search a maximum key in subtree without children.") + Assertions.assertEquals(tree.getRoot(), Pair(1, -1), "A tree is damaged.") + } + + @Test + @DisplayName("get maximum in subtree with children") + public fun testGetMaxInSubtreeWithChildren() { + tree[5] = -5 + tree[1] = -1 + tree[3] = -3 + tree[2] = -2 + Assertions.assertEquals(tree.getMaxInSubtree(1), Pair(3, -3), + "Incorrect search a maximum key in subtree with children.") + tree.assertIsBSTree() + } + + @Test + @DisplayName("get maximum") + public fun testGetMax() { + Assertions.assertEquals(tree.getMax(), null, "Incorrect search a maximum key in a empty tree.") + + tree[2] = -2 + tree[3] = -3 + tree[1] = -1 + tree[4] = -4 + Assertions.assertEquals(tree.getMax(), Pair(4, -4), "Incorrect search a maximum key in a tree.") + } + + @Test + @DisplayName("get minimum in subtree without children") + public fun testGetMinInSubtree() { + Assertions.assertEquals(tree.getMinInSubtree(0), null, + "Incorrect search a minimum key in a empty tree.") + + tree[1] = -1 + Assertions.assertEquals(tree.getMinInSubtree(1), Pair(1, -1), + "Incorrect search a minimum key in subtree without children.") + Assertions.assertEquals(tree.getRoot(), Pair(1, -1), "A tree is damaged.") + } + + @Test + @DisplayName("get minimum in subtree with children") + public fun testGetMinInSubtreeWithChildren() { + tree[5] = -5 + tree[1] = -1 + tree[3] = -3 + tree[2] = -2 + Assertions.assertEquals(tree.getMinInSubtree(3), Pair(2, -2), + "Incorrect search a minimum key in a subtree.") + tree.assertIsBSTree() + } + + @Test + @DisplayName("get minimum") + public fun testGetMin() { + Assertions.assertEquals(tree.getMin(), null, "Incorrect search a minimum key in a empty tree.") + + tree[2] = -2 + tree[3] = -3 + tree[1] = -1 + tree[4] = -4 + Assertions.assertEquals(tree.getMin(), Pair(1, -1), "Incorrect search a minimum key in a tree.") + } + + @Test + @DisplayName("remove root without children") + public fun testRemove() { + tree[1] = -1 + Assertions.assertEquals(tree.remove(1), -1, "Incorrect remove root.") + Assertions.assertEquals(0, tree.size) + tree.assertNullRoot() + + Assertions.assertEquals(tree.remove(1), null, "Incorrect remove a non-existent root.") + Assertions.assertEquals(0, tree.size) + } + + @Test + @DisplayName("remove root with children") + public fun testRemoveWithChildren() { + tree[2] = -2 + tree[1] = -1 + tree[3] = -3 + Assertions.assertEquals(tree.remove(2), -2, "Incorrect remove a root.") + Assertions.assertEquals(2, tree.size) + tree.assertIsBSTree() + + Assertions.assertEquals(tree.search(1), -1, "Incorrect remove a root and lose the left child.") + Assertions.assertEquals(tree.search(3), -3, "Incorrect remove a root and lose the right child.") + + Assertions.assertEquals(tree.remove(1), -1, "Incorrect remove a root.") + tree.assertIsBSTree() + Assertions.assertEquals(1, tree.size) + + Assertions.assertEquals(tree.search(3), -3, "Incorrect remove a root and lose the right child.") + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("getSizeAndTimeArguments") + @DisplayName("remove with size and time") + public fun testRemoveWithSizeAndTime(size: Int, seconds: Long) { + val setKeys: MutableSet = mutableSetOf() + repeat(size) { + val keyRandom = Random.nextInt(-1000, 1000) + setKeys.add(keyRandom) + tree[keyRandom] = (-1) * keyRandom + } + + assertTimeout(Duration.ofSeconds(seconds)) { + repeat(10) { + val keyRandom = setKeys.elementAt(Random.nextInt(0, setKeys.size - 1)) + Assertions.assertEquals(tree.remove(keyRandom), (-1) * keyRandom, + "Incorrect return of remove an existent node.") + setKeys.remove(keyRandom) + + if ((keyRandom - 10) !in setKeys) + Assertions.assertEquals(tree.remove(keyRandom - 10), null, + "Incorrect return of remove a non-existent node.") + } + } + + Assertions.assertEquals(tree.size, setKeys.size) + tree.assertIsBSTree() + } + + @Test + @DisplayName("remove or default") + public fun testRemoveOrDefault() { + Assertions.assertEquals(tree.removeOrDefault(1, 0), 0, + "Incorrect return of remove a non-existent node.") + Assertions.assertEquals(0, tree.size) + + tree.insert(1, -1) + Assertions.assertEquals(tree.removeOrDefault(1, 0), -1, + "Incorrect return of remove an existent node.") + Assertions.assertEquals(0, tree.size) + } + + @Test + @DisplayName("iterator") + public fun testIterator() { + Assertions.assertFalse(tree.iterator().hasNext(), "Incorrect check next.") + } + + @Test + @DisplayName("for each") + public fun testForEach() { + tree[2] = -2 + tree[1] = -1 + tree[3] = -3 + tree[4] = -4 + val arrayPair = arrayOf(Pair(2, -2), Pair(1, -1), Pair(3, -3), Pair(4, -4)) + var index = 0 + tree.forEach(IterationOrders.WIDTH_ORDER) { + Assertions.assertEquals(arrayPair[index++], it, "Incorrect iteration.") + } + } + + @Test + @DisplayName("tree to string") + public fun testToString() { + var builder = StringBuilder("BSTreeTestAssistant(") + tree.forEach { builder.append("${it.first}: ${it.second}, ") } + builder.append(')') + Assertions.assertEquals(tree.toString(), builder.toString(), "Incorrect construction string.") + + builder = StringBuilder("BSTreeTestAssistant(") + tree[2] = -2 + tree[1] = -1 + tree[3] = -3 + tree[4] = -4 + tree.forEach { builder.append("${it.first}: ${it.second}, ") } + repeat(2) { builder.deleteCharAt(builder.length - 1) } + builder.append(')') + Assertions.assertEquals(tree.toString(), builder.toString(), "Incorrect construction string.") + } + + @Test + @DisplayName("tree to string with tree view") + public fun testToStringWithTreeView() { + tree[2] = -2 + tree[1] = -1 + tree[3] = -3 + tree[4] = -4 + val string = "BSTreeTestAssistant(\n\t\t(4: -4)\n\t(3: -3)\n(2: -2)\n\t(1: -1)\n)" + Assertions.assertEquals(tree.toStringWithTreeView(), string, "Incorrect construction string.") + } + + public companion object { + @JvmStatic + fun getSizeAndTimeArguments() = listOf( + Arguments.of(100, 1L), + Arguments.of(10000, 1L) + ) + } + +} diff --git a/lib/src/test/kotlin/tree_tripper/binary_trees/RBTreeTest.kt b/lib/src/test/kotlin/tree_tripper/binary_trees/RBTreeTest.kt new file mode 100644 index 0000000..f553378 --- /dev/null +++ b/lib/src/test/kotlin/tree_tripper/binary_trees/RBTreeTest.kt @@ -0,0 +1,483 @@ +package tree_tripper.binary_trees + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import tree_tripper.binary_trees.assistants.RBTreeTestAssistant +import tree_tripper.nodes.binary_nodes.RBTreeNode +import kotlin.random.Random +import kotlin.random.nextInt + + +class RBTreeTest { + private lateinit var tree: RBTreeTestAssistant + private val randomizer = Random(42) + + @BeforeEach + public fun setup() { + tree = RBTreeTestAssistant() + } + + @Test + @DisplayName("is root after initializing equals null") + public fun testInitializing() { + tree.assertRoot(null) { "Root is not null after init" } + tree.assertIsRBTree() + Assertions.assertEquals(0, tree.size) + } + + @Test + @DisplayName("insert sorted elements") + public fun testInsertSortedElements() { + for (i in 0..20) { + tree.insert(i, i) + tree.assertIsRBTree() + Assertions.assertEquals(i + 1, tree.size) + } + + } + + @Test + @DisplayName("insert reversed sorted elements") + public fun testInsertReverseSortedElements() { + for (i in 20 downTo 0) { + tree.insert(i, i) + tree.assertIsRBTree() + Assertions.assertEquals((20 - i) + 1, tree.size) + } + + } + + @Test + @DisplayName("insert a lot of unsorted elements") + public fun testInsertUnsortedElements() { + val elements: MutableSet = mutableSetOf() + for (i in 0..256) { + val value = randomizer.nextInt() + tree.insert(value, value) + elements.add(value) + tree.assertIsRBTree() + Assertions.assertEquals(elements.size, tree.size) + } + } + + @Test + @DisplayName("remove not contains element") + public fun testRemoveNotContainsElement() { + for (i in 0..20) tree.insert(i, i) + Assertions.assertEquals(null, tree.remove(25)) + tree.assertIsRBTree() + Assertions.assertEquals(21, tree.size) + + Assertions.assertEquals(null, tree.remove(-100)) + tree.assertIsRBTree() + Assertions.assertEquals(21, tree.size) + } + + @Test + @DisplayName("remove root with children") + public fun testRemoveRootWithChildren() { + for (i in 0..20) tree.insert(i, i) + val root: Pair = tree.getRoot() + Assertions.assertEquals(root.second, tree.remove(root.first)) + tree.assertIsRBTree() + Assertions.assertEquals(20, tree.size) + } + + @Test + @DisplayName("remove black node with children") + public fun testRemoveBlackNodeWithChildren() { + for (i in 0..20) tree.insert(i, i) + Assertions.assertEquals(15, tree.remove(15)) + tree.assertIsRBTree() + Assertions.assertEquals(20, tree.size) + } + + @Test + @DisplayName("remove red node with children") + public fun testRemoveRedNodeWithChildren() { + for (i in 0..20) tree.insert(i, i) + Assertions.assertEquals(1, tree.remove(1)) + tree.assertIsRBTree() + Assertions.assertEquals(20, tree.size) + } + + @Test + @DisplayName("remove random node") + public fun testRemoveRandomNodeChildren() { + for (i in 0..20) tree.insert(i, i) + val key = randomizer.nextInt(0..20) + try { + Assertions.assertEquals(key, tree.remove(key)) + tree.assertIsRBTree() + Assertions.assertEquals(20, tree.size) + } catch (e: AssertionError) { + throw AssertionError( + "Try remove node with key $key from tree: ${tree.toStringWithTreeView()}", + e + ) + } + } + + @Test + @DisplayName("remove root without children") + public fun testRemoveRootWithoutChildren() { + tree.insert(0, 0) + Assertions.assertEquals(0, tree.remove(0)) + Assertions.assertEquals(0, tree.size) + + } + + @Test + @DisplayName("remove black node without children") + public fun testRemoveBlackNodeWithoutChildren() { + for (i in 0..20) tree.insert(i, i) + Assertions.assertEquals(6, tree.remove(6)) + tree.assertIsRBTree() + Assertions.assertEquals(20, tree.size) + } + + @Test + @DisplayName("remove red node without children") + public fun testRemoveRedNodeWithoutChildren() { + for (i in 0..21) tree.insert(i, i) + Assertions.assertEquals(20, tree.remove(20)) + tree.assertIsRBTree() + Assertions.assertEquals(21, tree.size) + } + + @Test + @DisplayName("remove root with one left child") + public fun testRemoveRootWithOneLeftChild() { + tree.insert(0, 0) + tree.insert(-1, -1) + Assertions.assertEquals(0, tree.remove(0)) + Assertions.assertEquals(1, tree.size) + Assertions.assertEquals(Pair(-1, -1), tree.getRoot()) + } + + @Test + @DisplayName("remove min node at empty tree") + public fun testRemoveMinNodeAtEmptyTree() { + tree.assertRemoveMinNode( + treeView = null, + expected = null + ) + } + + @Test + @DisplayName("remove min node without children") + public fun testRemoveMinNodeWithoutChildren() { + tree.assertRemoveMinNode( + treeView = RBTreeNode(1, 1, isRed = false), + expected = null + ) + tree.assertRemoveMinNode( + treeView = RBTreeNode(1, 1, isRed = true), + expected = null + ) + } + + @Test + @DisplayName("remove min node with red child") + public fun testRemoveMinNodeWithRedChild() { + tree.assertRemoveMinNode( + treeView = RBTreeNode( + 1, 1, isRed = false, + leftChild = RBTreeNode(0, 0, isRed = true), + rightChild = null + ), + expected = RBTreeNode(1, 1, isRed = false) + ) + } + + @Test + @DisplayName("move right node without children") + public fun testModeRightNodeWithoutChildren() { + tree.assertMoveRightNode( + treeView = RBTreeNode(0, 0, isRed = false), + expected = RBTreeNode(0, 0, isRed = false), + ) + } + + @Test + @DisplayName("move right node with red child") + public fun testModeRightNodeWithRedChild() { + tree.assertMoveRightNode( + treeView = RBTreeNode( + 0, 0, isRed = false, + leftChild = RBTreeNode(-1, -1, isRed = true), + rightChild = null + ), + expected = RBTreeNode( + 0, 0, isRed = false, + leftChild = RBTreeNode(-1, -1, isRed = true), + rightChild = null + ), + ) + } + + @Test + @DisplayName("move right node with children") + public fun testModeRightNodeWithChildren() { + tree.assertMoveRightNode( + treeView = RBTreeNode( + 0, 0, isRed = false, + leftChild = RBTreeNode(-1, -1, isRed = false), + rightChild = RBTreeNode(1, 1, isRed = false) + ), + expected = RBTreeNode( + 0, 0, isRed = true, + leftChild = RBTreeNode(-1, -1, isRed = true), + rightChild = RBTreeNode(1, 1, isRed = true) + ), + ) + } + + @Test + @DisplayName("move right node with children and left child of left child is red") + public fun testModeRightNodeWithChildrenAndLeftChildOfLeftChildIsRed() { + tree.assertMoveRightNode( + treeView = RBTreeNode( + 0, 0, isRed = false, + leftChild = RBTreeNode( + -1, -1, isRed = false, + leftChild = RBTreeNode(-2, -2, isRed = true), + rightChild = null + ), + rightChild = RBTreeNode(1, 1, isRed = false) + ), + expected = RBTreeNode( + -1, -1, isRed = false, + leftChild = RBTreeNode( + -2, -2, isRed = false + ), + rightChild = RBTreeNode( + 0, 0, isRed = false, + leftChild = null, + rightChild = RBTreeNode(1, 1, isRed = true) + ) + ), + ) + } + + @Test + @DisplayName("remove min node with two child") + public fun testRemoveMinNodeWithTwoChild() { + tree.assertRemoveMinNode( + treeView = RBTreeNode( + 1, 1, isRed = false, + leftChild = RBTreeNode(0, 0, isRed = false), + rightChild = RBTreeNode(2, 2, isRed = false), + ), + expected = RBTreeNode( + 2, 2, isRed = true, + leftChild = RBTreeNode(1, 1, isRed = true), + rightChild = null + ) + ) + } + + @Test + @DisplayName("remove min node with big subtree") + public fun testRemoveMinNodeWithBigSubtree() { + tree.assertRemoveMinNode( + treeView = RBTreeNode( + 2, 2, isRed = false, + leftChild = RBTreeNode( + 0, 0, isRed = false, + leftChild = RBTreeNode(-1, -1, isRed = false), + rightChild = RBTreeNode(1, 1, isRed = false) + ), + rightChild = RBTreeNode( + 4, 4, isRed = false, + leftChild = RBTreeNode(3, 3, isRed = false), + rightChild = RBTreeNode(5, 5, isRed = false) + ), + ), + expected = RBTreeNode( + 4, 4, isRed = true, + leftChild = RBTreeNode( + 2, 2, isRed = true, + leftChild = RBTreeNode( + 1, 1, isRed = false, + leftChild = RBTreeNode(0, 0, isRed = true), + rightChild = null + ), + rightChild = RBTreeNode(3, 3, isRed = false) + ), + rightChild = RBTreeNode(5, 5, isRed = false) + ) + ) + } + + @Test + @DisplayName("left rotate node without right") + public fun testLeftRotateNodeWithoutRightChild() { + val node = RBTreeNode( + 0, 0, false, + RBTreeNode(-1, -1, true), + null, + ) + tree.assertNodeLeftRotation( + node, node + ) + } + + @Test + @DisplayName("left rotate subtree") + public fun testLeftRotateOf() { + tree.assertNodeLeftRotation( + RBTreeNode( + 2, 2, false, + RBTreeNode( + 0, 0, true, + RBTreeNode(-1, -1, false), + RBTreeNode(1, 1, false) + ), + RBTreeNode(3, 3, false) + ), + RBTreeNode( + 0, 0, false, + RBTreeNode(-1, -1, false), + RBTreeNode( + 2, 2, true, + RBTreeNode(1, 1, false), + RBTreeNode(3, 3, false), + ) + ), + ) + } + + @Test + @DisplayName("right rotate node without left") + public fun testRightRotateNodeWithoutLeftChild() { + val node = RBTreeNode( + 0, 0, false, + null, + RBTreeNode(1, 1, true) + ) + tree.assertNodeRightRotation( + node, node + ) + } + + @Test + @DisplayName("right rotate of subtree") + public fun testRightRotate() { + tree.assertNodeRightRotation( + RBTreeNode( + 0, 0, false, + RBTreeNode(-1, -1, false), + RBTreeNode( + 2, 2, true, + RBTreeNode(1, 1, false), + RBTreeNode(3, 3, false), + ) + ), + RBTreeNode( + 2, 2, false, + RBTreeNode( + 0, 0, true, + RBTreeNode(-1, -1, false), + RBTreeNode(1, 1, false) + ), + RBTreeNode(3, 3, false) + ), + ) + } + + @Test + @DisplayName("flip colors of node and repeat it") + public fun testFlipColors() { + val node = RBTreeNode(0, 0, false, RBTreeNode(-1, -1), RBTreeNode(1, 1)) + tree.assertNodeColorFlip( + RBTreeNode( + 0, 0, true, + RBTreeNode(-1, -1, false), + RBTreeNode(1, 1, false) + ), node + ) + tree.assertNodeColorFlip( + RBTreeNode(0, 0, false, RBTreeNode(-1, -1), RBTreeNode(1, 1)), + node + ) + } + + @Test + @DisplayName("is left child of red null node") + public fun testIsRedLeftChildOfNullNode() { + tree.assertNodeLeftChildColor(false, null) + } + + @Test + @DisplayName("is red left child of node which property isRed equals true") + public fun testIsRedLeftChildOfNodeWhichPropertyEqualsRed() { + tree.assertNodeLeftChildColor( + true, + RBTreeNode( + 0, 0, false, + RBTreeNode(-1, -1, true), + null + ) + ) + } + + @Test + @DisplayName("is red left child of node with property isRed equals false") + public fun testIsRedLeftChildOfNodeWhichPropertyEqualsBlack() { + tree.assertNodeLeftChildColor( + false, + RBTreeNode( + 0, 0, true, + RBTreeNode(-1, -1, false), + null + ) + ) + } + + @Test + @DisplayName("is red null node") + public fun testIsRedNullNode() { + tree.assertNodeColor(false, null) + } + + @Test + @DisplayName("is red node with property isRed equals true") + public fun testIsRedNodeWithPropertyEqualsRed() { + tree.assertNodeColor(true, RBTreeNode(0, 0, true)) + } + + @Test + @DisplayName("is red node with property isRed equals false") + public fun testIsRedNodeWithPropertyEqualsBlack() { + tree.assertNodeColor(false, RBTreeNode(0, 0, false)) + } + + + @Test + @DisplayName("create node") + public fun testCreateNode() { + tree.assertNodeCreation(0, 0) + } + + @Test + @DisplayName("update root as null") + public fun testUpdateRootAsNull() { + tree.assertUpdateRoot(null) + } + + @Test + @DisplayName("update root as red node") + public fun testUpdateRootAsRedNode() { + tree.assertUpdateRoot(RBTreeNode(0, 0, true)) + } + + @Test + @DisplayName("update root as black node") + public fun testUpdateRootAsBlackNode() { + tree.assertUpdateRoot(RBTreeNode(0, 0, false)) + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/tree_tripper/binary_trees/assistants/AVLTreeTestAssistant.kt b/lib/src/test/kotlin/tree_tripper/binary_trees/assistants/AVLTreeTestAssistant.kt new file mode 100644 index 0000000..7e2cd8c --- /dev/null +++ b/lib/src/test/kotlin/tree_tripper/binary_trees/assistants/AVLTreeTestAssistant.kt @@ -0,0 +1,46 @@ +package tree_tripper.binary_trees.assistants + +import assertBinaryNodeDataEquals +import assertBinaryNodeDeepEquals +import tree_tripper.binary_trees.AVLTree +import tree_tripper.nodes.binary_nodes.AVLTreeNode + + +public class AVLTreeTestAssistant, V> : AVLTree() { + + fun assertRoot(node: AVLTreeNode?, lazyMassage: () -> String) { + try { + assertBinaryNodeDataEquals(root, node) {root, expected -> root.height == expected.height} + } catch (e: AssertionError) { + throw AssertionError(lazyMassage(), e) + } + } + + fun assertNodeCreation(key: K, value: V) { + assertBinaryNodeDeepEquals(createNode(key, value), AVLTreeNode(key, value)) {node1, node2 -> node1.height == node2.height} + } + + fun assertBalanceTree(expected: AVLTreeNode, node: AVLTreeNode) { + assertBinaryNodeDeepEquals(expected, balanceTree(node)) {node1, node2 -> node1.height == node2.height} + } + + fun assertBalanceFactor(expected: Int, node: AVLTreeNode?) { + val factor = balanceFactor(node) + assert(factor == expected) { + "Invalid height balance of $node, balance factor: $factor, expected: $expected." + } + } + + fun assertBalance(expected: AVLTreeNode, node: AVLTreeNode) { + assertBinaryNodeDeepEquals(expected, balance(node)) {node1, node2 -> node1.height == node2.height} + } + + fun assertNodeRightRotation(expected: AVLTreeNode, node: AVLTreeNode) { + assertBinaryNodeDeepEquals(expected, rotateRight(node)) {node1, node2 -> node1.height == node2.height} + } + + fun assertNodeLeftRotation(expected: AVLTreeNode, node: AVLTreeNode) { + assertBinaryNodeDeepEquals(expected, rotateLeft(node)) {node1, node2 -> node1.height == node2.height} + } + +} diff --git a/lib/src/test/kotlin/tree_tripper/binary_trees/assistants/BSTreeTestAssistant.kt b/lib/src/test/kotlin/tree_tripper/binary_trees/assistants/BSTreeTestAssistant.kt new file mode 100644 index 0000000..875aba5 --- /dev/null +++ b/lib/src/test/kotlin/tree_tripper/binary_trees/assistants/BSTreeTestAssistant.kt @@ -0,0 +1,55 @@ +package tree_tripper.binary_trees.assistants + +import assertBinaryNodeDeepEquals +import org.junit.jupiter.api.Assertions +import tree_tripper.binary_trees.BSTree +import tree_tripper.nodes.binary_nodes.BSTreeNode +import java.util.* + + +public class BSTreeTestAssistant, V>: BSTree() { + + public fun assertNullRoot() { + Assertions.assertEquals(root, null, "Incorrect a root initialization.") + } + + public fun assertWasCreatedNode(key: K, value: V) { + assertBinaryNodeDeepEquals(createNode(key, value), BSTreeNode(key, value)) + } + + public fun assertWasUpdatedRoot(key: K, value: V) { + val node = createNode(key, value) + updateRoot(node) + Assertions.assertEquals(root, node, "Incorrect a root update.") + } + + public fun assertWasBalancedTree(key: K, value: V) { + val node = createNode(key, value) + Assertions.assertEquals(balanceTree(node), node, "Incorrect a tree balance.") + } + + public fun assertIsBSTree() { + if (root == null) throw NullPointerException("Root is null") + val queue: Queue> = LinkedList(listOfNotNull(root)) + + while (queue.isNotEmpty()) { + val node = queue.remove() + val nodeLeft = node.leftChild + if (nodeLeft != null) + assert(nodeLeft.key < node.key) + { "Incorrect the binary search tree structure: a left child key is no less than the parent key." } + val nodeRight = node.rightChild + if (nodeRight != null) + assert(nodeRight.key > node.key) + { "Incorrect the binary search tree structure: a left child key is no more than the parent key." } + + queue.addAll(node.getChildren()) + } + } + + public fun getRoot(): Pair { + val root = this.root ?: throw NullPointerException("Root is null") + return Pair(root.key, root.value) + } + +} diff --git a/lib/src/test/kotlin/tree_tripper/binary_trees/assistants/RBTreeTestAssistant.kt b/lib/src/test/kotlin/tree_tripper/binary_trees/assistants/RBTreeTestAssistant.kt new file mode 100644 index 0000000..ac601d7 --- /dev/null +++ b/lib/src/test/kotlin/tree_tripper/binary_trees/assistants/RBTreeTestAssistant.kt @@ -0,0 +1,107 @@ +package tree_tripper.binary_trees.assistants + +import assertBinaryNodeDataEquals +import assertBinaryNodeDeepEquals +import org.junit.jupiter.api.Assertions +import tree_tripper.binary_trees.RBTree +import tree_tripper.nodes.binary_nodes.RBTreeNode +import tree_tripper.nodes.notNullNodeAction +import java.util.Queue +import java.util.LinkedList + + +public class RBTreeTestAssistant, V>: RBTree() { + + public fun assertIsRBTree() { + assert(!isRedColor(root)) {"Root of RBTree is red. Must be black."} + val queue: Queue> = LinkedList>(listOfNotNull(root)) + + while (queue.isNotEmpty()) { + val node: RBTreeNode = queue.remove() + val leftCompareResult: Int = notNullNodeAction( + node.leftChild, -1 + ) { leftChild -> leftChild.key.compareTo(node.key) } + val rightCompareResult: Int = notNullNodeAction( + node.rightChild, 1 + ) { rightChild -> rightChild.key.compareTo(node.key) } + + assert(leftCompareResult <= -1) { + "Left child of $node is not a BST node: keys compare result: $leftCompareResult" + } + assert(rightCompareResult >= 1) { + "Right child of $node is not a BST node: keys compare result: $rightCompareResult" + } + assert(!isRedColor(node.rightChild)) {"Right child of node at RBTree is red. Its must be black."} + if (isRedColor(node)) { + assert(!isRedLeftChild(node)) {"Left child of red node at RBTree is red. Its must be black."} + } + queue.addAll(node.getChildren()) + } + assertBlackHeight(root) + } + + private fun assertBlackHeight(node: RBTreeNode?): Int { + if (node == null) return 1 + val left = assertBlackHeight(node.leftChild) + val right = assertBlackHeight(node.rightChild) + Assertions.assertEquals(left, right) + return (if (node.isRed) 0 else 1) + left + } + + public fun assertRoot(node: RBTreeNode?, lazyMassage: () -> String) { + try { + assertBinaryNodeDataEquals(root, node) {rootNode, expectedNode -> rootNode.isRed == expectedNode.isRed} + } catch (e: AssertionError) { + throw AssertionError(lazyMassage(), e) + } + } + + public fun assertNodeColor(expected: Boolean, node: RBTreeNode?) { + Assertions.assertEquals(expected, isRedColor(node)) + } + + public fun assertNodeLeftChildColor(expected: Boolean, node: RBTreeNode?) { + Assertions.assertEquals(expected, isRedLeftChild(node)) + } + + public fun assertNodeLeftRotation(expected: RBTreeNode, node: RBTreeNode) { + assertBinaryNodeDeepEquals(expected, rotateLeft(node)) {n1, n2 -> n1.isRed == n2.isRed} + } + + public fun assertNodeRightRotation(expected: RBTreeNode, node: RBTreeNode) { + assertBinaryNodeDeepEquals(expected, rotateRight(node)) {n1, n2 -> n1.isRed == n2.isRed} + } + + public fun assertNodeColorFlip(expected: RBTreeNode, node: RBTreeNode) { + flipColors(node) + assertBinaryNodeDeepEquals(expected, node) {n1, n2 -> n1.isRed == n2.isRed} + } + + public fun assertNodeCreation(key: K, value: V) { + assertBinaryNodeDeepEquals(createNode(key, value), RBTreeNode(key, value)) { n1, n2 -> n1.isRed == n2.isRed} + } + + public fun assertUpdateRoot(node: RBTreeNode?) { + updateRoot(node) + assertBinaryNodeDataEquals( + root, + if (node != null) RBTreeNode(node.key, node.value, false) else null + ) + } + + public fun getRoot(): Pair { + val root = this.root ?: throw NullPointerException("Tree is empty can't get root pair") + return Pair(root.key, root.value) + } + + public fun assertRemoveMinNode(treeView: RBTreeNode?, expected: RBTreeNode?) { + val result = removeMinNode(treeView) + assertBinaryNodeDeepEquals(expected, result) {n1, n2 -> n1.isRed == n2.isRed} + } + + public fun assertMoveRightNode(treeView: RBTreeNode, expected: RBTreeNode) { + val result = moveRedRight(treeView) + assertBinaryNodeDeepEquals(expected, result) {n1, n2 -> n1.isRed == n2.isRed} + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/tree_tripper/iterators/BinarySearchTreeIteratorTest.kt b/lib/src/test/kotlin/tree_tripper/iterators/BinarySearchTreeIteratorTest.kt new file mode 100644 index 0000000..3ac0388 --- /dev/null +++ b/lib/src/test/kotlin/tree_tripper/iterators/BinarySearchTreeIteratorTest.kt @@ -0,0 +1,136 @@ +package tree_tripper.iterators + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import tree_tripper.nodes.binary_nodes.BSTreeNode + + +class BinarySearchTreeIteratorTest { + lateinit var iterator: BinarySearchTreeIterator> + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testIteratorCases") + @DisplayName("test iterator at width order") + public fun testWidthOrderIterator(expected: List, root: BSTreeNode) { + iterator = BinarySearchTreeIterator(root) + var index: Int = 0 + + while (iterator.hasNext()) { + Assertions.assertEquals(iterator.next(), Pair(expected[index], expected[index])) + index++ + } + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testGetALotOfElementsCases") + @DisplayName("try get more elements than iterator has") + public fun testGetALotOfElements(order: IterationOrders) { + iterator = BinarySearchTreeIterator(null, order) + Assertions.assertFalse(iterator.hasNext()) + Assertions.assertThrows(NoSuchElementException::class.java) { iterator.next() } + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testIteratorCases") + @DisplayName("test iterator at increase order") + public fun testIncreasingOrderIterator(expected: List, root: BSTreeNode) { + iterator = BinarySearchTreeIterator(root, IterationOrders.INCREASING_ORDER) + val sortedElements = expected.sorted() + var index: Int = 0 + + while (iterator.hasNext()) { + Assertions.assertEquals(iterator.next(), Pair(sortedElements[index], sortedElements[index])) + index++ + } + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testIteratorCases") + @DisplayName("test iterator at decrease order") + public fun testDecreasingOrderIterator(expected: List, root: BSTreeNode) { + iterator = BinarySearchTreeIterator(root, IterationOrders.DECREASING_ORDER) + val sortedElements = expected.sorted().reversed() + var index: Int = 0 + + while (iterator.hasNext()) { + Assertions.assertEquals(iterator.next(), Pair(sortedElements[index], sortedElements[index])) + index++ + } + } + + companion object { + + @JvmStatic + fun testGetALotOfElementsCases(): List = listOf( + Arguments.of(IterationOrders.WIDTH_ORDER), + Arguments.of(IterationOrders.INCREASING_ORDER), + Arguments.of(IterationOrders.DECREASING_ORDER), + ) + + @JvmStatic + fun testIteratorCases(): List = listOf( + Arguments.of(listOf(0), BSTreeNode(0, 0)), + Arguments.of( + listOf(0, -5, 5), + BSTreeNode( + 0, 0, + BSTreeNode(-5, -5), + BSTreeNode(5, 5), + ) + ), + Arguments.of( + listOf(0, -5, 5, -10), + BSTreeNode( + 0, 0, + BSTreeNode( + -5, -5, + BSTreeNode(-10, -10), + null, + ), + BSTreeNode(5, 5), + ) + ), + Arguments.of( + listOf(0, -5, 5, -10, 10), + BSTreeNode( + 0, 0, + BSTreeNode( + -5, -5, + BSTreeNode(-10, -10), + null, + ), + BSTreeNode( + 5, 5, + null, + BSTreeNode(10, 10), + ) + ) + ), + Arguments.of( + listOf(0, -5, 5, -10, -3, 3, 10, 4), + BSTreeNode( + 0, 0, + BSTreeNode( + -5, -5, + BSTreeNode(-10, -10), + BSTreeNode(-3, -3), + ), + BSTreeNode( + 5, 5, + BSTreeNode( + 3, 3, + null, + BSTreeNode(4, 4) + ), + BSTreeNode(10, 10), + ) + ) + ) + ) + + } + +} diff --git a/lib/src/test/kotlin/tree_tripper/nodes/UtilsTest.kt b/lib/src/test/kotlin/tree_tripper/nodes/UtilsTest.kt new file mode 100644 index 0000000..c21815d --- /dev/null +++ b/lib/src/test/kotlin/tree_tripper/nodes/UtilsTest.kt @@ -0,0 +1,43 @@ +package tree_tripper.nodes + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import tree_tripper.nodes.binary_nodes.BSTreeNode + + +public class UtilsTest { + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testNodeUpdateCases") + @DisplayName("util of node update") + public fun testNodeUpdate(expected: Boolean, node: BSTreeNode?) { + var isActivateAction: Boolean = false + notNullNodeUpdate(node) { isActivateAction = true } + Assertions.assertEquals(expected, isActivateAction) + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testNodeActionCases") + @DisplayName("util of action on node") + public fun testNodeAction(expected: Boolean, node: BSTreeNode?) { + Assertions.assertEquals(expected, notNullNodeAction(node, false) { expected }) + } + + companion object { + @JvmStatic + fun testNodeUpdateCases(): List = listOf( + Arguments.of(false, null), + Arguments.of(true, BSTreeNode(0, 0)), + ) + + @JvmStatic + fun testNodeActionCases(): List = listOf( + Arguments.of(false, null), + Arguments.of(true, BSTreeNode(0, 0)), + ) + } + +} diff --git a/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/AVLTreeNodeTest.kt b/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/AVLTreeNodeTest.kt new file mode 100644 index 0000000..ca1a2b2 --- /dev/null +++ b/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/AVLTreeNodeTest.kt @@ -0,0 +1,80 @@ +package tree_tripper.nodes.binary_nodes + +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + + +class AVLTreeNodeTest { + + @Test + @DisplayName("node initialization") + public fun nodeInitialization() { + val node = AVLTreeNode(0, 0) + Assertions.assertEquals(1, node.height) {"The height is not 1 by standard initialize."} + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testUpdateHeight") + @DisplayName("update height") + public fun testUpdateHeight(expected: Int, node: AVLTreeNode) { + node.updateHeight() + Assertions.assertEquals(expected, node.height) {"The height does not match the expected."} + } + + companion object { + + @JvmStatic + public fun testUpdateHeight(): List = listOf( + + Arguments.of(1, + AVLTreeNode(0, 0, 0, null, null) + ), + + Arguments.of(2, + AVLTreeNode( + 0, 0, 0, + AVLTreeNode(1, 1, 1, null, null), + null + ) + ), + + Arguments.of(2, + AVLTreeNode( + 0, 0, 0, + null, + AVLTreeNode(1, 1, 1, null, null) + ) + ), + + Arguments.of(2, + AVLTreeNode( + 0, 0, 0, + AVLTreeNode(1, 1, 1, null, null), + AVLTreeNode(2, 2, 1, null, null) + ) + ), + + Arguments.of(3, + AVLTreeNode( + 0, 0, 0, + AVLTreeNode(1, 1, 2, null, null), + AVLTreeNode(2, 2, 1, null, null) + ) + ), + + Arguments.of(3, + AVLTreeNode( + 0, 0, 0, + AVLTreeNode(1, 1, 1, null, null), + AVLTreeNode(2, 2, 2, null, null) + ) + ) + + ) + + } +} diff --git a/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/BSTreeNodeTest.kt b/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/BSTreeNodeTest.kt new file mode 100644 index 0000000..612d22e --- /dev/null +++ b/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/BSTreeNodeTest.kt @@ -0,0 +1,72 @@ +package tree_tripper.nodes.binary_nodes + +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + + +public class BSTreeNodeTest { + + @Test + @DisplayName("node initialization") + public fun testNodeInitialization() { + val node = BSTreeNode(1, -1) + assertEquals(node.key, 1, "Incorrect a key assignment") + assertEquals(node.value, -1, "Incorrect a value assignment") + assertEquals(node.leftChild, null, "Incorrect a left child assignment") + assertEquals(node.rightChild, null, "Incorrect a right child assignment") + } + + @Test + @DisplayName("get children") + public fun testGetChildren() { + val node = BSTreeNode(2, -2) + assertEquals(node.getChildren(), listOf()) + val nodeLeft = BSTreeNode(1, -1) + node.leftChild = nodeLeft + assertEquals(node.getChildren(), listOf(nodeLeft)) + val nodeRight = BSTreeNode(3, -3) + node.rightChild = nodeRight + assertEquals(node.getChildren(), listOf(nodeLeft, nodeRight)) + } + + @Test + @DisplayName("to string") + public fun testToString() { + val node = BSTreeNode(1, -1) + assertEquals(node.toString(), "BSTreeNode(key=1, value=-1)") + } + + @Test + @DisplayName("to string simple view") + public fun testToStringSimpleView() { + val node = BSTreeNode(1, -1) + assertEquals(node.toStringSimpleView(), "(1: -1)") + } + + @Test + @DisplayName("node to string with subtree view") + public fun testNodeToStringWithSubtreeView() { + val builder = StringBuilder() + val node = BSTreeNode(1, -1) + node.toStringWithSubtreeView(0, builder) + assertEquals(builder.toString(), "${node.toStringSimpleView()}\n") + } + + @Test + @DisplayName("node with children to string with subtree view") + public fun testNodeWithChildrenToStringWithSubtreeView() { + val builder = StringBuilder() + val node = BSTreeNode(2, -2) + val nodeLeft = BSTreeNode(1, -1) + node.leftChild = nodeLeft + val nodeRight = BSTreeNode(3, -3) + node.rightChild = nodeRight + node.toStringWithSubtreeView(0, builder) + assertEquals(builder.toString(), + "\t${nodeRight.toStringSimpleView()}\n" + + "${node.toStringSimpleView()}\n" + + "\t${nodeLeft.toStringSimpleView()}\n") + } + +} \ No newline at end of file diff --git a/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/RBTreeNodeTest.kt b/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/RBTreeNodeTest.kt new file mode 100644 index 0000000..ac95991 --- /dev/null +++ b/lib/src/test/kotlin/tree_tripper/nodes/binary_nodes/RBTreeNodeTest.kt @@ -0,0 +1,81 @@ +package tree_tripper.nodes.binary_nodes + +import assertBinaryNodeDeepEquals +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource + + +public class RBTreeNodeTest { + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testNodeSimpleInitializeCases") + @DisplayName("node simple initialization") + public fun testNodeSimpleInitialize(key: Int, value: Int?) { + val node = RBTreeNode(key, value) + Assertions.assertEquals(key, node.key) { "Key of node is not equal." } + Assertions.assertEquals(value, node.value) { "Value of node is not equal." } + Assertions.assertTrue(node.isRed) { "Color of node is not red." } + Assertions.assertNull(node.leftChild) { "Left child of node is not null." } + Assertions.assertNull(node.rightChild) { "Right child of node is not null." } + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testNodeColorTypeInitializeCases") + @DisplayName("node initialization with color") + public fun testNodeColorTypeInitialize(isRed: Boolean) { + val node = RBTreeNode(0, 0, isRed) + Assertions.assertEquals(isRed, node.isRed) { "Color of node is not equal." } + Assertions.assertNull(node.leftChild) { "Left child of node is not null." } + Assertions.assertNull(node.rightChild) { "Right child of node is not null." } + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testNodeFullInitializeCases") + @DisplayName("node initialization with color and children") + public fun testNodeFullInitialize(leftChild: RBTreeNode?, rightChild: RBTreeNode?) { + val node = RBTreeNode(0, 0, false, leftChild, rightChild) + assertBinaryNodeDeepEquals(leftChild, node.leftChild) { n1, n2 -> n1.isRed == n2.isRed } + assertBinaryNodeDeepEquals(rightChild, node.rightChild) { n1, n2 -> n1.isRed == n2.isRed } + } + + @ParameterizedTest(name = "{displayName}[{index}] {argumentsWithNames}") + @MethodSource("testToStringSimpleViewCases") + @DisplayName("to string simple view") + public fun testToStringSimpleView(expected: String, node: RBTreeNode) { + Assertions.assertEquals(expected, node.toStringSimpleView()) + } + + companion object { + @JvmStatic + fun testNodeSimpleInitializeCases(): List = listOf( + Arguments.of(-1, -1), + Arguments.of(0, 0), + Arguments.of(2, 2), + Arguments.of(3, null), + ) + + @JvmStatic + fun testNodeColorTypeInitializeCases(): List = listOf( + Arguments.of(false), + Arguments.of(true), + ) + + @JvmStatic + fun testNodeFullInitializeCases(): List = listOf( + Arguments.of(null, null), + Arguments.of(RBTreeNode(1, null), null), + Arguments.of(null, RBTreeNode(-1, null)), + Arguments.of(RBTreeNode(-1, null), RBTreeNode(-1, null)), + ) + + @JvmStatic + fun testToStringSimpleViewCases(): List = listOf( + Arguments.of("(-1: null) - RED", RBTreeNode(-1, null)), + Arguments.of("(0: 0) - BLACK", RBTreeNode(0, 0, false)), + ) + } + +} \ No newline at end of file diff --git a/scripts/csv-reports-printer.py b/scripts/csv-reports-printer.py new file mode 100644 index 0000000..4ee0bbe --- /dev/null +++ b/scripts/csv-reports-printer.py @@ -0,0 +1,198 @@ +import argparse +import csv +import sys +import typing +from text_colorize import ANSIColors, TextStyle, colorize + + +COLUMNS_TYPES = [ + '_MISSED', + '_COVERED', +] + +CSV_COLUMNS = [ + 'PACKAGES', + 'CLASS', + 'BRANCH_MISSED', + 'BRANCH_COVERED', + 'LINE_MISSED', + 'LINE_COVERED', + 'METHOD_MISSED', + 'METHOD_COVERED', +] +DISPLAY_COLUMNS = [ + 'PACKAGES', + 'CLASS', + 'BRANCH', + 'LINE', + 'METHOD', +] +DEFAULT_LABEL_SIZE = 8 + + +def create_row_info() -> dict: + return { + key: 0 for key in CSV_COLUMNS + } + + +def is_valid_lib(group: str, lib_name: str) -> bool: + if len(lib_name) == 0: + return True + return group == lib_name + + +def parse_args(args: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + prog="CSV Jacoco Test Reports Printer", + description="Program read csv file with jacoco report and print it at terminal at stdin", + ) + + parser.add_argument( + "-i", "--input", required=True, + help="setup path to CSV file with jacoco report information", + metavar="" + ) + parser.add_argument( + "-l", "--lib", + help="setup library name to remove from package path", + default="", + metavar="" + ) + parser.add_argument( + "-p", "--package-print", + help="setup flag to 'ON' to print packages of files at report (default 'OFF')", + action='store_true', + default=False + ) + + return parser.parse_args(args) + + +def read_csv(namespace: argparse.Namespace) -> typing.Optional[dict]: + table = [] + max_packages_name_length = 20 + max_classes_name_length = 20 + + with open(getattr(namespace, "input"), 'r') as file: + reader = csv.reader(file) + for row in reader: + if len(row) == 0: + break + if (row[0] == "GROUP") or not is_valid_lib(row[0], getattr(namespace, "lib", "")): + continue + + row_info = create_row_info() + is_skipped = False + for key in row_info.keys(): + if key not in CSV_COLUMNS: + row_info.pop(key) + + index = CSV_COLUMNS.index(key) + 1 + row_info[key] = row[index] + + if key == "PACKAGES": + max_packages_name_length = max(max_packages_name_length, len(row_info[key])) + elif key == "CLASS": + if '(' in row_info[key] or ')' in row_info[key] or ' ' in row_info[key]: + is_skipped = True + break + elif '.' in row_info[key]: + row_info[key] = row_info[key].split('.')[-1] + + max_classes_name_length = max(max_classes_name_length, len(row_info[key])) + + if not is_skipped: + table.append(row_info) + return { + "table": table, + "max_packages_name_length": max_packages_name_length, + "max_classes_name_length": max_classes_name_length + } + + +def create_label(text: str, lbl_size: int, color: ANSIColors = ANSIColors.BLACK): + if len(text) >= lbl_size: + text = text[:lbl_size] + + if len(text) % 2 != 0: + text = ' ' + text + + color_text = colorize( + f"{{:^{lbl_size}}}".format(text), + color, + TextStyle.BOLD + ) + return f'| {color_text} |' + + +def colorize_percent_label(percent: int) -> str: + color = ANSIColors.RED + if 50 <= percent < 75: + color = ANSIColors.YELLOW + elif 75 <= percent <= 100: + color = ANSIColors.GREEN + + return create_label(f"{percent}%", DEFAULT_LABEL_SIZE, color) + + +def display_columns(max_packages_name_length: int, max_classes_name_length: int) -> int: + global DISPLAY_COLUMNS, DEFAULT_LABEL_SIZE + for column in DISPLAY_COLUMNS: + lbl_size = DEFAULT_LABEL_SIZE + if column == "CLASS": + lbl_size = max_classes_name_length + elif column == "PACKAGES": + lbl_size = max_packages_name_length + + lbl = create_label(column, lbl_size, ANSIColors.PURPLE) + print(lbl, end="") + print() + + +def display_csv_data(namespace: argparse.Namespace, csv_data_dict: dict) -> None: + global DISPLAY_COLUMNS, COLUMNS_TYPES + if not getattr(namespace, "package_print", False): + DISPLAY_COLUMNS.remove("PACKAGES") + + if getattr(namespace, 'lib'): + print(f"Jacoco Covered Report Info for module named '{getattr(namespace, 'lib')}':") + + max_packages_name_length = csv_data_dict.get("max_packages_name_length", 20) + max_classes_name_length = csv_data_dict.get("max_classes_name_length", 20) + table: list[dict] = csv_data_dict.get("table", []) + display_columns(max_packages_name_length, max_classes_name_length) + + for row in table: + for column in DISPLAY_COLUMNS: + lbl = "" + if column in ["PACKAGES", "CLASS"]: + lbl = create_label( + row[column], + max_packages_name_length if column == "PACKAGES" else max_classes_name_length, + ANSIColors.YELLOW + ) + else: + vals = [int(row[column + _type]) for _type in COLUMNS_TYPES] + percent = int(round((vals[1] - vals[0]) / vals[1], 2) * 100) if vals[1] != 0 else 100 + lbl = colorize_percent_label( + percent, + ) + + print(lbl, end="") + print() + + +if __name__ == "__main__": + ns = parse_args(sys.argv[1:]) + + try: + csv_data = read_csv(ns) + except Exception as e: + print( + f"Can't read csv file: '{getattr(ns, 'input')}', get exception: '{e}'", + file=sys.stderr + ) + sys.exit(-1) + + display_csv_data(ns, csv_data) diff --git a/scripts/test-result-printer.py b/scripts/test-result-printer.py new file mode 100644 index 0000000..5f9e987 --- /dev/null +++ b/scripts/test-result-printer.py @@ -0,0 +1,185 @@ +import argparse +import sys, os +import xml.etree.ElementTree as ET +from text_colorize import ANSIColors, TextStyle, colorize + + +class TestCase: + + def __init__(self, name: str, is_passed: bool) -> None: + self.name = str(name) + self.is_passed = bool(is_passed) + + def toString(self, indent: int = 0): + return "\t" * indent + f"{self.name} -> {self.result_type()}" + + def result_type(self) -> str: + return colorize( + 'PASSED' if self.is_passed else 'FAILURE', + ANSIColors.GREEN if self.is_passed else ANSIColors.RED, + TextStyle.ITALIC + ) + + def __bool__(self): + return self.is_passed + + +class ParametrisedTestCase(TestCase): + + def __init__(self, name: str, cases: list[TestCase]) -> None: + super().__init__(name, all(cases)) + self.cases = cases + + def add_case(self, case: TestCase): + self.is_passed = self.is_passed and case.is_passed + self.cases.append(case) + + def toString(self, indent: int = 0, also_failed: bool = False): + inline_cases = [] + for case in self.cases: + if (also_failed and case.is_passed): + continue + + inline_cases.append(case.toString(indent + 1)) + + inline_cases = '\n'.join(inline_cases).rstrip() + return super().toString(indent) + f"\n{inline_cases}" + + +def parse_args(args: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + prog="Test Result Printer", + description="Program read xml files with tests results and print it at terminal at stdin", + ) + parser.add_argument( + "-d", "--dir", required=True, + help="setup path to directory with xml files with tests information", + metavar="" + ) + parser.add_argument( + "-a", "--all", + action="store_true", + help="setup mode of diplay type to print all test information to ON (default OFF)", + ) + parser.add_argument( + "-f", "--all-failures", + action="store_true", + help="setup mode of diplay type to print also test information which failed to ON (default OFF)", + ) + return parser.parse_args(args) + + +def parse_test_result(test_path: str) -> tuple[ET.Element, dict[str, TestCase]]: + tree_root = ET.parse(test_path).getroot() + cases = dict() + for child in tree_root: + if child.tag != "testcase": + continue + + name = child.get("name", "uncknown test cases") + is_passed = child.find("failure") is None + if '[' in name: + primary_name = name.split('[')[0] + args = '[' + "[".join(name.split('[')[1:]) + + case: ParametrisedTestCase = cases.get(primary_name, ParametrisedTestCase(primary_name, [])) + case.add_case(TestCase(args, is_passed)) + cases[primary_name] = case + else: + cases[name] = TestCase(name, is_passed) + + return (tree_root, cases,) + + +def display_all_test_result(tree_root: ET.Element, cases: dict[str, TestCase]): + print( + "Tests of", + tree_root.attrib.get("name", "UncnownTestSuite").split('.')[-1].replace("Test", ":"), + sep=" " + ) + for name in sorted(cases.keys()): + print(cases[name].toString(indent=1)) + + passed_test_count = int(tree_root.attrib.get("tests", 0)) - int(tree_root.attrib.get("failures", 0)) + print( + colorize(f"Passed: {passed_test_count}", ANSIColors.GREEN, TextStyle.BOLD), + colorize(f"Failures: {tree_root.attrib.get('failures', 0)}", ANSIColors.RED, TextStyle.BOLD), + f"Time: {tree_root.attrib.get('time', 0.0)}", + sep=" ", + end=os.linesep * 2 + ) + + +def display_failures_test_result(tree_root: ET.Element, cases: dict[str, TestCase]): + failed_tests = [] + for name in sorted(cases.keys()): + if not cases[name].is_passed: + failed_tests.append(cases[name]) + + if len(failed_tests) == 0: + return + + print( + "Failed tests of", + tree_root.attrib.get("name", "UncnownTestSuite").split('.')[-1].replace("Test", ":"), + sep=" " + ) + for case in failed_tests: + if isinstance(case, ParametrisedTestCase): + print(case.toString(indent=1, also_failed=True)) + elif isinstance(case, TestCase): + print(case.toString(indent=1)) + + passed_test_count = int(tree_root.attrib.get("tests", 0)) - int(tree_root.attrib.get("failures", 0)) + print( + colorize(f"Passed: {passed_test_count}", ANSIColors.GREEN, TextStyle.BOLD), + colorize(f"Failures: {tree_root.attrib.get('failures', 0)}", ANSIColors.RED, TextStyle.BOLD), + f"Time: {tree_root.attrib.get('time', 0.0)}", + sep=" ", + end=os.linesep * 2 + ) + + +if __name__ == "__main__": + ns = parse_args(sys.argv[1:]) + + tests_result_dir = getattr(ns, "dir") + childs = os.listdir(tests_result_dir) + tests_results: list[tuple[ET.Element, dict[str, TestCase]]] = [] + for child in sorted(childs): + child_path = os.path.join(tests_result_dir, child) + if not os.path.isfile(child_path): + continue + + if not (child.startswith("TEST") and child.endswith(".xml")): + continue + + try: + tests_results.append(parse_test_result(child_path)) + except Exception as e: + print(f"Can't display ttest information at file '{child}': {e}", file=sys.stderr) + + tests_count = 0 + tests_failed_count = 0 + time_of_all_tests = 0 + for test_result in tests_results: + tree_root: ET.Element = test_result[0] + tests_count += int(tree_root.attrib.get("tests", 0)) + tests_failed_count += int(tree_root.attrib.get("failures", 0)) + time_of_all_tests += float(tree_root.attrib.get('time', 0.0)) + + + print( + colorize(f"Count of tests: {tests_count}", ANSIColors.YELLOW, TextStyle.BOLD), + colorize(f"Count of passed tests: {tests_count - tests_failed_count}", ANSIColors.GREEN, TextStyle.BOLD), + colorize(f"Count of failured tests: {tests_failed_count}", ANSIColors.RED, TextStyle.BOLD), + colorize(f"Time: {time_of_all_tests}", ANSIColors.BLUE, TextStyle.BOLD), + sep=os.linesep, + end=os.linesep * 2 + ) + + for test_result in tests_results: + if getattr(ns, "all", False): + display_all_test_result(test_result[0], test_result[1]) + elif getattr(ns, "all_failures", False): + display_failures_test_result(test_result[0], test_result[1]) diff --git a/scripts/text_colorize.py b/scripts/text_colorize.py new file mode 100644 index 0000000..ce38225 --- /dev/null +++ b/scripts/text_colorize.py @@ -0,0 +1,20 @@ +import enum + +class ANSIColors(enum.IntEnum): + PURPLE = 35 + BLUE = 34 + GREEN = 32 + YELLOW = 33 + RED = 31 + BLACK = 30 + WHITE = 37 + + +class TextStyle(enum.IntEnum): + SIMPLE = 0 + BOLD = 1 + ITALIC = 3 + + +def colorize(text: str, color: ANSIColors, style: TextStyle = TextStyle.SIMPLE) -> str: + return f"\033[{style};{color}m{text}\033[0m" diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..7de2ad7 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,15 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.6/userguide/multi_project_builds.html in the Gradle documentation. + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0" +} + +rootProject.name = "tree-trippers" +include("lib")