From 8fb0b73b3b24b24b23064cc5614c41c0db5a4431 Mon Sep 17 00:00:00 2001 From: Alex Kuznetsov Date: Mon, 1 Jan 2024 11:08:06 -0600 Subject: [PATCH] Add `intersect` matcher to ranges (#3792) --- documentation/docs/assertions/ranges.md | 11 +- .../api/kotest-assertions-core.api | 58 +++++++ .../kotlin/io/kotest/matchers/ranges/Range.kt | 70 +++++++++ .../io/kotest/matchers/ranges/intersect.kt | 148 ++++++++++++++++++ .../ranges/ClosedIntersectClosedTest.kt | 96 ++++++++++++ .../ranges/ClosedIntersectOpenEndTest.kt | 103 ++++++++++++ .../ranges/OpenEndIntersectOpenEndTest.kt | 101 ++++++++++++ .../kotest/matchers/ranges/RangeTest.kt | 87 ++++++++++ 8 files changed, 670 insertions(+), 4 deletions(-) create mode 100644 kotest-assertions/kotest-assertions-core/src/commonMain/kotlin/io/kotest/matchers/ranges/Range.kt create mode 100644 kotest-assertions/kotest-assertions-core/src/commonMain/kotlin/io/kotest/matchers/ranges/intersect.kt create mode 100644 kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/ClosedIntersectClosedTest.kt create mode 100644 kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/ClosedIntersectOpenEndTest.kt create mode 100644 kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/OpenEndIntersectOpenEndTest.kt create mode 100644 kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/RangeTest.kt diff --git a/documentation/docs/assertions/ranges.md b/documentation/docs/assertions/ranges.md index 616ec613b09..4af53e1a4f6 100644 --- a/documentation/docs/assertions/ranges.md +++ b/documentation/docs/assertions/ranges.md @@ -8,7 +8,10 @@ sidebar_label: Ranges This page describes the rich assertions (matchers) that are available for [ClosedRange](https://kotlinlang.org/docs/ranges.html) and [OpenEndRange](https://kotlinlang.org/docs/ranges.html) types. -| Ranges | | -|------------------------------|-------------------------------------------------------------------------------------------| -| `value.shouldBeIn(range)` | Asserts that an object is contained in range, checking by value and not by reference. | -| `value.shouldNotBeIn(range)` | Asserts that an object is not contained in range, checking by value and not by reference. | +| Ranges | | +|-----------------------------------|------------------------------------------------------------------------------------------------------------------------| +| `value.shouldBeIn(range)` | Asserts that an object is contained in range, checking by value and not by reference. | +| `value.shouldNotBeIn(range)` | Asserts that an object is not contained in range, checking by value and not by reference. | +| `range.shouldIntersect(range)` | Asserts that a range intersects with another range. Both ranges can be either `ClosedRange` or `OpenEndRange`. | +| `range.shouldNotIntersect(range)` | Asserts that a range does not intersect with another range. Both ranges can be either `ClosedRange` or `OpenEndRange`. | + diff --git a/kotest-assertions/kotest-assertions-core/api/kotest-assertions-core.api b/kotest-assertions/kotest-assertions-core/api/kotest-assertions-core.api index ef129f32e79..05fa59ad6f7 100644 --- a/kotest-assertions/kotest-assertions-core/api/kotest-assertions-core.api +++ b/kotest-assertions/kotest-assertions-core/api/kotest-assertions-core.api @@ -1843,6 +1843,64 @@ public final class io/kotest/matchers/ranges/BeinKt { public static final fun shouldNotBeIn (Ljava/lang/Comparable;Lkotlin/ranges/ClosedRange;)Ljava/lang/Comparable; } +public final class io/kotest/matchers/ranges/IntersectKt { + public static final fun intersect (Lio/kotest/matchers/ranges/Range;)Lio/kotest/matchers/Matcher; + public static final fun shouldIntersect (Lkotlin/ranges/ClosedRange;Lkotlin/ranges/ClosedRange;)Lkotlin/ranges/ClosedRange; + public static final fun shouldIntersect (Lkotlin/ranges/ClosedRange;Lkotlin/ranges/OpenEndRange;)Lkotlin/ranges/ClosedRange; + public static final fun shouldIntersect (Lkotlin/ranges/OpenEndRange;Lkotlin/ranges/ClosedRange;)Lkotlin/ranges/OpenEndRange; + public static final fun shouldIntersect (Lkotlin/ranges/OpenEndRange;Lkotlin/ranges/OpenEndRange;)Lkotlin/ranges/OpenEndRange; + public static final fun shouldNotIntersect (Lkotlin/ranges/ClosedRange;Lkotlin/ranges/ClosedRange;)Lkotlin/ranges/ClosedRange; + public static final fun shouldNotIntersect (Lkotlin/ranges/ClosedRange;Lkotlin/ranges/OpenEndRange;)Lkotlin/ranges/ClosedRange; + public static final fun shouldNotIntersect (Lkotlin/ranges/OpenEndRange;Lkotlin/ranges/ClosedRange;)Lkotlin/ranges/OpenEndRange; + public static final fun shouldNotIntersect (Lkotlin/ranges/OpenEndRange;Lkotlin/ranges/OpenEndRange;)Lkotlin/ranges/OpenEndRange; +} + +public final class io/kotest/matchers/ranges/Range { + public static final field Companion Lio/kotest/matchers/ranges/Range$Companion; + public fun (Lio/kotest/matchers/ranges/RangeEdge;Lio/kotest/matchers/ranges/RangeEdge;)V + public final fun component1 ()Lio/kotest/matchers/ranges/RangeEdge; + public final fun component2 ()Lio/kotest/matchers/ranges/RangeEdge; + public final fun copy (Lio/kotest/matchers/ranges/RangeEdge;Lio/kotest/matchers/ranges/RangeEdge;)Lio/kotest/matchers/ranges/Range; + public static synthetic fun copy$default (Lio/kotest/matchers/ranges/Range;Lio/kotest/matchers/ranges/RangeEdge;Lio/kotest/matchers/ranges/RangeEdge;ILjava/lang/Object;)Lio/kotest/matchers/ranges/Range; + public fun equals (Ljava/lang/Object;)Z + public final fun getEnd ()Lio/kotest/matchers/ranges/RangeEdge; + public final fun getStart ()Lio/kotest/matchers/ranges/RangeEdge; + public final fun greaterThan (Lio/kotest/matchers/ranges/Range;)Z + public fun hashCode ()I + public final fun isEmpty ()Z + public final fun lessThan (Lio/kotest/matchers/ranges/Range;)Z + public fun toString ()Ljava/lang/String; +} + +public final class io/kotest/matchers/ranges/Range$Companion { + public final fun closedClosed (Ljava/lang/Comparable;Ljava/lang/Comparable;)Lio/kotest/matchers/ranges/Range; + public final fun closedOpen (Ljava/lang/Comparable;Ljava/lang/Comparable;)Lio/kotest/matchers/ranges/Range; + public final fun of (Lkotlin/ranges/ClosedRange;)Lio/kotest/matchers/ranges/Range; + public final fun of (Lkotlin/ranges/OpenEndRange;)Lio/kotest/matchers/ranges/Range; + public final fun openClosed (Ljava/lang/Comparable;Ljava/lang/Comparable;)Lio/kotest/matchers/ranges/Range; + public final fun openOpen (Ljava/lang/Comparable;Ljava/lang/Comparable;)Lio/kotest/matchers/ranges/Range; +} + +public final class io/kotest/matchers/ranges/RangeEdge { + public fun (Ljava/lang/Comparable;Lio/kotest/matchers/ranges/RangeEdgeType;)V + public final fun component1 ()Ljava/lang/Comparable; + public final fun component2 ()Lio/kotest/matchers/ranges/RangeEdgeType; + public final fun copy (Ljava/lang/Comparable;Lio/kotest/matchers/ranges/RangeEdgeType;)Lio/kotest/matchers/ranges/RangeEdge; + public static synthetic fun copy$default (Lio/kotest/matchers/ranges/RangeEdge;Ljava/lang/Comparable;Lio/kotest/matchers/ranges/RangeEdgeType;ILjava/lang/Object;)Lio/kotest/matchers/ranges/RangeEdge; + public fun equals (Ljava/lang/Object;)Z + public final fun getEdgeType ()Lio/kotest/matchers/ranges/RangeEdgeType; + public final fun getValue ()Ljava/lang/Comparable; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class io/kotest/matchers/ranges/RangeEdgeType : java/lang/Enum { + public static final field EXCLUSIVE Lio/kotest/matchers/ranges/RangeEdgeType; + public static final field INCLUSIVE Lio/kotest/matchers/ranges/RangeEdgeType; + public static fun valueOf (Ljava/lang/String;)Lio/kotest/matchers/ranges/RangeEdgeType; + public static fun values ()[Lio/kotest/matchers/ranges/RangeEdgeType; +} + public final class io/kotest/matchers/reflection/CallableMatchersKt { public static final fun acceptParametersOfType (Ljava/util/List;)Lio/kotest/matchers/Matcher; public static final fun beAbstract ()Lio/kotest/matchers/Matcher; diff --git a/kotest-assertions/kotest-assertions-core/src/commonMain/kotlin/io/kotest/matchers/ranges/Range.kt b/kotest-assertions/kotest-assertions-core/src/commonMain/kotlin/io/kotest/matchers/ranges/Range.kt new file mode 100644 index 00000000000..42ee1fa55fa --- /dev/null +++ b/kotest-assertions/kotest-assertions-core/src/commonMain/kotlin/io/kotest/matchers/ranges/Range.kt @@ -0,0 +1,70 @@ +package io.kotest.matchers.ranges + +data class Range>( + val start: RangeEdge, + val end: RangeEdge +) { + init { + require(start.value <= end.value) { + "${start.value} cannot be after ${end.value}" + } + } + + override fun toString(): String { + return "${if(start.edgeType == RangeEdgeType.INCLUSIVE) "[" else "("}${start.value}, ${end.value}${if(end.edgeType == RangeEdgeType.INCLUSIVE) "]" else ")"}" + } + + fun isEmpty() = start.value == end.value && ( + start.edgeType == RangeEdgeType.EXCLUSIVE || + end.edgeType == RangeEdgeType.EXCLUSIVE + ) + + fun lessThan(other: Range): Boolean { + val endOfThis: T = this.end.value + val startOfOther: T = other.start.value + return when { + (this.end.edgeType== RangeEdgeType.INCLUSIVE && other.start.edgeType == RangeEdgeType.INCLUSIVE) -> (endOfThis < startOfOther) + else -> (endOfThis <= startOfOther) + } + } + + fun greaterThan(other: Range) = other.lessThan(this) + + companion object { + fun> of(range: ClosedRange) = Range( + start = RangeEdge(range.start, RangeEdgeType.INCLUSIVE), + end = RangeEdge(range.endInclusive, RangeEdgeType.INCLUSIVE) + ) + + @OptIn(ExperimentalStdlibApi::class) + fun> of(range: OpenEndRange) = Range( + start = RangeEdge(range.start, RangeEdgeType.INCLUSIVE), + end = RangeEdge(range.endExclusive, RangeEdgeType.EXCLUSIVE) + ) + + fun> openOpen(start: T, end: T) = Range( + start = RangeEdge(start, RangeEdgeType.EXCLUSIVE), + end = RangeEdge(end, RangeEdgeType.EXCLUSIVE) + ) + + fun> openClosed(start: T, end: T) = Range( + start = RangeEdge(start, RangeEdgeType.EXCLUSIVE), + end = RangeEdge(end, RangeEdgeType.INCLUSIVE) + ) + + fun> closedOpen(start: T, end: T) = Range( + start = RangeEdge(start, RangeEdgeType.INCLUSIVE), + end = RangeEdge(end, RangeEdgeType.EXCLUSIVE) + ) + + fun> closedClosed(start: T, end: T) = Range( + start = RangeEdge(start, RangeEdgeType.INCLUSIVE), + end = RangeEdge(end, RangeEdgeType.INCLUSIVE) + ) + + } +} + +enum class RangeEdgeType { INCLUSIVE, EXCLUSIVE } + +data class RangeEdge>(val value: T, val edgeType: RangeEdgeType) diff --git a/kotest-assertions/kotest-assertions-core/src/commonMain/kotlin/io/kotest/matchers/ranges/intersect.kt b/kotest-assertions/kotest-assertions-core/src/commonMain/kotlin/io/kotest/matchers/ranges/intersect.kt new file mode 100644 index 00000000000..e69b88fce31 --- /dev/null +++ b/kotest-assertions/kotest-assertions-core/src/commonMain/kotlin/io/kotest/matchers/ranges/intersect.kt @@ -0,0 +1,148 @@ +package io.kotest.matchers.ranges + +import io.kotest.assertions.print.print +import io.kotest.matchers.Matcher +import io.kotest.matchers.MatcherResult +import io.kotest.matchers.should +import io.kotest.matchers.shouldNot + +/** + * Verifies that this [ClosedRange] intersects with another [ClosedRange]. + * + * Assertion to check that this [ClosedRange] intersects with another [ClosedRange]. + * + * An empty range will always fail. If you need to check for empty range, use [ClosedRange.shouldBeEmpty] + * + * @see [shouldIntersect] + * @see [intersect] + */ +infix fun > ClosedRange.shouldIntersect(range: ClosedRange): ClosedRange { + Range.of(this) should intersect(Range.of(range)) + return this +} + +/** + * Verifies that this [OpenEndRange] intersects with a [ClosedRange]. + * + * Assertion to check that this [OpenEndRange] intersects with a [ClosedRange]. + * + * An empty range will always fail. If you need to check for empty range, use [ClosedRange.shouldBeEmpty] + * + * @see [shouldIntersect] + * @see [intersect] + */ +@OptIn(ExperimentalStdlibApi::class) +infix fun > OpenEndRange.shouldIntersect(range: ClosedRange): OpenEndRange { + Range.of(this) should intersect(Range.of(range)) + return this +} + +/** + * Verifies that this [ClosedRange] intersects with an [OpenEndRange]. + * + * Assertion to check that this [ClosedRange] intersects with an [OpenEndRange]. + * + * @see [shouldIntersect] + * @see [intersect] + */ +@OptIn(ExperimentalStdlibApi::class) +infix fun > ClosedRange.shouldIntersect(range: OpenEndRange): ClosedRange { + Range.of(this) should intersect(Range.of(range)) + return this +} + +/** + * Verifies that this [OpenEndRange] intersects with another [OpenEndRange]. + * + * Assertion to check that this [OpenEndRange] intersects with another [OpenEndRange]. + * + * @see [shouldIntersect] + * @see [intersect] + */ +@OptIn(ExperimentalStdlibApi::class) +infix fun > OpenEndRange.shouldIntersect(range: OpenEndRange): OpenEndRange { + Range.of(this) should intersect(Range.of(range)) + return this +} + +/** + * Verifies that this [ClosedRange] does not intersect with another [ClosedRange]. + * + * Assertion to check that this [ClosedRange] does not intersect with another [ClosedRange]. + * + * An empty range will always fail. If you need to check for empty range, use [Iterable.shouldBeEmpty] + * + * @see [shouldNotIntersect] + * @see [intersect] + */ +infix fun > ClosedRange.shouldNotIntersect(range: ClosedRange): ClosedRange { + Range.of(this) shouldNot intersect(Range.of(range)) + return this +} + +/** + * Verifies that this [ClosedRange] does not intersect with an [OpenEndRange]. + * + * Assertion to check that this [ClosedRange] does not intersect with an [OpenEndRange]. + * + * @see [shouldNotIntersect] + * @see [intersect] + */ +@OptIn(ExperimentalStdlibApi::class) +infix fun > ClosedRange.shouldNotIntersect(range: OpenEndRange): ClosedRange { + Range.of(this) shouldNot intersect(Range.of(range)) + return this +} + +/** + * Verifies that this [OpenEndRange] does not intersect with a [ClosedRange]. + * + * Assertion to check that this [OpenEndRange] does not intersect with a [ClosedRange]. + * + * An empty range will always fail. If you need to check for empty range, use [ClosedRange.shouldBeEmpty] + * + * @see [shouldNotIntersect] + * @see [intersect] + */ +@OptIn(ExperimentalStdlibApi::class) +infix fun > OpenEndRange.shouldNotIntersect(range: ClosedRange): OpenEndRange { + Range.of(this) shouldNot intersect(Range.of(range)) + return this +} + +/** + * Verifies that this [OpenEndRange] does not intersect with another [OpenEndRange]. + * + * Assertion to check that this [OpenEndRange] does not intersect with another [OpenEndRange]. + * + * @see [shouldNotIntersect] + * @see [intersect] + */ +@OptIn(ExperimentalStdlibApi::class) +infix fun > OpenEndRange.shouldNotIntersect(range: OpenEndRange): OpenEndRange { + Range.of(this) shouldNot intersect(Range.of(range)) + return this +} + +/** + * Matcher that verifies that this [range] intersects with another [range] + * + * Assertion to check that this [range] intersects with another [range]. + * + * An empty range will always fail. If you need to check for empty range, use [Iterable.shouldBeEmpty] + * + */ +fun > intersect(range: Range) = object : Matcher> { + override fun test(value: Range): MatcherResult { + if (range.isEmpty()) throw AssertionError("Asserting content on empty range. Use Iterable.shouldBeEmpty() instead.") + + val match = !range.lessThan(value) && !value.lessThan(range) + + return MatcherResult( + match, + { "Range ${value.print().value} should intersect ${range.print().value}, but doesn't" }, + { "Range ${value.print().value} should not intersect ${range.print().value}, but does" } + ) + } +} + diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/ClosedIntersectClosedTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/ClosedIntersectClosedTest.kt new file mode 100644 index 00000000000..82c102c639d --- /dev/null +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/ClosedIntersectClosedTest.kt @@ -0,0 +1,96 @@ +package com.sksamuel.kotest.matchers.ranges + +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrowAny +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.ranges.shouldIntersect +import io.kotest.matchers.ranges.shouldNotIntersect +import io.kotest.matchers.shouldBe + +class ClosedIntersectClosedTest: WordSpec() { + private val oneThree: ClosedRange = (1..3) + private val twoFour: ClosedRange = (2..4) + private val threeFour: ClosedRange = (3..4) + private val threeFive: ClosedRange = (3..5) + private val fourSix: ClosedRange = (4..6) + init { + "should" should { + "fail if left below right" { + shouldThrowAny { + oneThree shouldIntersect fourSix + }.message shouldBe "Range [1, 3] should intersect [4, 6], but doesn't" + } + + "fail if right below left" { + shouldThrowAny { + fourSix shouldIntersect oneThree + }.message shouldBe "Range [4, 6] should intersect [1, 3], but doesn't" + } + + "pass if have common edge" { + shouldNotThrowAny { + oneThree shouldIntersect threeFive + threeFive shouldIntersect oneThree + } + } + + "pass if intersect but not completely inside one another" { + shouldNotThrowAny { + oneThree shouldIntersect twoFour + twoFour shouldIntersect oneThree + } + } + + "pass if one completely inside another" { + shouldNotThrowAny { + twoFour shouldIntersect threeFour + threeFour shouldIntersect twoFour + } + } + } + + "shouldNot" should { + "pass if left below right" { + shouldNotThrowAny { + oneThree shouldNotIntersect fourSix + } + } + + "pass if right below left" { + shouldNotThrowAny { + fourSix shouldNotIntersect oneThree + } + } + + "fail if have common edge" { + shouldThrowAny { + oneThree shouldNotIntersect threeFive + }.message shouldBe "Range [1, 3] should not intersect [3, 5], but does" + + shouldThrowAny { + threeFive shouldNotIntersect oneThree + }.message shouldBe "Range [3, 5] should not intersect [1, 3], but does" + } + + "fail if intersect but not completely inside one another" { + shouldThrowAny { + oneThree shouldNotIntersect twoFour + }.message shouldBe "Range [1, 3] should not intersect [2, 4], but does" + + shouldThrowAny { + twoFour shouldNotIntersect oneThree + }.message shouldBe "Range [2, 4] should not intersect [1, 3], but does" + } + + "fail if one completely inside another" { + shouldThrowAny { + twoFour shouldNotIntersect threeFour + }.message shouldBe "Range [2, 4] should not intersect [3, 4], but does" + + shouldThrowAny { + threeFour shouldNotIntersect twoFour + }.message shouldBe "Range [3, 4] should not intersect [2, 4], but does" + } + } + } +} diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/ClosedIntersectOpenEndTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/ClosedIntersectOpenEndTest.kt new file mode 100644 index 00000000000..96057646f3e --- /dev/null +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/ClosedIntersectOpenEndTest.kt @@ -0,0 +1,103 @@ +package com.sksamuel.kotest.matchers.ranges + +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrowAny +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.ranges.shouldIntersect +import io.kotest.matchers.ranges.shouldNotIntersect +import io.kotest.matchers.shouldBe + +@OptIn(ExperimentalStdlibApi::class) +class ClosedIntersectOpenEndTest: WordSpec() { + private val oneThree: ClosedRange = (1.0..3.0) + private val twoFour: ClosedRange = (2.0 .. 4.0) + private val threeFour: ClosedRange = (3.0..4.0) + private val threeFive: ClosedRange = (3.0..5.0) + private val fourSix: ClosedRange = (4.0..6.0) + init { + "should" should { + "fail if left below right" { + shouldThrowAny { + val openEndRange = oneThree.toOpenEndRange() + println(openEndRange) + openEndRange shouldIntersect fourSix + }.message shouldBe "Range [1.0, 3.0) should intersect [4.0, 6.0], but doesn't" + } + + "fail if right below left" { + shouldThrowAny { + fourSix shouldIntersect (oneThree.toOpenEndRange()) + }.message shouldBe "Range [4.0, 6.0] should intersect [1.0, 3.0), but doesn't" + } + + "fail if have common edge, but only one inclusive" { + val openEndRange = oneThree.toOpenEndRange() + shouldThrowAny { + openEndRange shouldIntersect threeFive + }.message shouldBe "Range [1.0, 3.0) should intersect [3.0, 5.0], but doesn't" + shouldThrowAny { + threeFive shouldIntersect openEndRange + }.message shouldBe "Range [3.0, 5.0] should intersect [1.0, 3.0), but doesn't" + } + + "pass if intersect but not completely inside one another" { + shouldNotThrowAny { + oneThree.toOpenEndRange() shouldIntersect twoFour + twoFour shouldIntersect oneThree.toOpenEndRange() + } + } + + "pass if one completely inside another" { + shouldNotThrowAny { + twoFour shouldIntersect threeFour.toOpenEndRange() + threeFour.toOpenEndRange() shouldIntersect twoFour + } + } + } + + "shouldNot" should { + "pass if left below right" { + shouldNotThrowAny { + oneThree.toOpenEndRange() shouldNotIntersect fourSix + oneThree.toOpenEndRange() shouldNotIntersect threeFour + } + } + + "pass if right below left" { + shouldNotThrowAny { + fourSix shouldNotIntersect oneThree.toOpenEndRange() + } + } + + "pass if have common edge" { + shouldNotThrowAny { + oneThree.toOpenEndRange() shouldNotIntersect threeFive + threeFive shouldNotIntersect oneThree.toOpenEndRange() + } + } + + "fail if intersect but not completely inside one another" { + shouldThrowAny { + oneThree.toOpenEndRange() shouldNotIntersect twoFour + }.message shouldBe "Range [1.0, 3.0) should not intersect [2.0, 4.0], but does" + + shouldThrowAny { + twoFour shouldNotIntersect oneThree.toOpenEndRange() + }.message shouldBe "Range [2.0, 4.0] should not intersect [1.0, 3.0), but does" + } + + "fail if one completely inside another" { + shouldThrowAny { + twoFour shouldNotIntersect threeFour.toOpenEndRange() + }.message shouldBe "Range [2.0, 4.0] should not intersect [3.0, 4.0), but does" + + shouldThrowAny { + threeFour.toOpenEndRange() shouldNotIntersect twoFour + }.message shouldBe "Range [3.0, 4.0) should not intersect [2.0, 4.0], but does" + } + } + } + + private fun ClosedRange.toOpenEndRange() = this.start.rangeUntil(this.endInclusive) +} + diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/OpenEndIntersectOpenEndTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/OpenEndIntersectOpenEndTest.kt new file mode 100644 index 00000000000..4025a5fde3e --- /dev/null +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/OpenEndIntersectOpenEndTest.kt @@ -0,0 +1,101 @@ +package com.sksamuel.kotest.matchers.ranges + +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrowAny +import io.kotest.core.spec.style.WordSpec +import io.kotest.matchers.ranges.shouldIntersect +import io.kotest.matchers.ranges.shouldNotIntersect +import io.kotest.matchers.shouldBe + +@OptIn(ExperimentalStdlibApi::class) +class OpenEndIntersectOpenEndTest: WordSpec() { + private val oneThree: OpenEndRange = 1.0.rangeUntil(3.0) + private val twoFour: OpenEndRange = 2.0.rangeUntil(4.0) + private val threeFour: OpenEndRange = 3.0.rangeUntil(4.0) + private val threeFive: OpenEndRange = 3.0.rangeUntil(5.0) + private val fourSix: OpenEndRange = 4.0.rangeUntil(6.0) + init { + "should" should { + "fail if left below right" { + shouldThrowAny { + val openEndRange = oneThree + println(openEndRange) + openEndRange shouldIntersect fourSix + }.message shouldBe "Range [1.0, 3.0) should intersect [4.0, 6.0), but doesn't" + } + + "fail if right below left" { + shouldThrowAny { + fourSix shouldIntersect (oneThree) + }.message shouldBe "Range [4.0, 6.0) should intersect [1.0, 3.0), but doesn't" + } + + "fail if have common edge, but only one inclusive" { + val openEndRange = oneThree + shouldThrowAny { + openEndRange shouldIntersect threeFive + }.message shouldBe "Range [1.0, 3.0) should intersect [3.0, 5.0), but doesn't" + shouldThrowAny { + threeFive shouldIntersect openEndRange + }.message shouldBe "Range [3.0, 5.0) should intersect [1.0, 3.0), but doesn't" + } + + "pass if intersect but not completely inside one another" { + shouldNotThrowAny { + oneThree shouldIntersect twoFour + twoFour shouldIntersect oneThree + } + } + + "pass if one completely inside another" { + shouldNotThrowAny { + twoFour shouldIntersect threeFour + threeFour shouldIntersect twoFour + } + } + } + + "shouldNot" should { + "pass if left below right" { + shouldNotThrowAny { + oneThree shouldNotIntersect fourSix + oneThree shouldNotIntersect threeFour + } + } + + "pass if right below left" { + shouldNotThrowAny { + fourSix shouldNotIntersect oneThree + } + } + + "pass if have common edge" { + shouldNotThrowAny { + oneThree shouldNotIntersect threeFive + threeFive shouldNotIntersect oneThree + } + } + + "fail if intersect but not completely inside one another" { + shouldThrowAny { + oneThree shouldNotIntersect twoFour + }.message shouldBe "Range [1.0, 3.0) should not intersect [2.0, 4.0), but does" + + shouldThrowAny { + twoFour shouldNotIntersect oneThree + }.message shouldBe "Range [2.0, 4.0) should not intersect [1.0, 3.0), but does" + } + + "fail if one completely inside another" { + shouldThrowAny { + twoFour shouldNotIntersect threeFour + }.message shouldBe "Range [2.0, 4.0) should not intersect [3.0, 4.0), but does" + + shouldThrowAny { + threeFour shouldNotIntersect twoFour + }.message shouldBe "Range [3.0, 4.0) should not intersect [2.0, 4.0), but does" + } + } + } +} + diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/RangeTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/RangeTest.kt new file mode 100644 index 00000000000..0351fde0bba --- /dev/null +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/RangeTest.kt @@ -0,0 +1,87 @@ +package com.sksamuel.kotest.matchers.ranges + +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.throwables.shouldThrowAny +import io.kotest.assertions.withClue +import io.kotest.core.spec.style.WordSpec +import io.kotest.data.forAll +import io.kotest.data.row +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.booleans.shouldBeTrue +import io.kotest.matchers.ranges.Range +import io.kotest.matchers.shouldBe + +class RangeTest: WordSpec() { + private val openOpenRange = Range.openOpen(1, 2) + private val openClosedRange = Range.openClosed(2, 3) + private val closedOpenRange = Range.closedOpen(3, 4) + private val closedClosedRange = Range.closedClosed(4, 5) + + init { + "create" should { + "openOpen" { + openOpenRange.toString() shouldBe "(1, 2)" + } + + "openClosed" { + openClosedRange.toString() shouldBe "(2, 3]" + } + + "closedOpen" { + closedOpenRange.toString() shouldBe "[3, 4)" + } + + "closedClosed" { + closedClosedRange.toString() shouldBe "[4, 5]" + } + + "cannot create if end less than start" { + shouldThrowAny { + Range.closedClosed(4, 3) + }.message shouldBe "4 cannot be after 3" + } + } + + "isEmpty" should { + "false if start less than end" { + openOpenRange.isEmpty().shouldBeFalse() + } + + "true if start equals end and closed-closed" { + Range.closedClosed(1, 1).isEmpty().shouldBeFalse() + } + + "false if start equals end and not closed-closed" { + Range.openClosed(1, 1).isEmpty().shouldBeTrue() + Range.closedOpen(1, 1).isEmpty().shouldBeTrue() + Range.openOpen(1, 1).isEmpty().shouldBeTrue() + } + } + + "lessThan" should { + "true if gap" { + openOpenRange.lessThan(closedOpenRange).shouldBeTrue() + closedOpenRange.greaterThan(openOpenRange).shouldBeTrue() + } + + "true if common edge but not both are inclusive" { + forAll( + row(Range.openOpen(1, 2), Range.openOpen(2, 3), "both ends exclusive"), + row(Range.openClosed(1, 2), Range.openOpen(2, 3), "left inclusive, right exclusive"), + row(Range.openOpen(1, 2), Range.closedOpen(2, 3), "left exclusive, right inclusive"), + ) { left, right, description -> + withClue(description) { + assertSoftly { + left.lessThan(right).shouldBeTrue() + right.greaterThan(left).shouldBeTrue() + } + } + } + } + + "false if common edge and both ends are inclusive" { + Range.openClosed(1, 2).lessThan(Range.closedOpen(2, 3)).shouldBeFalse() + } + } + } +}