Skip to content

Instantly share code, notes, and snippets.

@erikhuizinga
Last active March 30, 2025 21:38
Show Gist options
  • Save erikhuizinga/d2ca2b501864df219fd7f25e4dd000a4 to your computer and use it in GitHub Desktop.
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
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()) }
// 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?)
}
@sarimmehdi
Copy link

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:

private fun cartesianProduct(vararg sets: Set<*>): Set<List<*>> {
    val nonEmptySets = sets.filter { it.isNotEmpty() }
    return if (nonEmptySets.isEmpty()) {
        emptySet()
    } else if (nonEmptySets.size == 1) {
        nonEmptySets[0].map { listOf(it) }.toSet()
    } else {
        nonEmptySets
            .fold(listOf(listOf<Any?>())) { acc, set ->
                acc.flatMap { list -> set.map { element -> list + element } }
            }.toSet()
    }
}

@erikhuizinga
Copy link
Author

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