Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement beWithin for ClosedRange and OpenEndRange #3839

Merged
merged 6 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1845,6 +1845,15 @@ 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/BewithinKt {
public static final fun shouldBeWithin (Lkotlin/ranges/ClosedRange;Lkotlin/ranges/ClosedRange;)Lkotlin/ranges/ClosedRange;
public static final fun shouldBeWithin (Lkotlin/ranges/ClosedRange;Lkotlin/ranges/OpenEndRange;)Lkotlin/ranges/ClosedRange;
public static final fun shouldBeWithin (Lkotlin/ranges/OpenEndRange;Lkotlin/ranges/OpenEndRange;)Lkotlin/ranges/OpenEndRange;
public static final fun shouldNotBeWithin (Lkotlin/ranges/ClosedRange;Lkotlin/ranges/ClosedRange;)Lkotlin/ranges/ClosedRange;
public static final fun shouldNotBeWithin (Lkotlin/ranges/ClosedRange;Lkotlin/ranges/OpenEndRange;)Lkotlin/ranges/ClosedRange;
public static final fun shouldNotBeWithin (Lkotlin/ranges/OpenEndRange;Lkotlin/ranges/OpenEndRange;)Lkotlin/ranges/OpenEndRange;
}

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;
Expand All @@ -1862,6 +1871,8 @@ public final class io/kotest/matchers/ranges/Range {
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 contains (Lio/kotest/matchers/ranges/Range;)Z
public final fun contains (Lio/kotest/matchers/ranges/RangeEdge;)Z
AlexCue987 marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,18 @@ data class Range<T: Comparable<T>>(

fun greaterThan(other: Range<T>) = other.lessThan(this)

fun contains(other: Range<T>) = contains(other.start) && contains(other.end)

fun contains(edge: RangeEdge<T>) = when {
edge.value < this.start.value -> false
edge.value == this.start.value ->
this.start.edgeType == RangeEdgeType.INCLUSIVE || edge.edgeType == RangeEdgeType.EXCLUSIVE
this.start.value < edge.value && edge.value < this.end.value -> true
edge.value == this.end.value ->
this.end.edgeType == RangeEdgeType.INCLUSIVE || edge.edgeType == RangeEdgeType.EXCLUSIVE
else -> false
}

companion object {
fun<T: Comparable<T>> of(range: ClosedRange<T>) = Range(
start = RangeEdge(range.start, RangeEdgeType.INCLUSIVE),
Expand Down Expand Up @@ -61,10 +73,20 @@ data class Range<T: Comparable<T>>(
start = RangeEdge(start, RangeEdgeType.INCLUSIVE),
end = RangeEdge(end, RangeEdgeType.INCLUSIVE)
)

}
}

internal fun<T: Comparable<T>> ClosedRange<T>.toClosedClosedRange(): Range<T> = Range(
start = RangeEdge(this.start, RangeEdgeType.INCLUSIVE),
end = RangeEdge(this.endInclusive, RangeEdgeType.INCLUSIVE)
)

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

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,216 @@
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] beWithin with another [ClosedRange].
*
* Assertion to check that this [ClosedRange] beWithin with another [ClosedRange].
AlexCue987 marked this conversation as resolved.
Show resolved Hide resolved
*
* An empty range will always fail. If you need to check for empty range, use [ClosedRange.shouldBeEmpty]
*
* @see [shouldBeWithin]
* @see [beWithin]
*/
infix fun <T: Comparable<T>> ClosedRange<T>.shouldBeWithin(range: ClosedRange<T>): ClosedRange<T> {
Range.of(this) should beWithin(Range.of(range))
return this
}

/**
* Verifies that this [OpenEndRange] beWithins with a [ClosedRange].
*
* Assertion to check that this [OpenEndRange] beWithins with a [ClosedRange].
*
* An empty range will always fail. If you need to check for empty range, use [ClosedRange.shouldBeEmpty]
*
* @see [shouldbeWithin]
* @see [beWithin]
*/
@OptIn(ExperimentalStdlibApi::class)
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
inline infix fun <reified T: Comparable<T>> OpenEndRange<T>.shouldBeWithin(range: ClosedRange<T>): OpenEndRange<T> {
when(T::class) {
Int::class -> shouldBeWithinRangeOfInt(this as OpenEndRange<Int>, range as ClosedRange<Int>)
Long::class -> shouldBeWithinRangeOfLong(this as OpenEndRange<Long>, range as ClosedRange<Long>)
else -> Range.of(this) should beWithin(Range.of(range))
}
return this
}

/**
* Verifies that this [ClosedRange] beWithins with an [OpenEndRange].
*
* Assertion to check that this [ClosedRange] beWithins with an [OpenEndRange].
*
* @see [shouldbeWithin]
* @see [beWithin]
*/
@OptIn(ExperimentalStdlibApi::class)
infix fun <T: Comparable<T>> ClosedRange<T>.shouldBeWithin(range: OpenEndRange<T>): ClosedRange<T> {
Range.of(this) should beWithin(Range.of(range))
return this
}

/**
* Verifies that this [OpenEndRange] beWithins with another [OpenEndRange].
*
* Assertion to check that this [OpenEndRange] beWithins with another [OpenEndRange].
*
* @see [shouldbeWithin]
* @see [beWithin]
*/
@OptIn(ExperimentalStdlibApi::class)
infix fun <T: Comparable<T>> OpenEndRange<T>.shouldBeWithin(range: OpenEndRange<T>): OpenEndRange<T> {
Range.of(this) should beWithin(Range.of(range))
return this
}

/**
* Verifies that this [ClosedRange] does not beWithin with another [ClosedRange].
*
* Assertion to check that this [ClosedRange] does not beWithin with another [ClosedRange].
*
* An empty range will always fail. If you need to check for empty range, use [Iterable.shouldBeEmpty]
*
* @see [shouldNotbeWithin]
* @see [beWithin]
*/
infix fun <T: Comparable<T>> ClosedRange<T>.shouldNotBeWithin(range: ClosedRange<T>): ClosedRange<T> {
Range.of(this) shouldNot beWithin(Range.of(range))
return this
}

/**
* Verifies that this [ClosedRange] does not beWithin with an [OpenEndRange].
*
* Assertion to check that this [ClosedRange] does not beWithin with an [OpenEndRange].
*
* @see [shouldNotbeWithin]
* @see [beWithin]
*/
@OptIn(ExperimentalStdlibApi::class)
infix fun <T: Comparable<T>> ClosedRange<T>.shouldNotBeWithin(range: OpenEndRange<T>): ClosedRange<T> {
Range.of(this) shouldNot beWithin(Range.of(range))
return this
}

/**
* Verifies that this [OpenEndRange] does not beWithin with a [ClosedRange].
*
* Assertion to check that this [OpenEndRange] does not beWithin with a [ClosedRange].
*
* An empty range will always fail. If you need to check for empty range, use [ClosedRange.shouldBeEmpty]
*
* @see [shouldNotbeWithin]
* @see [beWithin]
*/
@OptIn(ExperimentalStdlibApi::class)
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
inline infix fun <reified T: Comparable<T>> OpenEndRange<T>.shouldNotBeWithin(range: ClosedRange<T>): OpenEndRange<T> {
when(T::class) {
Int::class -> shouldNotBeWithinRangeOfInt(this as OpenEndRange<Int>, range as ClosedRange<Int>)
Long::class -> shouldNotBeWithinRangeOfLong(this as OpenEndRange<Long>, range as ClosedRange<Long>)
else -> Range.of(this) shouldNot beWithin(Range.of(range))
}
return this
}

/**
* Verifies that this [OpenEndRange] does not beWithin with another [OpenEndRange].
*
* Assertion to check that this [OpenEndRange] does not beWithin with another [OpenEndRange].
*
* @see [shouldNotbeWithin]
* @see [beWithin]
*/
@OptIn(ExperimentalStdlibApi::class)
infix fun <T: Comparable<T>> OpenEndRange<T>.shouldNotBeWithin(range: OpenEndRange<T>): OpenEndRange<T> {
Range.of(this) shouldNot beWithin(Range.of(range))
return this
}

/**
* Matcher that verifies that this [range] beWithins with another [range]
*
* Assertion to check that this [range] beWithins with another [range].
*
* An empty range will always fail. If you need to check for empty range, use [Iterable.shouldBeEmpty]
*
*/
internal fun <T: Comparable<T>> beWithin(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.contains(value)

return MatcherResult(
match,
{ "Range ${value.print().value} should be within ${range.print().value}, but it isn't" },
{ "Range ${value.print().value} should not be within ${range.print().value}, but it is" }
)
}
}

@OptIn(ExperimentalStdlibApi::class)
internal fun shouldBeWithinRangeOfInt(value: OpenEndRange<Int>,
range: ClosedRange<Int>
){
value should beWithinRangeOfInt(range)
}

@OptIn(ExperimentalStdlibApi::class)
internal fun shouldNotBeWithinRangeOfInt(value: OpenEndRange<Int>,
range: ClosedRange<Int>
){
value shouldNot beWithinRangeOfInt(range)
}

@OptIn(ExperimentalStdlibApi::class)
internal fun beWithinRangeOfInt(
range: ClosedRange<Int>
) = object : Matcher<OpenEndRange<Int>> {
override fun test(value: OpenEndRange<Int>): MatcherResult {
return resultForWithin(range, value, (range.endInclusive + 1))
}
}

@OptIn(ExperimentalStdlibApi::class)
internal fun shouldBeWithinRangeOfLong(value: OpenEndRange<Long>,
range: ClosedRange<Long>
){
value should beWithinRangeOfLong(range)
}

@OptIn(ExperimentalStdlibApi::class)
internal fun shouldNotBeWithinRangeOfLong(value: OpenEndRange<Long>,
range: ClosedRange<Long>
){
value shouldNot beWithinRangeOfLong(range)
}

@OptIn(ExperimentalStdlibApi::class)
internal fun beWithinRangeOfLong(
range: ClosedRange<Long>
) = object : Matcher<OpenEndRange<Long>> {
override fun test(value: OpenEndRange<Long>): MatcherResult {
return resultForWithin(range, value, (range.endInclusive + 1L))
}
}

@OptIn(ExperimentalStdlibApi::class)
internal fun<T: Comparable<T>> resultForWithin(range: ClosedRange<T>, value: OpenEndRange<T>, valueAfterRangeEnd: T): MatcherResult {
if (range.isEmpty()) throw AssertionError("Asserting content on empty range. Use Iterable.shouldBeEmpty() instead.")
val match = (range.start <= value.start) && (value.endExclusive <= valueAfterRangeEnd)
val valueStr = "[${value.start}, ${value.endExclusive})"
val rangeStr = "[${range.start}, ${range.endInclusive}]"
return MatcherResult(
match,
{ "Range $valueStr should be within $rangeStr, but it isn't" },
{ "Range $valueStr should not be within $rangeStr, but it is" }
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.sksamuel.kotest.matchers.ranges

import io.kotest.core.spec.style.WordSpec
import io.kotest.matchers.ranges.Range
import io.kotest.property.Arb
import io.kotest.property.arbitrary.int
import io.kotest.property.forAll

class ClosedBeWithinClosedTest: WordSpec() {
init {
"should" should {
"work" {
forAll(
Arb.int(1..5), Arb.int(1..5), Arb.int(1..5), Arb.int(1..5)
) { rangeStart, rangeLength, otherStart, otherLength ->
val rangeEnd = rangeStart + rangeLength
val otherEnd = otherStart + otherLength
Range.closedClosed(rangeStart, rangeEnd).contains(
Range.openClosed(otherStart, otherEnd)
) == (rangeStart <= otherStart && otherEnd <= rangeEnd)
}
}
}
}
}