diff --git a/README.md b/README.md index 480e6332..c1025160 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,16 @@ Toolbox of utilities/helpers for Kotlin development. - [`coroutines`](coroutines) - Tools/utilities for [Coroutines] ![badge][badge-js] ![badge][badge-jvm] -![badge][badge-macos] +![badge][badge-mac] + +- [`functional`](functional) - Tools/utilities for manipulating functions +![badge][badge-js] +![badge][badge-jvm] - [`test`](test) - Utilities for test suites ![badge][badge-js] ![badge][badge-jvm] -![badge][badge-macos] +![badge][badge-mac] [Collections]: https://kotlinlang.org/docs/reference/collections-overview.html diff --git a/codecov.yml b/codecov.yml index d63a59b6..5b5c9b56 100644 --- a/codecov.yml +++ b/codecov.yml @@ -4,3 +4,4 @@ coverage: fixes: - "com/juul/tuulbox/collections::" - "com/juul/tuulbox/coroutines/flow::" + - "com/juul/tuulbox/functional::" diff --git a/functional/README.md b/functional/README.md new file mode 100644 index 00000000..c3689a22 --- /dev/null +++ b/functional/README.md @@ -0,0 +1,61 @@ +![badge-js] +![badge-jvm] + +# Functional + +Tool box with utilities for manipulating functions. +For a full functional ecosystem, complete with `Monad` and the like, prefer [Arrow](https://arrow-kt.io/). + +## Installation + +### Gradle + +Artifacts are hosted on GitHub packages, which can be configured as follows: + +```kotlin +import java.net.URI + +repositories { + maven { + url = URI("https://maven.pkg.github.com/juullabs/android-github-packages") + credentials { + username = findProperty("github.packages.username") as String + password = findProperty("github.packages.password") as String + } + } +} +``` + +Then the needed artifact(s) can be defined as dependencies. + +**Multiplatform projects** + +```kotlin +kotlin { + sourceSets { + val commonMain by getting { + implementation("com.juul.tuulbox:functional:$version") + } + } +} +``` + +**Platform-specific projects** + +```kotlin +dependencies { + implementation("com.juul.tuulbox:functional-$platform:$version") +} +``` + + +[badge-android]: http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat +[badge-ios]: http://img.shields.io/badge/platform-ios-CDCDCD.svg?style=flat +[badge-js]: http://img.shields.io/badge/platform-js-F8DB5D.svg?style=flat +[badge-jvm]: http://img.shields.io/badge/platform-jvm-DB413D.svg?style=flat +[badge-linux]: http://img.shields.io/badge/platform-linux-2D3F6C.svg?style=flat +[badge-windows]: http://img.shields.io/badge/platform-windows-4D76CD.svg?style=flat +[badge-mac]: http://img.shields.io/badge/platform-macos-111111.svg?style=flat +[badge-watchos]: http://img.shields.io/badge/platform-watchos-C0C0C0.svg?style=flat +[badge-tvos]: http://img.shields.io/badge/platform-tvos-808080.svg?style=flat +[badge-wasm]: https://img.shields.io/badge/platform-wasm-624FE8.svg?style=flat diff --git a/functional/api/functional.api b/functional/api/functional.api new file mode 100644 index 00000000..81a63bfe --- /dev/null +++ b/functional/api/functional.api @@ -0,0 +1,59 @@ +public final class com/juul/tuulbox/functional/ComposeKt { + public static final fun of (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1; + public static final fun then (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1; +} + +public final class com/juul/tuulbox/functional/CurryKt { + public static final fun curry (Lkotlin/jvm/functions/Function2;)Lkotlin/jvm/functions/Function1; + public static final fun curry (Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function1; + public static final fun curry (Lkotlin/jvm/functions/Function4;)Lkotlin/jvm/functions/Function1; + public static final fun curry (Lkotlin/jvm/functions/Function5;)Lkotlin/jvm/functions/Function1; + public static final fun curry (Lkotlin/jvm/functions/Function6;)Lkotlin/jvm/functions/Function1; + public static final fun curryFirst (Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function1; + public static final fun curryFirst (Lkotlin/jvm/functions/Function4;)Lkotlin/jvm/functions/Function1; + public static final fun curryFirst (Lkotlin/jvm/functions/Function5;)Lkotlin/jvm/functions/Function1; + public static final fun curryFirst (Lkotlin/jvm/functions/Function6;)Lkotlin/jvm/functions/Function1; +} + +public final class com/juul/tuulbox/functional/MemoizeKt { + public static final fun memoize (Lkotlin/jvm/functions/Function0;)Lkotlin/jvm/functions/Function0; + public static final fun memoize (Lkotlin/jvm/functions/Function1;)Lkotlin/jvm/functions/Function1; + public static final fun memoize (Lkotlin/jvm/functions/Function2;)Lkotlin/jvm/functions/Function2; + public static final fun memoize (Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function3; + public static final fun memoize (Lkotlin/jvm/functions/Function4;)Lkotlin/jvm/functions/Function4; + public static final fun memoize (Lkotlin/jvm/functions/Function5;)Lkotlin/jvm/functions/Function5; + public static final fun memoize (Lkotlin/jvm/functions/Function6;)Lkotlin/jvm/functions/Function6; +} + +public final class com/juul/tuulbox/functional/PartialApplyKt { + public static final fun partialApply (Lkotlin/jvm/functions/Function1;Ljava/lang/Object;)Lkotlin/jvm/functions/Function0; + public static final fun partialApply (Lkotlin/jvm/functions/Function2;Ljava/lang/Object;)Lkotlin/jvm/functions/Function1; + public static final fun partialApply (Lkotlin/jvm/functions/Function3;Ljava/lang/Object;)Lkotlin/jvm/functions/Function2; + public static final fun partialApply (Lkotlin/jvm/functions/Function4;Ljava/lang/Object;)Lkotlin/jvm/functions/Function3; + public static final fun partialApply (Lkotlin/jvm/functions/Function5;Ljava/lang/Object;)Lkotlin/jvm/functions/Function4; + public static final fun partialApply (Lkotlin/jvm/functions/Function6;Ljava/lang/Object;)Lkotlin/jvm/functions/Function5; +} + +public final class com/juul/tuulbox/functional/RepeatKt { + public static final fun repeat (Lkotlin/jvm/functions/Function1;I)Lkotlin/jvm/functions/Function1; +} + +public final class com/juul/tuulbox/functional/ReverseKt { + public static final fun reverse (Lkotlin/jvm/functions/Function2;)Lkotlin/jvm/functions/Function2; + public static final fun reverse (Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function3; + public static final fun reverse (Lkotlin/jvm/functions/Function4;)Lkotlin/jvm/functions/Function4; + public static final fun reverse (Lkotlin/jvm/functions/Function5;)Lkotlin/jvm/functions/Function5; + public static final fun reverse (Lkotlin/jvm/functions/Function6;)Lkotlin/jvm/functions/Function6; +} + +public final class com/juul/tuulbox/functional/RotateKt { + public static final fun rotate (Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function3; + public static final fun rotate (Lkotlin/jvm/functions/Function4;)Lkotlin/jvm/functions/Function4; + public static final fun rotate (Lkotlin/jvm/functions/Function5;)Lkotlin/jvm/functions/Function5; + public static final fun rotate (Lkotlin/jvm/functions/Function6;)Lkotlin/jvm/functions/Function6; + public static final fun rotateBack (Lkotlin/jvm/functions/Function3;)Lkotlin/jvm/functions/Function3; + public static final fun rotateBack (Lkotlin/jvm/functions/Function4;)Lkotlin/jvm/functions/Function4; + public static final fun rotateBack (Lkotlin/jvm/functions/Function5;)Lkotlin/jvm/functions/Function5; + public static final fun rotateBack (Lkotlin/jvm/functions/Function6;)Lkotlin/jvm/functions/Function6; +} + diff --git a/functional/build.gradle.kts b/functional/build.gradle.kts new file mode 100644 index 00000000..4791f418 --- /dev/null +++ b/functional/build.gradle.kts @@ -0,0 +1,38 @@ +plugins { + kotlin("multiplatform") + id("org.jmailen.kotlinter") + jacoco + `maven-publish` +} + +apply(from = rootProject.file("gradle/jacoco.gradle.kts")) +apply(from = rootProject.file("gradle/publish.gradle.kts")) + +kotlin { + explicitApi() + + jvm() + js().browser() + + sourceSets { + val commonTest by getting { + dependencies { + implementation(project(":test")) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + + val jvmTest by getting { + dependencies { + implementation(kotlin("test-junit")) + } + } + + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) + } + } + } +} diff --git a/functional/src/commonMain/kotlin/Compose.kt b/functional/src/commonMain/kotlin/Compose.kt new file mode 100644 index 00000000..e1f745b3 --- /dev/null +++ b/functional/src/commonMain/kotlin/Compose.kt @@ -0,0 +1,9 @@ +package com.juul.tuulbox.functional + +/** Compose two functions. [this] is called first, and then [second] is called with the result. */ +public fun ((I1) -> I2).then(second: (I2) -> O): (I1) -> O = + { i1 -> second(this(i1)) } + +/** Compose two functions. [first] is called first, and then [this] is called with the result. */ +public fun ((I2) -> O).of(first: (I1) -> I2): (I1) -> O = + { i1 -> this(first(i1)) } diff --git a/functional/src/commonMain/kotlin/Curry.kt b/functional/src/commonMain/kotlin/Curry.kt new file mode 100644 index 00000000..4b8444cd --- /dev/null +++ b/functional/src/commonMain/kotlin/Curry.kt @@ -0,0 +1,37 @@ +package com.juul.tuulbox.functional + +/** Curry a two-argument function. */ +public fun ((I1, I2) -> O).curry(): (I1) -> (I2) -> O = + { i1 -> { i2 -> this(i1, i2) } } + +/** Curry a three-argument function. */ +public fun ((I1, I2, I3) -> O).curry(): (I1) -> (I2) -> (I3) -> O = + { i1 -> { i2 -> { i3 -> this(i1, i2, i3) } } } + +/** Curry a four-argument function. */ +public fun ((I1, I2, I3, I4) -> O).curry(): (I1) -> (I2) -> (I3) -> (I4) -> O = + { i1 -> { i2 -> { i3 -> { i4 -> this(i1, i2, i3, i4) } } } } + +/** Curry a five-argument function. */ +public fun ((I1, I2, I3, I4, I5) -> O).curry(): (I1) -> (I2) -> (I3) -> (I4) -> (I5) -> O = + { i1 -> { i2 -> { i3 -> { i4 -> { i5 -> this(i1, i2, i3, i4, i5) } } } } } + +/** Curry a six-argument function. */ +public fun ((I1, I2, I3, I4, I5, I6) -> O).curry(): (I1) -> (I2) -> (I3) -> (I4) -> (I5) -> (I6) -> O = + { i1 -> { i2 -> { i3 -> { i4 -> { i5 -> { i6 -> this(i1, i2, i3, i4, i5, i6) } } } } } } + +/** Curry the first argument of a three-argument function. */ +public fun ((I1, I2, I3) -> O).curryFirst(): (I1) -> (I2, I3) -> O = + { i1 -> { i2, i3 -> this(i1, i2, i3) } } + +/** Curry the first argument of a four-argument function. */ +public fun ((I1, I2, I3, I4) -> O).curryFirst(): (I1) -> (I2, I3, I4) -> O = + { i1 -> { i2, i3, i4 -> this(i1, i2, i3, i4) } } + +/** Curry the first argument of a five-argument function. */ +public fun ((I1, I2, I3, I4, I5) -> O).curryFirst(): (I1) -> (I2, I3, I4, I5) -> O = + { i1 -> { i2, i3, i4, i5 -> this(i1, i2, i3, i4, i5) } } + +/** Curry the first argument of a six-argument function. */ +public fun ((I1, I2, I3, I4, I5, I6) -> O).curryFirst(): (I1) -> (I2, I3, I4, I5, I6) -> O = + { i1 -> { i2, i3, i4, i5, i6 -> this(i1, i2, i3, i4, i5, i6) } } diff --git a/functional/src/commonMain/kotlin/Memoize.kt b/functional/src/commonMain/kotlin/Memoize.kt new file mode 100644 index 00000000..c66c79d1 --- /dev/null +++ b/functional/src/commonMain/kotlin/Memoize.kt @@ -0,0 +1,46 @@ +package com.juul.tuulbox.functional + +/** Returns a locally-memoized version of this function. Two separate invocations of [memoize] will have two separate caches. */ +public fun (() -> O).memoize(): () -> O { + val value by lazy { this() } + return { value } +} + +/** Returns a locally-memoized version of this function. Two separate invocations of [memoize] will have two separate caches. */ +public fun ((I1) -> O).memoize(): (I1) -> O { + val cache = mutableMapOf() + return { i1 -> cache.getOrPut(i1) { this(i1) } } +} + +/** Returns a locally-memoized version of this function. Two separate invocations of [memoize] will have two separate caches. */ +public fun ((I1, I2) -> O).memoize(): (I1, I2) -> O { + val cache = mutableMapOf, O>() + return { i1, i2 -> cache.getOrPut(Pair(i1, i2)) { this(i1, i2) } } +} + +/** Returns a locally-memoized version of this function. Two separate invocations of [memoize] will have two separate caches. */ +public fun ((I1, I2, I3) -> O).memoize(): (I1, I2, I3) -> O { + val cache = mutableMapOf, O>() + return { i1, i2, i3 -> cache.getOrPut(Triple(i1, i2, i3)) { this(i1, i2, i3) } } +} + +/** Returns a locally-memoized version of this function. Two separate invocations of [memoize] will have two separate caches. */ +public fun ((I1, I2, I3, I4) -> O).memoize(): (I1, I2, I3, I4) -> O { + // TODO: This is slightly less efficient than a Tuple4 data class. + val cache = mutableMapOf, O>() + return { i1, i2, i3, i4 -> cache.getOrPut(listOf(i1, i2, i3, i4)) { this(i1, i2, i3, i4) } } +} + +/** Returns a locally-memoized version of this function. Two separate invocations of [memoize] will have two separate caches. */ +public fun ((I1, I2, I3, I4, I5) -> O).memoize(): (I1, I2, I3, I4, I5) -> O { + // TODO: This is slightly less efficient than a Tuple5 data class. + val cache = mutableMapOf, O>() + return { i1, i2, i3, i4, i5 -> cache.getOrPut(listOf(i1, i2, i3, i4, i5)) { this(i1, i2, i3, i4, i5) } } +} + +/** Returns a locally-memoized version of this function. Two separate invocations of [memoize] will have two separate caches. */ +public fun ((I1, I2, I3, I4, I5, I6) -> O).memoize(): (I1, I2, I3, I4, I5, I6) -> O { + // TODO: This is slightly less efficient than a Tuple6 data class. + val cache = mutableMapOf, O>() + return { i1, i2, i3, i4, i5, i6 -> cache.getOrPut(listOf(i1, i2, i3, i4, i5, i6)) { this(i1, i2, i3, i4, i5, i6) } } +} diff --git a/functional/src/commonMain/kotlin/PartialApply.kt b/functional/src/commonMain/kotlin/PartialApply.kt new file mode 100644 index 00000000..addeb616 --- /dev/null +++ b/functional/src/commonMain/kotlin/PartialApply.kt @@ -0,0 +1,25 @@ +package com.juul.tuulbox.functional + +/** Binds the argument of this function with a constant [value]. */ +public fun ((I1) -> O).partialApply(value: I1): () -> O = + { this(value) } + +/** Binds the first argument of this function with a constant [value]. */ +public fun ((I1, I2) -> O).partialApply(value: I1): (I2) -> O = + { i2 -> this(value, i2) } + +/** Binds the first argument of this function with a constant [value]. */ +public fun ((I1, I2, I3) -> O).partialApply(value: I1): (I2, I3) -> O = + { i2, i3 -> this(value, i2, i3) } + +/** Binds the first argument of this function with a constant [value]. */ +public fun ((I1, I2, I3, I4) -> O).partialApply(value: I1): (I2, I3, I4) -> O = + { i2, i3, i4 -> this(value, i2, i3, i4) } + +/** Binds the first argument of this function with a constant [value]. */ +public fun ((I1, I2, I3, I4, I5) -> O).partialApply(value: I1): (I2, I3, I4, I5) -> O = + { i2, i3, i4, i5 -> this(value, i2, i3, i4, i5) } + +/** Binds the first argument of this function with a constant [value]. */ +public fun ((I1, I2, I3, I4, I5, I6) -> O).partialApply(value: I1): (I2, I3, I4, I5, I6) -> O = + { i2, i3, i4, i5, i6 -> this(value, i2, i3, i4, i5, i6) } diff --git a/functional/src/commonMain/kotlin/Repeat.kt b/functional/src/commonMain/kotlin/Repeat.kt new file mode 100644 index 00000000..79dba65e --- /dev/null +++ b/functional/src/commonMain/kotlin/Repeat.kt @@ -0,0 +1,13 @@ +package com.juul.tuulbox.functional + +/** Returns a function that repeats the application of [this] [count] times. */ +public fun ((T) -> T).repeat(count: Int): (T) -> T { + require(count >= 0) { "Repeat `count` must not be negative." } + return { initialValue -> + var value = initialValue + kotlin.repeat(count) { + value = this(value) + } + value + } +} diff --git a/functional/src/commonMain/kotlin/Reverse.kt b/functional/src/commonMain/kotlin/Reverse.kt new file mode 100644 index 00000000..2a4b5898 --- /dev/null +++ b/functional/src/commonMain/kotlin/Reverse.kt @@ -0,0 +1,21 @@ +package com.juul.tuulbox.functional + +/** Returns a function where the argument order has been reversed. */ +public fun ((I1, I2) -> O).reverse(): (I2, I1) -> O = + { i2, i1 -> this(i1, i2) } + +/** Returns a function where the argument order has been reversed. */ +public fun ((I1, I2, I3) -> O).reverse(): (I3, I2, I1) -> O = + { i3, i2, i1 -> this(i1, i2, i3) } + +/** Returns a function where the argument order has been reversed. */ +public fun ((I1, I2, I3, I4) -> O).reverse(): (I4, I3, I2, I1) -> O = + { i4, i3, i2, i1 -> this(i1, i2, i3, i4) } + +/** Returns a function where the argument order has been reversed. */ +public fun ((I1, I2, I3, I4, I5) -> O).reverse(): (I5, I4, I3, I2, I1) -> O = + { i5, i4, i3, i2, i1 -> this(i1, i2, i3, i4, i5) } + +/** Returns a function where the argument order has been reversed. */ +public fun ((I1, I2, I3, I4, I5, I6) -> O).reverse(): (I6, I5, I4, I3, I2, I1) -> O = + { i6, i5, i4, i3, i2, i1 -> this(i1, i2, i3, i4, i5, i6) } diff --git a/functional/src/commonMain/kotlin/Rotate.kt b/functional/src/commonMain/kotlin/Rotate.kt new file mode 100644 index 00000000..f01fd102 --- /dev/null +++ b/functional/src/commonMain/kotlin/Rotate.kt @@ -0,0 +1,33 @@ +package com.juul.tuulbox.functional + +/** Returns a function where the last argument has been moved to before the first argument. */ +public fun ((I1, I2, I3) -> O).rotate(): (I3, I1, I2) -> O = + { i3, i1, i2 -> this(i1, i2, i3) } + +/** Returns a function where the last argument has been moved to before the first argument. */ +public fun ((I1, I2, I3, I4) -> O).rotate(): (I4, I1, I2, I3) -> O = + { i4, i1, i2, i3 -> this(i1, i2, i3, i4) } + +/** Returns a function where the last argument has been moved to before the first argument. */ +public fun ((I1, I2, I3, I4, I5) -> O).rotate(): (I5, I1, I2, I3, I4) -> O = + { i5, i1, i2, i3, i4 -> this(i1, i2, i3, i4, i5) } + +/** Returns a function where the last argument has been moved to before the first argument. */ +public fun ((I1, I2, I3, I4, I5, I6) -> O).rotate(): (I6, I1, I2, I3, I4, I5) -> O = + { i6, i1, i2, i3, i4, i5 -> this(i1, i2, i3, i4, i5, i6) } + +/** Returns a function where the first argument has been moved to after the last argument. */ +public fun ((I1, I2, I3) -> O).rotateBack(): (I2, I3, I1) -> O = + { i2, i3, i1 -> this(i1, i2, i3) } + +/** Returns a function where the first argument has been moved to after the last argument. */ +public fun ((I1, I2, I3, I4) -> O).rotateBack(): (I2, I3, I4, I1) -> O = + { i2, i3, i4, i1 -> this(i1, i2, i3, i4) } + +/** Returns a function where the first argument has been moved to after the last argument. */ +public fun ((I1, I2, I3, I4, I5) -> O).rotateBack(): (I2, I3, I4, I5, I1) -> O = + { i2, i3, i4, i5, i1 -> this(i1, i2, i3, i4, i5) } + +/** Returns a function where the first argument has been moved to after the last argument. */ +public fun ((I1, I2, I3, I4, I5, I6) -> O).rotateBack(): (I2, I3, I4, I5, I6, I1) -> O = + { i2, i3, i4, i5, i6, i1 -> this(i1, i2, i3, i4, i5, i6) } diff --git a/functional/src/commonTest/kotlin/ComposeTests.kt b/functional/src/commonTest/kotlin/ComposeTests.kt new file mode 100644 index 00000000..dd277827 --- /dev/null +++ b/functional/src/commonTest/kotlin/ComposeTests.kt @@ -0,0 +1,19 @@ +package com.juul.tuulbox.functional + +import com.juul.tuulbox.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ComposeTests { + @Test + fun checkOfOrderIsCorrect() = runTest { + assertEquals(202, double.of(increment).invoke(100)) + assertEquals(201, increment.of(double).invoke(100)) + } + + @Test + fun checkThenOrderIsCorrect() = runTest { + assertEquals(201, double.then(increment).invoke(100)) + assertEquals(202, increment.then(double).invoke(100)) + } +} diff --git a/functional/src/commonTest/kotlin/CurryTests.kt b/functional/src/commonTest/kotlin/CurryTests.kt new file mode 100644 index 00000000..d5427df4 --- /dev/null +++ b/functional/src/commonTest/kotlin/CurryTests.kt @@ -0,0 +1,50 @@ +package com.juul.tuulbox.functional + +import com.juul.tuulbox.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class CurryTests { + @Test + fun checkCurryWorksWithArity2() = runTest { + val expected = stringConcat2("one", "two") + val actualCurry = stringConcat2.curry()("one")("two") + assertEquals(expected, actualCurry) + } + + @Test + fun checkCurryWorksWithArity3() = runTest { + val expected = stringConcat3("one", "two", "three") + val actualCurry = stringConcat3.curry()("one")("two")("three") + assertEquals(expected, actualCurry) + val actualCurryFirst = stringConcat3.curryFirst()("one")("two", "three") + assertEquals(expected, actualCurryFirst) + } + + @Test + fun checkCurryWorksWithArity4() = runTest { + val expected = stringConcat4("one", "two", "three", "four") + val actualCurry = stringConcat4.curry()("one")("two")("three")("four") + assertEquals(expected, actualCurry) + val actualCurryFirst = stringConcat4.curryFirst()("one")("two", "three", "four") + assertEquals(expected, actualCurryFirst) + } + + @Test + fun checkCurryWorksWithArity5() = runTest { + val expected = stringConcat5("one", "two", "three", "four", "five") + val actualCurry = stringConcat5.curry()("one")("two")("three")("four")("five") + assertEquals(expected, actualCurry) + val actualCurryFirst = stringConcat5.curryFirst()("one")("two", "three", "four", "five") + assertEquals(expected, actualCurryFirst) + } + + @Test + fun checkCurryWorksWithArity6() = runTest { + val expected = stringConcat6("one", "two", "three", "four", "five", "six") + val actualCurry = stringConcat6.curry()("one")("two")("three")("four")("five")("six") + assertEquals(expected, actualCurry) + val actualCurryFirst = stringConcat6.curryFirst()("one")("two", "three", "four", "five", "six") + assertEquals(expected, actualCurryFirst) + } +} diff --git a/functional/src/commonTest/kotlin/Functions.kt b/functional/src/commonTest/kotlin/Functions.kt new file mode 100644 index 00000000..ebccf376 --- /dev/null +++ b/functional/src/commonTest/kotlin/Functions.kt @@ -0,0 +1,29 @@ +package com.juul.tuulbox.functional + +// Functions in this file are written in lambda form to avoid :: all over the place when writing tests + +val identity = { value: Any? -> value } + +val increment = { value: Int -> value + 1 } + +val double = { value: Int -> value * 2 } + +val stringConcat2 = { first: String, second: String -> + "$first$second" +} + +val stringConcat3 = { first: String, second: String, third: String -> + "$first$second$third" +} + +val stringConcat4 = { first: String, second: String, third: String, fourth: String -> + "$first$second$third$fourth" +} + +val stringConcat5 = { first: String, second: String, third: String, fourth: String, fifth: String -> + "$first$second$third$fourth$fifth" +} + +val stringConcat6 = { first: String, second: String, third: String, fourth: String, fifth: String, sixth: String -> + "$first$second$third$fourth$fifth$sixth" +} diff --git a/functional/src/commonTest/kotlin/MemoizeTests.kt b/functional/src/commonTest/kotlin/MemoizeTests.kt new file mode 100644 index 00000000..2f5e5495 --- /dev/null +++ b/functional/src/commonTest/kotlin/MemoizeTests.kt @@ -0,0 +1,94 @@ +package com.juul.tuulbox.functional + +import com.juul.tuulbox.test.runTest +import kotlin.test.Test +import kotlin.test.assertFailsWith + +class MemoizeTests { + + private class DuplicationChecker { + private val visitedValues = mutableSetOf>() + + fun visit(vararg values: Any?) { + val list = values.toList() + check(list !in visitedValues) + visitedValues.add(list) + } + } + + /** Let's test our test utilities, too! */ + @Test + fun checkDuplicationCheckerWorks() = runTest { + val checker = DuplicationChecker() + checker.visit() + checker.visit(1) + checker.visit(1, "two", 3.0) + assertFailsWith { checker.visit() } + assertFailsWith { checker.visit(1) } + assertFailsWith { checker.visit(1, "two", 3.0) } + } + + @Test + fun checkMemoizeWithArity0() = runTest { + val duplicationChecker = DuplicationChecker() + val function = { duplicationChecker.visit() }.memoize() + function() + function() + } + + @Test + fun checkMemoizeWithArity1() = runTest { + val duplicationChecker = DuplicationChecker() + val function = { first: Int -> duplicationChecker.visit(first) }.memoize() + function(1) + function(1) + } + + @Test + fun checkMemoizeWithArity2() = runTest { + val duplicationChecker = DuplicationChecker() + val function = { first: Int, second: Int -> duplicationChecker.visit(first, second) }.memoize() + function(1, 2) + function(1, 2) + } + + @Test + fun checkMemoizeWithArity3() = runTest { + val duplicationChecker = DuplicationChecker() + val function = { first: Int, second: Int, third: Int -> + duplicationChecker.visit(first, second, third) + }.memoize() + function(1, 2, 3) + function(1, 2, 3) + } + + @Test + fun checkMemoizeWithArity4() = runTest { + val duplicationChecker = DuplicationChecker() + val function = { first: Int, second: Int, third: Int, fourth: Int -> + duplicationChecker.visit(first, second, third, fourth) + }.memoize() + function(1, 2, 3, 4) + function(1, 2, 3, 4) + } + + @Test + fun checkMemoizeWithArity5() = runTest { + val duplicationChecker = DuplicationChecker() + val function = { first: Int, second: Int, third: Int, fourth: Int, fifth: Int -> + duplicationChecker.visit(first, second, third, fourth, fifth) + }.memoize() + function(1, 2, 3, 4, 5) + function(1, 2, 3, 4, 5) + } + + @Test + fun checkMemoizeWithArity6() = runTest { + val duplicationChecker = DuplicationChecker() + val function = { first: Int, second: Int, third: Int, fourth: Int, fifth: Int, sixth: Int -> + duplicationChecker.visit(first, second, third, fourth, fifth, sixth) + }.memoize() + function(1, 2, 3, 4, 5, 6) + function(1, 2, 3, 4, 5, 6) + } +} diff --git a/functional/src/commonTest/kotlin/PartialApplyTests.kt b/functional/src/commonTest/kotlin/PartialApplyTests.kt new file mode 100644 index 00000000..c0d6ea1a --- /dev/null +++ b/functional/src/commonTest/kotlin/PartialApplyTests.kt @@ -0,0 +1,22 @@ +package com.juul.tuulbox.functional + +import com.juul.tuulbox.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class PartialApplyTests { + + @Test + fun checkPartialApply() = runTest { + val expected = stringConcat6("first", "second", "third", "fourth", "fifth", "sixth") + val actual = stringConcat6 + .partialApply("first") + .partialApply("second") + .partialApply("third") + .partialApply("fourth") + .partialApply("fifth") + .partialApply("sixth") + .invoke() + assertEquals(expected, actual) + } +} diff --git a/functional/src/commonTest/kotlin/RepeatTests.kt b/functional/src/commonTest/kotlin/RepeatTests.kt new file mode 100644 index 00000000..334fbb89 --- /dev/null +++ b/functional/src/commonTest/kotlin/RepeatTests.kt @@ -0,0 +1,37 @@ +package com.juul.tuulbox.functional + +import com.juul.tuulbox.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith + +class RepeatTests { + + @Test + fun checkRepeatNegativeThrowsException() = runTest { + assertFailsWith { double.repeat(-1) } + assertFailsWith { double.repeat(Int.MIN_VALUE) } + } + + @Test + fun checkRepeatZeroReturnsIdentityFunction() = runTest { + val repeated = double.repeat(0) + for (input in listOf(0, 1, 2, 3, 5)) { + assertEquals(identity(input), repeated(input)) + } + } + + @Test + fun checkRepeatOneReturnsOriginalFunction() = runTest { + val repeated = double.repeat(1) + for (input in listOf(0, 1, 2, 3, 5)) { + assertEquals(double(input), repeated(input)) + } + } + + @Test + fun checkRepeatTenReturnsRepeatedFunction() = runTest { + val repeated = double.repeat(10) + assertEquals(1024, repeated(1)) + } +} diff --git a/functional/src/commonTest/kotlin/ReverseTests.kt b/functional/src/commonTest/kotlin/ReverseTests.kt new file mode 100644 index 00000000..88a6f3d8 --- /dev/null +++ b/functional/src/commonTest/kotlin/ReverseTests.kt @@ -0,0 +1,43 @@ +package com.juul.tuulbox.functional + +import com.juul.tuulbox.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ReverseTests { + + @Test + fun checkReverseWithArity2() = runTest { + val expected = stringConcat2("first", "second") + val actual = stringConcat2.reverse()("second", "first") + assertEquals(expected, actual) + } + + @Test + fun checkReverseWithArity3() = runTest { + val expected = stringConcat3("first", "second", "third") + val actual = stringConcat3.reverse()("third", "second", "first") + assertEquals(expected, actual) + } + + @Test + fun checkReverseWithArity4() = runTest { + val expected = stringConcat4("first", "second", "third", "fourth") + val actual = stringConcat4.reverse()("fourth", "third", "second", "first") + assertEquals(expected, actual) + } + + @Test + fun checkReverseWithArity5() = runTest { + val expected = stringConcat5("first", "second", "third", "fourth", "fifth") + val actual = stringConcat5.reverse()("fifth", "fourth", "third", "second", "first") + assertEquals(expected, actual) + } + + @Test + fun checkReverseWithArity6() = runTest { + val expected = stringConcat6("first", "second", "third", "fourth", "fifth", "sixth") + val actual = stringConcat6.reverse()("sixth", "fifth", "fourth", "third", "second", "first") + assertEquals(expected, actual) + } +} diff --git a/functional/src/commonTest/kotlin/RotateTests.kt b/functional/src/commonTest/kotlin/RotateTests.kt new file mode 100644 index 00000000..0618c4f7 --- /dev/null +++ b/functional/src/commonTest/kotlin/RotateTests.kt @@ -0,0 +1,44 @@ +package com.juul.tuulbox.functional + +import com.juul.tuulbox.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class RotateTests { + + @Test + fun checkRotateWithArity3() = runTest { + val expected = stringConcat3("first", "second", "third") + val actualRotate = stringConcat3.rotate()("third", "first", "second") + assertEquals(expected, actualRotate) + val actualRotateBack = stringConcat3.rotateBack()("second", "third", "first") + assertEquals(expected, actualRotateBack) + } + + @Test + fun checkRotateWithArity4() = runTest { + val expected = stringConcat4("first", "second", "third", "fourth") + val actualRotate = stringConcat4.rotate()("fourth", "first", "second", "third") + assertEquals(expected, actualRotate) + val actualRotateBack = stringConcat4.rotateBack()("second", "third", "fourth", "first") + assertEquals(expected, actualRotateBack) + } + + @Test + fun checkRotateWithArity5() = runTest { + val expected = stringConcat5("first", "second", "third", "fourth", "fifth") + val actualRotate = stringConcat5.rotate()("fifth", "first", "second", "third", "fourth") + assertEquals(expected, actualRotate) + val actualRotateBack = stringConcat5.rotateBack()("second", "third", "fourth", "fifth", "first") + assertEquals(expected, actualRotateBack) + } + + @Test + fun checkRotateWithArity6() = runTest { + val expected = stringConcat6("first", "second", "third", "fourth", "fifth", "sixth") + val actualRotate = stringConcat6.rotate()("sixth", "first", "second", "third", "fourth", "fifth") + assertEquals(expected, actualRotate) + val actualRotateBack = stringConcat6.rotateBack()("second", "third", "fourth", "fifth", "sixth", "first") + assertEquals(expected, actualRotateBack) + } +} diff --git a/gradle/jacoco.gradle b/gradle/jacoco.gradle index 08485619..a73016da 100644 --- a/gradle/jacoco.gradle +++ b/gradle/jacoco.gradle @@ -1,5 +1,5 @@ jacoco { - toolVersion = "0.8.5" + toolVersion = "0.8.6" } jacocoTestReport { diff --git a/settings.gradle.kts b/settings.gradle.kts index 35a9aa6e..eecfcc4e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,5 +15,6 @@ pluginManagement { include( "collections", "coroutines", + "functional", "test" )