From 6ca58249447594775efcebfb260ac7e004c84c72 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Sun, 21 May 2023 23:06:37 +0800 Subject: [PATCH] moved to Kotlin multiplatform --- Jenkinsfile | 4 +- benchmark/build.gradle | 24 +++++-- .../woggioni/klevtree/benchmark/Benchmark.kt | 0 .../resources/cracklib-small | 0 build.gradle | 60 +++++++++-------- gradle.properties | 3 +- .../kotlin/net.woggioni.klevtree}/CharTrie.kt | 0 .../DistanceCalculator.kt | 16 +++-- .../kotlin/net.woggioni.klevtree}/LevTrie.kt | 8 ++- .../kotlin/net.woggioni.klevtree}/Trie.kt | 4 +- .../kotlin/net.woggioni.klevtree}/WordTrie.kt | 0 .../net.woggioni.klevtree}/node/nodes.kt | 2 +- .../tree/TreeNodeVisitor.kt | 64 +++++++++++++++++++ .../net.woggioni.klevtree/tree/TreeWalker.kt | 48 ++++++++++++++ .../net/woggioni/klevtree/LevtreeTest.kt | 50 ++++++++------- .../java9 => jvmMain/java}/module-info.java | 2 +- src/test/resources/log4j2.xml | 17 ----- 17 files changed, 214 insertions(+), 88 deletions(-) rename benchmark/src/{main => jvmMain}/kotlin/net/woggioni/klevtree/benchmark/Benchmark.kt (100%) rename benchmark/src/{main => jvmMain}/resources/cracklib-small (100%) rename src/{main/kotlin/net/woggioni/klevtree => commonMain/kotlin/net.woggioni.klevtree}/CharTrie.kt (100%) rename src/{main/kotlin/net/woggioni/klevtree => commonMain/kotlin/net.woggioni.klevtree}/DistanceCalculator.kt (82%) rename src/{main/kotlin/net/woggioni/klevtree => commonMain/kotlin/net.woggioni.klevtree}/LevTrie.kt (91%) rename src/{main/kotlin/net/woggioni/klevtree => commonMain/kotlin/net.woggioni.klevtree}/Trie.kt (98%) rename src/{main/kotlin/net/woggioni/klevtree => commonMain/kotlin/net.woggioni.klevtree}/WordTrie.kt (100%) rename src/{main/kotlin/net/woggioni/klevtree => commonMain/kotlin/net.woggioni.klevtree}/node/nodes.kt (96%) create mode 100644 src/commonMain/kotlin/net.woggioni.klevtree/tree/TreeNodeVisitor.kt create mode 100644 src/commonMain/kotlin/net.woggioni.klevtree/tree/TreeWalker.kt rename src/{test => commonTest}/kotlin/net/woggioni/klevtree/LevtreeTest.kt (63%) rename src/{main/java9 => jvmMain/java}/module-info.java (68%) delete mode 100644 src/test/resources/log4j2.xml diff --git a/Jenkinsfile b/Jenkinsfile index 8e3a24b..947f645 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -7,8 +7,8 @@ pipeline { stage("Build") { steps { sh "./gradlew clean build" - junit testResults: "build/test-results/test/*.xml" - archiveArtifacts artifacts: 'build/libs/*.jar,benchmark/build/libs/*.jar', + junit testResults: "build/test-results/*Test/*.xml" + archiveArtifacts artifacts: 'build/libs/*.jar,build/libs/*.klib,benchmark/build/distributions/*.jar', allowEmptyArchive: true, fingerprint: true, onlyIfSuccessful: true diff --git a/benchmark/build.gradle b/benchmark/build.gradle index a4bf803..cb8e555 100644 --- a/benchmark/build.gradle +++ b/benchmark/build.gradle @@ -1,13 +1,27 @@ plugins { - alias catalog.plugins.kotlin.jvm - alias catalog.plugins.envelope + alias catalog.plugins.kotlin.multiplatform + alias catalog.plugins.envelope apply false +} + +import net.woggioni.gradle.envelope.EnvelopeJarTask + +kotlin { + jvm() } dependencies { - implementation catalog.jwo - implementation(rootProject) + jvmMainImplementation catalog.jwo + jvmMainImplementation(rootProject) } -envelopeJar { +Provider envelopeJarTaskProvider = project.tasks.register("envelopeJar", EnvelopeJarTask.class) { + group = BasePlugin.BUILD_GROUP + description = "Package the application in a single executable jar file" + includeLibraries(project.configurations.named("jvmRuntimeClasspath")) + includeLibraries(project.getTasks().named("jvmJar", Jar.class)) mainClass = "net.woggioni.klevtree.benchmark.BenchmarkKt" } + +project.tasks.named(BasePlugin.ASSEMBLE_TASK_NAME, DefaultTask.class) { + dependsOn(envelopeJarTaskProvider) +} diff --git a/benchmark/src/main/kotlin/net/woggioni/klevtree/benchmark/Benchmark.kt b/benchmark/src/jvmMain/kotlin/net/woggioni/klevtree/benchmark/Benchmark.kt similarity index 100% rename from benchmark/src/main/kotlin/net/woggioni/klevtree/benchmark/Benchmark.kt rename to benchmark/src/jvmMain/kotlin/net/woggioni/klevtree/benchmark/Benchmark.kt diff --git a/benchmark/src/main/resources/cracklib-small b/benchmark/src/jvmMain/resources/cracklib-small similarity index 100% rename from benchmark/src/main/resources/cracklib-small rename to benchmark/src/jvmMain/resources/cracklib-small diff --git a/build.gradle b/build.gradle index 913d458..9e18b43 100644 --- a/build.gradle +++ b/build.gradle @@ -1,9 +1,10 @@ plugins { id 'maven-publish' - alias catalog.plugins.kotlin.jvm - alias catalog.plugins.multi.release.jar + alias catalog.plugins.kotlin.multiplatform } +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions allprojects { group = "net.woggioni" @@ -17,34 +18,41 @@ allprojects { } } -ext { - setProperty('jpms.module.name', 'net.woggioni.klevtree') -} - -dependencies { - implementation catalog.jwo - - testImplementation catalog.junit.jupiter.api - testImplementation catalog.junit.jupiter.params - testRuntimeOnly catalog.junit.jupiter.engine - testRuntimeOnly catalog.log4j.slf4j.impl -} - -compileJava { - options.release = 8 - options.compilerArgs << '-parameters' -} - -compileKotlin { - kotlinOptions.with { - jvmTarget = '1.8' +kotlin { + sourceSets { + commonTest { + dependencies { + implementation group: 'org.jetbrains.kotlin', + name: 'kotlin-test', + version: catalog.versions.kotlin.get() + } + } + } + jvm { + jvmToolchain(17) + withJava() + compilations.main { + Action action = { KotlinJvmCompilerOptions kjco -> + kjco.javaParameters = true + kjco.jvmTarget = JvmTarget.JVM_1_8 + kjco.moduleName = "net.woggioni.klevtree" + } + compilerOptions.configure(action) + } + } + linuxX64() + js(IR) { + nodejs() } } -test { - useJUnitPlatform() +compileJava { + options.release = 11 + String patchString = 'net.woggioni.klevtree=' + kotlin.targets.jvm.compilations.main.output.classesDirs.asPath + options.compilerArgs << '--patch-module' << patchString } + publishing { repositories { maven { @@ -53,7 +61,7 @@ publishing { } publications { maven(MavenPublication) { - from(components["java"]) + from(components.kotlin) } } } diff --git a/gradle.properties b/gradle.properties index 9928574..194aad4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,7 @@ kotlin.code.style=official +kotlin.jvm.target.validation.mode=ignore woggioniMavenRepositoryUrl=https://woggioni.net/mvn/ klevtree.version = 2023.03 -lys.version = 2023.05 +lys.version = 2023.05.20 diff --git a/src/main/kotlin/net/woggioni/klevtree/CharTrie.kt b/src/commonMain/kotlin/net.woggioni.klevtree/CharTrie.kt similarity index 100% rename from src/main/kotlin/net/woggioni/klevtree/CharTrie.kt rename to src/commonMain/kotlin/net.woggioni.klevtree/CharTrie.kt diff --git a/src/main/kotlin/net/woggioni/klevtree/DistanceCalculator.kt b/src/commonMain/kotlin/net.woggioni.klevtree/DistanceCalculator.kt similarity index 82% rename from src/main/kotlin/net/woggioni/klevtree/DistanceCalculator.kt rename to src/commonMain/kotlin/net.woggioni.klevtree/DistanceCalculator.kt index bd6d81e..86f3e5e 100644 --- a/src/main/kotlin/net/woggioni/klevtree/DistanceCalculator.kt +++ b/src/commonMain/kotlin/net.woggioni.klevtree/DistanceCalculator.kt @@ -1,12 +1,14 @@ package net.woggioni.klevtree -import net.woggioni.jwo.TreeNodeVisitor +import net.woggioni.klevtree.tree.TreeNodeVisitor +import kotlin.math.min + sealed class DistanceCalculator { abstract fun compute(keyChecker : Trie.Keychecker, - stack: List>, - wordkey: String, - worstCase : Int) : TreeNodeVisitor.VisitOutcome + stack: List>, + wordkey: String, + worstCase : Int) : TreeNodeVisitor.VisitOutcome object LevenshteinDistanceCalculator : DistanceCalculator() { override fun compute(keyChecker : Trie.Keychecker, @@ -21,7 +23,7 @@ sealed class DistanceCalculator { if(keyChecker.check(wordkey[i - 1], currentStackElement.node.key)) { currentRow[i] = previousRow[i - 1] } else { - currentRow[i] = Math.min(Math.min(currentRow[i - 1], previousRow[i -1]), previousRow[i]) + 1 + currentRow[i] = min(min(currentRow[i - 1], previousRow[i -1]), previousRow[i]) + 1 } } return if(worstCase >= 0 && worstCase <= currentRow.minOrNull()!!) { @@ -45,13 +47,13 @@ sealed class DistanceCalculator { if (keyChecker.check(wordkey[i - 1], cse.node.key)) { crow[i] = prow[i - 1] } else { - crow[i] = Math.min(Math.min(crow[i - 1], prow[i - 1]), prow[i]) + 1 + crow[i] = min(min(crow[i - 1], prow[i - 1]), prow[i]) + 1 } if (stack.size > 2 && i > 1 && keyChecker.check(wordkey[i - 2], cse.node.key) && keyChecker.check(wordkey[i - 1], pse.node.key)) { val ppse = stack[stack.size - 3] val pprow: IntArray = ppse.node.payload!! - crow[i] = Math.min(crow[i], pprow[i - 2] + 1) + crow[i] = min(crow[i], pprow[i - 2] + 1) } } return if(worstCase >= 0 && worstCase <= prow.minOrNull()!!) { diff --git a/src/main/kotlin/net/woggioni/klevtree/LevTrie.kt b/src/commonMain/kotlin/net.woggioni.klevtree/LevTrie.kt similarity index 91% rename from src/main/kotlin/net/woggioni/klevtree/LevTrie.kt rename to src/commonMain/kotlin/net.woggioni.klevtree/LevTrie.kt index 062896a..19397c0 100644 --- a/src/main/kotlin/net/woggioni/klevtree/LevTrie.kt +++ b/src/commonMain/kotlin/net.woggioni.klevtree/LevTrie.kt @@ -1,9 +1,9 @@ package net.woggioni.klevtree -import net.woggioni.jwo.TreeNodeVisitor -import net.woggioni.jwo.TreeWalker import net.woggioni.klevtree.node.CharNode import net.woggioni.klevtree.node.TrieNode +import net.woggioni.klevtree.tree.TreeNodeVisitor +import net.woggioni.klevtree.tree.TreeWalker internal typealias LevNode = TrieNode @@ -41,7 +41,8 @@ class LevTrie : CharTrie() { } fun fuzzySearch(word : String, maxResult: Int) : List> { - val result = sortedSetOf>(compareBy({ it.second }, { it.first })) + val comparator : Comparator> = compareBy({ it.second }, { it.first }) + val result = mutableListOf>() val requiredSize = word.length + 1 val visitor = object: TreeNodeVisitor { override fun visitPre(stack: List>): TreeNodeVisitor.VisitOutcome { @@ -64,6 +65,7 @@ class LevTrie : CharTrie() { val candidate = sb.toString() val distance = stack[stack.size - 2].node.payload!![word.length] result.add(candidate to distance) + result.sortWith(comparator) if(result.size > maxResult) { result.remove(result.last()) } diff --git a/src/main/kotlin/net/woggioni/klevtree/Trie.kt b/src/commonMain/kotlin/net.woggioni.klevtree/Trie.kt similarity index 98% rename from src/main/kotlin/net/woggioni/klevtree/Trie.kt rename to src/commonMain/kotlin/net.woggioni.klevtree/Trie.kt index 289466f..beacc9e 100644 --- a/src/main/kotlin/net/woggioni/klevtree/Trie.kt +++ b/src/commonMain/kotlin/net.woggioni.klevtree/Trie.kt @@ -1,8 +1,8 @@ package net.woggioni.klevtree -import net.woggioni.jwo.TreeNodeVisitor -import net.woggioni.jwo.TreeWalker import net.woggioni.klevtree.node.TrieNode +import net.woggioni.klevtree.tree.TreeNodeVisitor +import net.woggioni.klevtree.tree.TreeWalker abstract class Trie, KEY, PAYLOAD> { diff --git a/src/main/kotlin/net/woggioni/klevtree/WordTrie.kt b/src/commonMain/kotlin/net.woggioni.klevtree/WordTrie.kt similarity index 100% rename from src/main/kotlin/net/woggioni/klevtree/WordTrie.kt rename to src/commonMain/kotlin/net.woggioni.klevtree/WordTrie.kt diff --git a/src/main/kotlin/net/woggioni/klevtree/node/nodes.kt b/src/commonMain/kotlin/net.woggioni.klevtree/node/nodes.kt similarity index 96% rename from src/main/kotlin/net/woggioni/klevtree/node/nodes.kt rename to src/commonMain/kotlin/net.woggioni.klevtree/node/nodes.kt index 7d4b863..ceedad5 100644 --- a/src/main/kotlin/net/woggioni/klevtree/node/nodes.kt +++ b/src/commonMain/kotlin/net.woggioni.klevtree/node/nodes.kt @@ -1,6 +1,6 @@ package net.woggioni.klevtree.node -import net.woggioni.jwo.TreeNodeVisitor +import net.woggioni.klevtree.tree.TreeNodeVisitor open class TrieNode(val key : T?) : TreeNodeVisitor.TreeNode> { diff --git a/src/commonMain/kotlin/net.woggioni.klevtree/tree/TreeNodeVisitor.kt b/src/commonMain/kotlin/net.woggioni.klevtree/tree/TreeNodeVisitor.kt new file mode 100644 index 0000000..047d77d --- /dev/null +++ b/src/commonMain/kotlin/net.woggioni.klevtree/tree/TreeNodeVisitor.kt @@ -0,0 +1,64 @@ +package net.woggioni.klevtree.tree + +/** + * This interface must be implemented by the user of [TreeWalker] and its methods will be called by + * [TreeWalker.walk]. The methods will receive as an input a list of [StackContext] + * instances each one correspond to a node in the tree, each node is preceded in the list + * by its parents in the tree. Each instance has a method, [StackContext.context] + * to set a custom object that can be used in the [.visitPre] method and the method + * [StackContext.context] that can be used in the [.visitPost] method to retrieve + * the same instance. This is to provide support for algorithms that require both pre-order and post-order logic. + * The last element of the list corresponds to the node currently being traversed. + * @param the type of the context object used + */ +interface TreeNodeVisitor, T> { + interface TreeNode> { + fun children(): Iterator? + } + + /** + * This interface exposes the methods that are visible to the user of + * [TreeWalker], it allows to + * set/get a custom object in the current stack context or to get the current link's Aci + * @param the type of the context object used + */ + interface StackContext?, T> { + /** + * @return the current user object + */ + /** + * @param ctx the user object to set for this stack level + */ + var context: T? + + /** + * @return the current TreeNode + */ + val node: NODE + } + + enum class VisitOutcome { + CONTINUE, + SKIP, + EARLY_EXIT + } + + /** + * This method will be called for each link using + * [a Depth-first pre-oder algorithm](https://en.wikipedia.org/wiki/Tree_traversal#Pre-order_(NLR)) + * @param stack is a list of [StackContext] instances corresponding to the full path from the root to the + * current node in the tree + * @return a boolean that will be used to decide whether to traverse the subtree rooted in the current link or not + */ + fun visitPre(stack: List>): VisitOutcome { + return VisitOutcome.CONTINUE + } + + /** + * This method will be called for each node using + * [a Depth-first post-oder algorithm](https://en.wikipedia.org/wiki/Tree_traversal#Post-order_(LRN)) + * @param stack is a list of [StackContext] instances corresponding to the full path from the root to the + * current node in the tree + */ + fun visitPost(stack: List>) {} +} diff --git a/src/commonMain/kotlin/net.woggioni.klevtree/tree/TreeWalker.kt b/src/commonMain/kotlin/net.woggioni.klevtree/tree/TreeWalker.kt new file mode 100644 index 0000000..c62c579 --- /dev/null +++ b/src/commonMain/kotlin/net.woggioni.klevtree/tree/TreeWalker.kt @@ -0,0 +1,48 @@ +package net.woggioni.klevtree.tree + + +class TreeWalker, T>( + private val visitor: TreeNodeVisitor +) { + + private class StackElement, T>(override val node: NODE) : + TreeNodeVisitor.StackContext { + override var context: T? = null + var childrenIterator: Iterator? = null + } + + /** + * This methods does the actual job of traversing the tree calling the methods of the provided + * [TreeNodeVisitor] instance + * @param root the root node of the tree + */ + fun walk(root: NODE) { + val stack: MutableList> = mutableListOf() + val rootStackElement = StackElement(root) + stack.add(rootStackElement) + val publicStack: List> = stack + when (visitor.visitPre(publicStack)) { + TreeNodeVisitor.VisitOutcome.CONTINUE -> rootStackElement.childrenIterator = root.children() + TreeNodeVisitor.VisitOutcome.SKIP -> rootStackElement.childrenIterator = null + TreeNodeVisitor.VisitOutcome.EARLY_EXIT -> return + } + while (stack.isNotEmpty()) { + val lastElement: StackElement = stack.last() + val childrenIterator = lastElement.childrenIterator + if (childrenIterator != null && childrenIterator.hasNext()) { + val childNode = childrenIterator.next() + val childStackElement = StackElement(childNode) + stack.add(childStackElement) + when (visitor.visitPre(publicStack)) { + TreeNodeVisitor.VisitOutcome.CONTINUE -> childStackElement.childrenIterator = childNode.children() + TreeNodeVisitor.VisitOutcome.SKIP -> childStackElement.childrenIterator = null + TreeNodeVisitor.VisitOutcome.EARLY_EXIT -> return + } + } else { + visitor.visitPost(publicStack) + stack.removeLast() + } + } + } +} + diff --git a/src/test/kotlin/net/woggioni/klevtree/LevtreeTest.kt b/src/commonTest/kotlin/net/woggioni/klevtree/LevtreeTest.kt similarity index 63% rename from src/test/kotlin/net/woggioni/klevtree/LevtreeTest.kt rename to src/commonTest/kotlin/net/woggioni/klevtree/LevtreeTest.kt index 6b2f0cd..4cd7fb5 100644 --- a/src/test/kotlin/net/woggioni/klevtree/LevtreeTest.kt +++ b/src/commonTest/kotlin/net/woggioni/klevtree/LevtreeTest.kt @@ -1,7 +1,11 @@ package net.woggioni.klevtree -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + class LevtreeTest { @@ -13,23 +17,23 @@ class LevtreeTest { val word = "dailies" run { val pair = tree.add(word) - Assertions.assertTrue(pair.first) + assertTrue(pair.first) val node = tree.search(word) - Assertions.assertNotNull(node) - Assertions.assertEquals( + assertNotNull(node) + assertEquals( word, - node!!.linealDescendant().fold(StringBuilder(), StringBuilder::append).toString() + node.linealDescendant().fold(StringBuilder(), StringBuilder::append).toString() ) val result = tree.fuzzySearch(word, 5) - Assertions.assertEquals(1, result.size) - Assertions.assertEquals(word to 0, result[0]) + assertEquals(1, result.size) + assertEquals(word to 0, result[0]) } run { tree.remove(word) val node = tree.search(word) - Assertions.assertNull(node) + assertNull(node) val result = tree.fuzzySearch(word, 5) - Assertions.assertEquals(0, result.size) + assertEquals(0, result.size) } } @@ -63,21 +67,21 @@ class LevtreeTest { run { val word = "fired" val result = tree.fuzzySearch(word, 4) - Assertions.assertEquals(4, result.size) - Assertions.assertEquals("tired" to 1, result[0]) + assertEquals(4, result.size) + assertEquals("tired" to 1, result[0]) } run { val word = "tierd" val result = tree.fuzzySearch(word, 4) - Assertions.assertEquals(4, result.size) - Assertions.assertEquals("tired" to 2, result[0]) + assertEquals(4, result.size) + assertEquals("tired" to 2, result[0]) } run { val word = "tierd" tree.remove("tired") val result = tree.fuzzySearch(word, 4) - Assertions.assertEquals(4, result.size) - Assertions.assertEquals("trail" to 4, result[0]) + assertEquals(4, result.size) + assertEquals("trail" to 4, result[0]) } } @@ -89,22 +93,22 @@ class LevtreeTest { run { val word = "fired" val result = tree.fuzzySearch(word, 4) - Assertions.assertEquals(4, result.size) - Assertions.assertEquals("tired" to 1, result[0]) + assertEquals(4, result.size) + assertEquals("tired" to 1, result[0]) } run { val word = "capitvate" val result = tree.fuzzySearch(word, 4) - Assertions.assertEquals(4, result.size) - Assertions.assertEquals("captivate" to 1, result[0]) - Assertions.assertEquals("captivity" to 3, result[1]) + assertEquals(4, result.size) + assertEquals("captivate" to 1, result[0]) + assertEquals("captivity" to 3, result[1]) } run { tree.remove("captivate") val word = "capitvate" val result = tree.fuzzySearch(word, 4) - Assertions.assertEquals(4, result.size) - Assertions.assertEquals("captivity" to 3, result[0]) + assertEquals(4, result.size) + assertEquals("captivity" to 3, result[0]) } } } \ No newline at end of file diff --git a/src/main/java9/module-info.java b/src/jvmMain/java/module-info.java similarity index 68% rename from src/main/java9/module-info.java rename to src/jvmMain/java/module-info.java index 5adf97e..8421deb 100644 --- a/src/main/java9/module-info.java +++ b/src/jvmMain/java/module-info.java @@ -1,4 +1,4 @@ module net.woggioni.klevtree { - requires net.woggioni.jwo; + requires kotlin.stdlib; exports net.woggioni.klevtree; } \ No newline at end of file diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml deleted file mode 100644 index 47eaefc..0000000 --- a/src/test/resources/log4j2.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file