Last active
March 30, 2025 21:38
-
-
Save erikhuizinga/d2ca2b501864df219fd7f25e4dd000a4 to your computer and use it in GitHub Desktop.
Kotlin function for cartesian product of any number of sets of any size
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import kotlin.reflect.KFunction | |
typealias CartesianProduct = Set<List<*>> | |
/** | |
* Create the cartesian product of any number of sets of any size. Useful for parameterized tests | |
* to generate a large parameter space with little code. Note that any type information is lost, as | |
* the returned set contains list of any combination of types in the input set. | |
* | |
* @param a The first set. | |
* @param b The second set. | |
* @param sets Any additional sets. | |
*/ | |
fun cartesianProduct(a: Set<*>, b: Set<*>, vararg sets: Set<*>): CartesianProduct = | |
(setOf(a, b).plus(sets)) | |
.fold(listOf(listOf<Any?>())) { acc, set -> | |
acc.flatMap { list -> set.map { element -> list + element } } | |
} | |
.toSet() | |
/** | |
* Transform elements of a cartesian product. | |
*/ | |
fun <T> CartesianProduct.map(transform: KFunction<T>) = map { transform.call(*it.toTypedArray()) } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// JUnit 4 is a dependency for these imports | |
import org.junit.Assert.assertEquals | |
import org.junit.Assert.assertNotEquals | |
import org.junit.Assert.assertTrue | |
import org.junit.Test | |
class CartesianProductTest { | |
private val emptySet: Set<*> = emptySet<Nothing>() | |
private val a = setOf(1, 2) | |
private val b = setOf('3', '4') | |
@Test | |
fun `Given the empty set, When the cartesian product with itself is created, Then it returns the empty set`() { | |
assertTrue(cartesianProduct(emptySet, emptySet).isEmpty()) | |
} | |
@Test | |
fun `Given nonempty set A, When the cartesian product with the empty set is created, Then it returns the empty set`() { | |
assertTrue(cartesianProduct(a, emptySet).isEmpty()) | |
assertTrue(cartesianProduct(emptySet, a).isEmpty()) | |
} | |
@Test | |
fun `Given different nonempty sets A and B, When A x B and B x A are created, Then they are unequal`() = | |
assertNotEquals( | |
cartesianProduct(a, b), | |
cartesianProduct(b, a) | |
) | |
@Test | |
fun `Given different nonempty sets A, B, C and D, When A x B x C x D is created, Then it returns the correct cartesian product`() { | |
val c = setOf(5) | |
val d = setOf("6", "7", "8, 9") | |
val expected: CartesianProduct = setOf( | |
listOf(1, '3', 5, "6"), | |
listOf(1, '3', 5, "7"), | |
listOf(1, '3', 5, "8, 9"), | |
listOf(1, '4', 5, "6"), | |
listOf(1, '4', 5, "7"), | |
listOf(1, '4', 5, "8, 9"), | |
listOf(2, '3', 5, "6"), | |
listOf(2, '3', 5, "7"), | |
listOf(2, '3', 5, "8, 9"), | |
listOf(2, '4', 5, "6"), | |
listOf(2, '4', 5, "7"), | |
listOf(2, '4', 5, "8, 9") | |
) | |
val actual = cartesianProduct(a, b, c, d) | |
assertEquals(expected, actual) | |
} | |
@Test | |
fun `Given a parameter class and sets of input arguments, When the arguments' cartesian product is created and it is mapped to the parameter class, Then it returns the correct list of parameters`() = | |
assertEquals( | |
listOf( | |
Parameters(1, true), | |
Parameters(1, false), | |
Parameters(1, null), | |
Parameters(2, true), | |
Parameters(2, false), | |
Parameters(2, null) | |
), | |
cartesianProduct(a, setOf(true, false, null)) | |
.map(::Parameters) | |
) | |
internal data class Parameters(val number: Int, val maybe: Boolean?) | |
} |
Thanks for the suggestion @sarimmehdi. I think, because mathematically the cartesian product of any sets with an empty set is defined to be empty, that it would make more sense to keep your wish out of fun cartesianProduct
. That is why the function signature takes sets a
and b
and then vararg sets
as inputs: the cartesian product simply isn't defined for fewer than two operands.
See also the definitions on set operands, empty operands and the n-ary generalisation here: https://en.wikipedia.org/wiki/Cartesian_product.
To avoid confusion, I suggest that you consider renaming your function, or that you do the if
-else if
-else
outside of fun cartesianProduct
, so that you call the function (as written in this Gist) only in the else
branch.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
If anyone wishes to return the original set if only one set is being passed or empty if all passed sets are empty, then they can use the following variation of the above: