diff --git a/build.gradle.kts b/build.gradle.kts index 501302a..2cfb061 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { testImplementation(Config.Libs.junit) testImplementation(Config.Libs.kluent) + testImplementation(Config.Libs.mockk) // testImplementation(Config.Libs.spekDsl) // testRuntimeOnly(Config.Libs.kotlinReflect) diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 18f2e2c..b8ca9df 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -31,6 +31,7 @@ object Config { val junit = "5.1.0" val spek = "2.0.1" val kotlin = "1.3.21." + val mockk = "1.9.2" } object Libs { @@ -41,5 +42,6 @@ object Config { val spekDsl = "org.spekframework.spek2:spek-dsl-jvm:${Versions.spek}" val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect:${Versions.kotlin}" val spekRunner = "org.spekframework.spek2:spek-runner-junit5:${Versions.spek}" + val mockk = "io.mockk:mockk:${Versions.mockk}" } } \ No newline at end of file diff --git a/src/main/kotlin/it/norangeb/algorithms/exercises/BranchAndMerge.kt b/src/main/kotlin/it/norangeb/algorithms/exercises/BranchAndMerge.kt new file mode 100644 index 0000000..4c49c9d --- /dev/null +++ b/src/main/kotlin/it/norangeb/algorithms/exercises/BranchAndMerge.kt @@ -0,0 +1,163 @@ +/* + * MIT License + * + * Copyright (c) 2019 norangebit + * + * 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, tryMerge, 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. + * + */ + +package it.norangeb.algorithms.exercises + +import java.io.File +import java.io.PrintStream +import java.util.Scanner + +object BranchAndMerge { + private const val TMP_FILE_PATH_1 = "/tmp/file1" + private const val TMP_FILE_PATH_2 = "/tmp/file2" + + fun run(source: File) { + tryBranch(source) + + while (!isSorted()) { + tryMerge(source) + tryBranch(source) + } + } + + private fun isSorted(): Boolean { + val sc1 = Scanner(File(TMP_FILE_PATH_1)) + val sc2 = Scanner(File(TMP_FILE_PATH_2)) + return !sc1.hasNextInt() || !sc2.hasNextInt() + } + + private fun tryBranch(fileToSort: File) { + try { + val destination1 = PrintStream(File(TMP_FILE_PATH_1)) + val destination2 = PrintStream(File(TMP_FILE_PATH_2)) + val source = Scanner(fileToSort) + + branch(source, destination1, destination2) + + destination1.close() + destination2.close() + source.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun branch( + source: Scanner, + destination1: PrintStream, + destination2: PrintStream + ) { + var destination = destination1 + var current: Int + var last: Int? = null + + while (source.hasNextInt()) { + current = source.nextInt() + if (isLess(current, last)) + destination = switchDestination( + destination, destination1, destination2 + ) + destination.println(current) + last = current + } + } + + private fun tryMerge(fileToSort: File) { + try { + val source1 = Scanner(File(TMP_FILE_PATH_1)) + val source2 = Scanner(File(TMP_FILE_PATH_2)) + val destination = PrintStream(fileToSort) + + merge(source1, source2, destination) + + source1.close() + source2.close() + destination.close() + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun merge(source1: Scanner, source2: Scanner, destination: PrintStream) { + var source = source1 + + var current: Int + var cache: Int? = null + + while (source1.hasNextInt() && source2.hasNextInt()) { + if (cache == null) + cache = switchSource(source, source1, source2).nextInt() + + current = source.nextInt() + + if (!isLess(current, cache)) { + source = switchSource(source, source1, source2) + current = cache.also { cache = current } + } + + destination.println(current) + } + destination.println(cache) + + mergeTail(source1, source2, destination) + } + + private fun mergeTail( + source1: Scanner, + source2: Scanner, + destination: PrintStream + ) { + while (source1.hasNextInt()) + destination.println(source1.nextInt()) + + while (source2.hasNextInt()) + destination.println(source2.nextInt()) + } + + private fun isLess(current: Int, last: Int?): Boolean { + if (last == null) + return false + + return current < last + } + + private fun switchDestination( + currentDestination: PrintStream, + destination1: PrintStream, + destination2: PrintStream + ) = when (currentDestination) { + destination1 -> destination2 + else -> destination1 + } + + private fun switchSource( + currentSource: Scanner, + source1: Scanner, + source2: Scanner + ) = when (currentSource) { + source1 -> source2 + else -> source1 + } +} \ No newline at end of file diff --git a/src/test/kotlin/it/norangeb/algorithms/exercises/BranchAndMergeTest.kt b/src/test/kotlin/it/norangeb/algorithms/exercises/BranchAndMergeTest.kt new file mode 100644 index 0000000..529907b --- /dev/null +++ b/src/test/kotlin/it/norangeb/algorithms/exercises/BranchAndMergeTest.kt @@ -0,0 +1,247 @@ +/* + * MIT License + * + * Copyright (c) 2019 norangebit + * + * 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. + * + */ + +package it.norangeb.algorithms.exercises + +import io.mockk.every +import io.mockk.mockk +import org.amshove.kluent.`should equal` +import org.junit.jupiter.api.Test +import java.io.File +import java.io.PrintStream +import java.util.Scanner +import kotlin.reflect.full.memberFunctions +import kotlin.reflect.jvm.isAccessible + +class BranchAndMergeTest { + + @Test + fun branchTest() { + val branch = BranchAndMerge::class.memberFunctions + .find { it.name == "branch" } + .also { it?.isAccessible = true } + + val list1 = arrayListOf() + val list2 = arrayListOf() + val source = mockk() + val destination1 = mockk() + val destination2 = mockk() + + every { source.hasNextInt() } returnsMany + listOf(true, true, true, true, true, false) + every { source.nextInt() } returnsMany listOf(3, 1, 4, 2, 0) + every { destination1.println(any() as Int) } answers { list1.add(arg(0)) } + every { destination2.println(any() as Int) } answers { list2.add(arg(0)) } + + branch?.call(BranchAndMerge, source, destination1, destination2) + + list1.toIntArray() `should equal` intArrayOf(3, 2) + list2.toIntArray() `should equal` intArrayOf(1, 4, 0) + } + + @Test + fun noBranchTest() { + val branch = BranchAndMerge::class.memberFunctions + .find { it.name == "branch" } + .also { it?.isAccessible = true } + + val list1 = arrayListOf() + val list2 = arrayListOf() + val source = mockk() + val destination1 = mockk() + val destination2 = mockk() + + every { source.hasNextInt() } returnsMany + listOf(true, true, true, true, true, false) + every { source.nextInt() } returnsMany listOf(1, 2, 3, 10, 12) + every { destination1.println(any() as Int) } answers { list1.add(arg(0)) } + + branch?.call(BranchAndMerge, source, destination1, destination2) + + list1.toIntArray() `should equal` intArrayOf(1, 2, 3, 10, 12) + list2.toIntArray() `should equal` intArrayOf() + } + + @Test + fun mergeTest() { + val merge = BranchAndMerge::class.memberFunctions + .find { it.name == "merge" } + .also { it?.isAccessible = true } + + val list = arrayListOf() + val source1 = mockk() + val source2 = mockk() + val destination = mockk() + + every { source1.hasNextInt() } returnsMany + listOf(true, true, true, false) + every { source1.nextInt() } returnsMany listOf(3, 2) + every { source2.hasNextInt() } returnsMany + listOf(true, true, true, true, false) + every { source2.nextInt() } returnsMany listOf(1, 4, 0) + every { destination.println(any() as Int) } answers { list.add(arg(0)) } + every { destination.println(any() as Int?) } answers { list.add(arg(0)) } + + merge?.call(BranchAndMerge, source1, source2, destination) + + list.toIntArray() `should equal` intArrayOf(1, 3, 2, 4, 0) + } + + @Test + fun switchSourceTest() { + val switchSource = BranchAndMerge::class.memberFunctions + .find { it.name == "switchSource" } + .also { it?.isAccessible = true } + + val source1 = mockk() + val source2 = mockk() + + val result1 = switchSource?.call( + BranchAndMerge, source1, source1, source2 + ) + val result2 = switchSource?.call( + BranchAndMerge, source2, source1, source2 + ) + + result1 `should equal` source2 + result2 `should equal` source1 + } + + @Test + fun switchDestinationTest() { + val switchDestination = BranchAndMerge::class.memberFunctions + .find { it.name == "switchDestination" } + .also { it?.isAccessible = true } + + val destination1 = mockk() + val destination2 = mockk() + + val result1 = switchDestination?.call( + BranchAndMerge, destination1, destination1, destination2 + ) + val result2 = switchDestination?.call( + BranchAndMerge, destination2, destination1, destination2 + ) + + result1 `should equal` destination2 + result2 `should equal` destination1 + } + + @Test + fun mergeTailEmptyTest() { + val mergeTail = BranchAndMerge::class.memberFunctions + .find { it.name == "mergeTail" } + .also { it?.isAccessible = true } + + val list = arrayListOf() + val source1 = mockk() + val source2 = mockk() + val destination = mockk() + + every { source1.hasNextInt() } returns false + every { source2.hasNextInt() } returns false + every { destination.println(any() as Int) } answers { list.add(arg(0)) } + + mergeTail?.call( + BranchAndMerge, source1, source2, destination + ) + + list.toIntArray() `should equal` intArrayOf() + } + + @Test + fun mergeTailFirstEmptyTest() { + val mergeTail = BranchAndMerge::class.memberFunctions + .find { it.name == "mergeTail" } + .also { it?.isAccessible = true } + + val list = arrayListOf() + val source1 = mockk() + val source2 = mockk() + val destination = mockk() + + every { source1.hasNextInt() } returns false + every { source2.hasNextInt() } returnsMany listOf(true, true, false) + every { source2.nextInt() } returnsMany listOf(3, 7) + every { destination.println(any() as Int) } answers { list.add(arg(0)) } + + mergeTail?.call( + BranchAndMerge, source1, source2, destination + ) + + list.toIntArray() `should equal` intArrayOf(3, 7) + } + + @Test + fun mergeTailSecondEmptyTest() { + val mergeTail = BranchAndMerge::class.memberFunctions + .find { it.name == "mergeTail" } + .also { it?.isAccessible = true } + + val list = arrayListOf() + val source1 = mockk() + val source2 = mockk() + val destination = mockk() + + every { source1.hasNextInt() } returnsMany listOf(true, false) + every { source1.nextInt() } returns 1 + every { source2.hasNextInt() } returns false + every { destination.println(any() as Int) } answers { list.add(arg(0)) } + + mergeTail?.call( + BranchAndMerge, source1, source2, destination + ) + + list.toIntArray() `should equal` intArrayOf(1) + } + + @Test + fun sortAlreadySortedTest() { + val path = "/tmp/test" + val ps = PrintStream(path) + val testSet = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 14, 16, 21, 34, 45) + + testSet.forEach { ps.println(it) } + ps.close() + + BranchAndMerge.run(File(path)) + + File(path).readLines().map { it.toInt() } `should equal` testSet + } + + @Test + fun sortTest() { + val path = "/tmp/test" + val ps = PrintStream(path) + val testSet = listOf(6, 2, 39, 4, 5, 11, 7, 99, 9, 10, 3, 16, 1, 45, 44) + + testSet.forEach { ps.println(it) } + ps.close() + + BranchAndMerge.run(File(path)) + + File(path).readLines().map { it.toInt() } `should equal` testSet.sorted() + } +} \ No newline at end of file