Skip to content

Commit

Permalink
Add intersect matcher to ranges (#3792)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexCue987 committed Jan 1, 2024
1 parent e0ffe2a commit 8fb0b73
Show file tree
Hide file tree
Showing 8 changed files with 670 additions and 4 deletions.
11 changes: 7 additions & 4 deletions documentation/docs/assertions/ranges.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`. |

Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (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 <init> (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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.kotest.matchers.ranges

data class Range<T: Comparable<T>>(
val start: RangeEdge<T>,
val end: RangeEdge<T>
) {
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<T>): 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<T>) = other.lessThan(this)

companion object {
fun<T: Comparable<T>> of(range: ClosedRange<T>) = Range(
start = RangeEdge(range.start, RangeEdgeType.INCLUSIVE),
end = RangeEdge(range.endInclusive, RangeEdgeType.INCLUSIVE)
)

@OptIn(ExperimentalStdlibApi::class)
fun<T: Comparable<T>> of(range: OpenEndRange<T>) = Range(
start = RangeEdge(range.start, RangeEdgeType.INCLUSIVE),
end = RangeEdge(range.endExclusive, RangeEdgeType.EXCLUSIVE)
)

fun<T: Comparable<T>> openOpen(start: T, end: T) = Range(
start = RangeEdge(start, RangeEdgeType.EXCLUSIVE),
end = RangeEdge(end, RangeEdgeType.EXCLUSIVE)
)

fun<T: Comparable<T>> openClosed(start: T, end: T) = Range(
start = RangeEdge(start, RangeEdgeType.EXCLUSIVE),
end = RangeEdge(end, RangeEdgeType.INCLUSIVE)
)

fun<T: Comparable<T>> closedOpen(start: T, end: T) = Range(
start = RangeEdge(start, RangeEdgeType.INCLUSIVE),
end = RangeEdge(end, RangeEdgeType.EXCLUSIVE)
)

fun<T: Comparable<T>> closedClosed(start: T, end: T) = Range(
start = RangeEdge(start, RangeEdgeType.INCLUSIVE),
end = RangeEdge(end, RangeEdgeType.INCLUSIVE)
)

}
}

enum class RangeEdgeType { INCLUSIVE, EXCLUSIVE }

data class RangeEdge<T: Comparable<T>>(val value: T, val edgeType: RangeEdgeType)
Original file line number Diff line number Diff line change
@@ -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 <T: Comparable<T>> ClosedRange<T>.shouldIntersect(range: ClosedRange<T>): ClosedRange<T> {
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 <T: Comparable<T>> OpenEndRange<T>.shouldIntersect(range: ClosedRange<T>): OpenEndRange<T> {
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 <T: Comparable<T>> ClosedRange<T>.shouldIntersect(range: OpenEndRange<T>): ClosedRange<T> {
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 <T: Comparable<T>> OpenEndRange<T>.shouldIntersect(range: OpenEndRange<T>): OpenEndRange<T> {
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 <T: Comparable<T>> ClosedRange<T>.shouldNotIntersect(range: ClosedRange<T>): ClosedRange<T> {
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 <T: Comparable<T>> ClosedRange<T>.shouldNotIntersect(range: OpenEndRange<T>): ClosedRange<T> {
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 <T: Comparable<T>> OpenEndRange<T>.shouldNotIntersect(range: ClosedRange<T>): OpenEndRange<T> {
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 <T: Comparable<T>> OpenEndRange<T>.shouldNotIntersect(range: OpenEndRange<T>): OpenEndRange<T> {
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 <T: Comparable<T>> intersect(range: Range<T>) = object : Matcher<Range<T>> {
override fun test(value: Range<T>): 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" }
)
}
}

Original file line number Diff line number Diff line change
@@ -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<Int> = (1..3)
private val twoFour: ClosedRange<Int> = (2..4)
private val threeFour: ClosedRange<Int> = (3..4)
private val threeFive: ClosedRange<Int> = (3..5)
private val fourSix: ClosedRange<Int> = (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"
}
}
}
}

0 comments on commit 8fb0b73

Please sign in to comment.