forked from kotest/kotest
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
intersect
matcher to ranges (kotest#3792)
- Loading branch information
1 parent
e0ffe2a
commit 8fb0b73
Showing
8 changed files
with
670 additions
and
4 deletions.
There are no files selected for viewing
This file contains 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
This file contains 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
70 changes: 70 additions & 0 deletions
70
...ssertions/kotest-assertions-core/src/commonMain/kotlin/io/kotest/matchers/ranges/Range.kt
This file contains 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
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) |
148 changes: 148 additions & 0 deletions
148
...tions/kotest-assertions-core/src/commonMain/kotlin/io/kotest/matchers/ranges/intersect.kt
This file contains 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
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" } | ||
) | ||
} | ||
} | ||
|
96 changes: 96 additions & 0 deletions
96
...-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/ranges/ClosedIntersectClosedTest.kt
This file contains 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
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" | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.