From e7610ef5c5b8c5d416413fae0aa2c381cd3b7766 Mon Sep 17 00:00:00 2001 From: Micke Elm Date: Fri, 19 May 2023 15:55:20 +0200 Subject: [PATCH] Adding support for partial permutations Adding length parameter (defaults to list size) to the permutations function, providing the opportunity of creating exhaustives of partial permutations/k-permutations. Also, changed the assertion of the full permutations test from shouldBe to shouldContainExactlyInAnyOrder, as the order in which the permutations come are not (should not be) relevant. This also makes the test less dependent on the underlying implementation. The empty list check was removed, as an empty list will result in a list of an empty list (as with all permutations of length 0). This commit does break binary compatibility, due to adding the length parameter. --- kotest-property/api/kotest-property.api | 3 ++- .../kotest/property/exhaustive/collections.kt | 19 ++++++-------- .../property/exhaustive/PermutationsTest.kt | 26 +++++++++++++++++-- 3 files changed, 34 insertions(+), 14 deletions(-) diff --git a/kotest-property/api/kotest-property.api b/kotest-property/api/kotest-property.api index 95e574a8f5f..99b7ec50961 100644 --- a/kotest-property/api/kotest-property.api +++ b/kotest-property/api/kotest-property.api @@ -1365,7 +1365,8 @@ public final class io/kotest/property/exhaustive/CharKt { public final class io/kotest/property/exhaustive/CollectionsKt { public static final fun collection (Lio/kotest/property/Exhaustive$Companion;Ljava/util/Collection;)Lio/kotest/property/Exhaustive; - public static final fun permutations (Lio/kotest/property/Exhaustive$Companion;Ljava/util/List;)Lio/kotest/property/Exhaustive; + public static final fun permutations (Lio/kotest/property/Exhaustive$Companion;Ljava/util/List;I)Lio/kotest/property/Exhaustive; + public static synthetic fun permutations$default (Lio/kotest/property/Exhaustive$Companion;Ljava/util/List;IILjava/lang/Object;)Lio/kotest/property/Exhaustive; } public final class io/kotest/property/exhaustive/ExhaustiveKt { diff --git a/kotest-property/src/commonMain/kotlin/io/kotest/property/exhaustive/collections.kt b/kotest-property/src/commonMain/kotlin/io/kotest/property/exhaustive/collections.kt index f9d8aad3b6d..c949e01cc49 100644 --- a/kotest-property/src/commonMain/kotlin/io/kotest/property/exhaustive/collections.kt +++ b/kotest-property/src/commonMain/kotlin/io/kotest/property/exhaustive/collections.kt @@ -7,20 +7,17 @@ fun Exhaustive.Companion.collection(collection: Collection): Exhaustive Exhaustive.Companion.permutations(list: List): Exhaustive> { - require(list.isNotEmpty()) { "Can't build an Exhaustive for an empty list." } +fun Exhaustive.Companion.permutations(list: List, length: Int = list.size): Exhaustive> { + require(length in 0..list.size) { "length must be between 0 and the list size (${list.size}), but was $length." } - fun perms(list: List): List> = when { - list.isEmpty() -> emptyList() - list.size == 1 -> listOf(list) - else -> { - val result = mutableListOf>() - for (i in list.indices) { - perms(list - list[i]).forEach { result.add(it + list[i]) } + fun perms(list: List, length: Int): List> = buildList { + when (length) { + 0 -> add(emptyList()) + else -> list.forEach { element -> + perms(list - element, length - 1).forEach { add(it + element) } } - result.toList() } } - return perms(list).exhaustive() + return perms(list, length).exhaustive() } diff --git a/kotest-property/src/jvmTest/kotlin/com/sksamuel/kotest/property/exhaustive/PermutationsTest.kt b/kotest-property/src/jvmTest/kotlin/com/sksamuel/kotest/property/exhaustive/PermutationsTest.kt index d38a3b5a336..32257d272ec 100644 --- a/kotest-property/src/jvmTest/kotlin/com/sksamuel/kotest/property/exhaustive/PermutationsTest.kt +++ b/kotest-property/src/jvmTest/kotlin/com/sksamuel/kotest/property/exhaustive/PermutationsTest.kt @@ -1,16 +1,38 @@ package com.sksamuel.kotest.property.exhaustive +import io.kotest.assertions.throwables.shouldThrowWithMessage import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.shouldBe import io.kotest.property.Exhaustive import io.kotest.property.exhaustive.permutations class PermutationsTest : FunSpec() { init { - test("Exhaustive.permutations") { - Exhaustive.permutations(listOf(1, 2, 3)).values shouldBe listOf( + test("Exhaustive.permutations should generate full permutations when length is omitted") { + Exhaustive.permutations(listOf(1, 2, 3)).values shouldContainExactlyInAnyOrder listOf( listOf(3, 2, 1), listOf(2, 3, 1), listOf(3, 1, 2), listOf(1, 3, 2), listOf(2, 1, 3), listOf(1, 2, 3) ) } + + test("Exhaustive.permutations should generate partial permutations of given length") { + Exhaustive.permutations(listOf(1, 2, 3), 2).values shouldContainExactlyInAnyOrder listOf( + listOf(1, 2), listOf(1, 3), listOf(2, 1), listOf(2, 3), listOf(3, 1), listOf(3, 2) + ) + } + + test("Exhaustive.permutations should generate a single empty list when length is zero") { + Exhaustive.permutations(listOf(1, 2, 3), 0).values shouldBe listOf( listOf() ) + } + + test("Exhaustive.permutations should throw if length is negative") { + shouldThrowWithMessage("length must be between 0 and the list size (1), but was -1.") + { Exhaustive.permutations(listOf(1), -1) } + } + + test("Exhaustive.permutations should throw if length is greater than list length") { + shouldThrowWithMessage("length must be between 0 and the list size (3), but was 7.") + { Exhaustive.permutations(listOf(1,2,3), 7) } + } } }