Skip to content

Commit

Permalink
Implement beWithin for ClosedRange and OpenEndRange (#3839)
Browse files Browse the repository at this point in the history
Linked: #3736 

Co-authored-by: Leonardo Colman Lopes <dev@leonardo.colman.com.br>
  • Loading branch information
AlexCue987 and LeoColman committed Mar 9, 2024
1 parent 55e8960 commit 24f0117
Show file tree
Hide file tree
Showing 7 changed files with 514 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1855,8 +1855,16 @@ 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;
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;
Expand All @@ -1867,53 +1875,6 @@ public final class io/kotest/matchers/ranges/IntersectKt {
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 intersect (Lio/kotest/matchers/ranges/Range;)Z
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 ofClosedRange (Lkotlin/ranges/ClosedRange;)Lio/kotest/matchers/ranges/Range;
public final fun ofOpenEndRange (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
@@ -1,6 +1,6 @@
package io.kotest.matchers.ranges

data class Range<T: Comparable<T>>(
internal data class Range<T: Comparable<T>>(
val start: RangeEdge<T>,
val end: RangeEdge<T>
) {
Expand Down Expand Up @@ -32,6 +32,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>> ofClosedRange(range: ClosedRange<T>) = Range(
start = RangeEdge(range.start, RangeEdgeType.INCLUSIVE),
Expand Down Expand Up @@ -63,10 +75,20 @@ data class Range<T: Comparable<T>>(
start = RangeEdge(start, RangeEdgeType.INCLUSIVE),
end = RangeEdge(end, RangeEdgeType.INCLUSIVE)
)

}
}

enum class RangeEdgeType { INCLUSIVE, EXCLUSIVE }
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)
)

internal enum class RangeEdgeType { INCLUSIVE, EXCLUSIVE }

data class RangeEdge<T: Comparable<T>>(val value: T, val edgeType: RangeEdgeType)
internal 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] is within another [ClosedRange].
* This means that every element in the range is in another range as well.
* For instance, there are two `Int` numbers in range [2, 3], and both are in range [1, 3],
* so the range [2, 3] is within range [1, 3].
*
* 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.ofClosedRange(this) should beWithin(Range.ofClosedRange(range))
return this
}

/**
* Verifies that this [OpenEndRange] is within a [ClosedRange].
* This means that every element in the range is in another range as well.
* For instance, there are two `Int` numbers in [OpenEndRange] [1, 3), and both are in [ClosedRange] [1, 3],
* so the range [1, 3) is within range [1, 2].
*
* 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.ofOpenEndRange(this) should beWithin(Range.ofClosedRange(range))
}
return this
}

/**
* Verifies that this [ClosedRange] is within with an [OpenEndRange].
* This means that every element in the range is in another range as well.
* For instance, there are two `Int` numbers in [ClosedRange] [1, 2], and both are in [OpenEndRange] [1, 3),
* so the range [1, 2] is within range [1, 3).
*
* @see [shouldbeWithin]
* @see [beWithin]
*/
@OptIn(ExperimentalStdlibApi::class)
infix fun <T: Comparable<T>> ClosedRange<T>.shouldBeWithin(range: OpenEndRange<T>): ClosedRange<T> {
Range.ofClosedRange(this) should beWithin(Range.ofOpenEndRange(range))
return this
}

/**
* Verifies that this [OpenEndRange] is within another [OpenEndRange].
* This means that every element in the range is in another range as well.
* For instance, there are two `Int` numbers in [OpenEndRange] [1, 3), and both are in [OpenEndRange] [0, 3),
* so the range [1, 3) is within range [0, 3).
*
* @see [shouldbeWithin]
* @see [beWithin]
*/
@OptIn(ExperimentalStdlibApi::class)
infix fun <T: Comparable<T>> OpenEndRange<T>.shouldBeWithin(range: OpenEndRange<T>): OpenEndRange<T> {
Range.ofOpenEndRange(this) should beWithin(Range.ofOpenEndRange(range))
return this
}

/**
* Verifies that this [ClosedRange] does not beWithin with another [ClosedRange].
* This means that every element in the range is in another range as well.
* For instance, there are two `Int` numbers in [ClosedRange] [2, 3], and both are in [ClosedRange] [0, 3],
* so the range [2, 3] is within range [0, 3].
*
* 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.ofClosedRange(this) shouldNot beWithin(Range.ofClosedRange(range))
return this
}

/**
* Verifies that this [ClosedRange] is not within an [OpenEndRange].
* This means that every element in the range is in another range.
* For instance, there are two `Int` numbers in [ClosedRange] [2, 3], and 3 is not in [OpenEndRange] [2, 3),
* so the range [2, 3] is not within range [2, 3).
*
* @see [shouldNotbeWithin]
* @see [beWithin]
*/
@OptIn(ExperimentalStdlibApi::class)
infix fun <T: Comparable<T>> ClosedRange<T>.shouldNotBeWithin(range: OpenEndRange<T>): ClosedRange<T> {
Range.ofClosedRange(this) shouldNot beWithin(Range.ofOpenEndRange(range))
return this
}

/**
* Verifies that this [OpenEndRange] is not within a [ClosedRange].
* This means that every element in the range is in another range.
* For instance, there are two `Int` numbers in [OpenEndRange] [1, 3), and 2 is not in [ClosedRange] [0, 1],
* so the range [1, 3) is not within range [0, 1].
*
* 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.ofOpenEndRange(this) shouldNot beWithin(Range.ofClosedRange(range))
}
return this
}

/**
* Verifies that this [OpenEndRange] does not beWithin with another [OpenEndRange].
* This means that every element in the range is in another range.
* For instance, there are two `Int` numbers in [OpenEndRange] [1, 3), and 2 is not in [OpenEndRange] [0, 2),
* so the range [1, 3) is not within range [0, 2).
*
* @see [shouldNotbeWithin]
* @see [beWithin]
*/
@OptIn(ExperimentalStdlibApi::class)
infix fun <T: Comparable<T>> OpenEndRange<T>.shouldNotBeWithin(range: OpenEndRange<T>): OpenEndRange<T> {
Range.ofOpenEndRange(this) shouldNot beWithin(Range.ofOpenEndRange(range))
return this
}

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
Expand Up @@ -132,7 +132,7 @@ infix fun <T: Comparable<T>> OpenEndRange<T>.shouldNotIntersect(range: OpenEndRa
* 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>> {
internal 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.")

Expand Down
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)
}
}
}
}
}

0 comments on commit 24f0117

Please sign in to comment.