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 all commits
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 @@ -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)
}
}
}
}
}