Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions kotlinx.interval.testcases/src/commonMain/kotlin/IntervalTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import kotlin.test.*
* which should be smaller than [c].
* For evenly-spaced types of [T], the distance between [a] and [b], and [b] and [c], should be greater than the spacing
* between any two subsequent values in the set.
* For non-evenly-spaced types, the distance between [a] and [b] should be small enough so that there isn't sufficient
* precision to represent them as individual values when shifted close to the max possible values represented by [T].
*/
@Suppress( "FunctionName" )
abstract class IntervalTest<T : Comparable<T>, TSize : Comparable<TSize>>(
Expand Down Expand Up @@ -498,6 +500,20 @@ abstract class IntervalTest<T : Comparable<T>, TSize : Comparable<TSize>>(
assertEquals( expectedShift, shifted.offsetAmount )
}

@Test
fun shift_tiny_intervals_long_distances_for_value_types_with_lossy_precision()
{
if ( valueOperations.spacing != null ) return // Evenly-spaced types don't lose precision.

val tinyInterval = createOpenInterval( a, b )
val shifted = tinyInterval.shift( sizeOperations.maxValue )

// Due to loss of precision, there is no more distinction between `a` and `b` after shifting, resulting in an
// interval with `size` 0. This causes the interval to "collapse" into a single value, represented as a closed
// interval.
assertTrue( shifted.shiftedInterval.isClosedInterval )
}

@Test
fun intersects_for_fully_contained_intervals()
{
Expand Down
36 changes: 29 additions & 7 deletions kotlinx.interval/src/commonMain/kotlin/Interval.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,32 @@ open class Interval<T : Comparable<T>, TSize : Comparable<TSize>>(
val size: TSize get() = operations.getDistance( start, end )


/**
* Safe initialization of a calculated interval in case loss of precision can cause invalid initialization.
* I.e., coerce values to the minimum and maximum allowed values, and collapse the interval to a single value in
* case its size is zero.
*/
private fun safeInterval(
start: T,
isStartIncluded: Boolean,
end: T,
isEndIncluded: Boolean
): Interval<T, TSize>
{
val coercedStart = coerceMinMax( start )
val coercedEnd = coerceMinMax( end )

val interval =
if ( coercedStart == coercedEnd ) Interval( coercedStart, true, coercedEnd, true, operations )
else Interval( coercedStart, isStartIncluded, coercedEnd, isEndIncluded, operations )
return interval
}

private fun coerceMinMax( value: T ) =
if ( value > operations.maxValue ) operations.maxValue
else if ( value < operations.minValue ) operations.minValue
else value

override fun iterator(): Iterator<Interval<T, TSize>> = listOf( this ).iterator()

override fun getBounds(): Interval<T, TSize> = this.canonicalize()
Expand Down Expand Up @@ -190,13 +216,9 @@ open class Interval<T : Comparable<T>, TSize : Comparable<TSize>>(
val toShift = if ( overflows ) maxShift else amount

// Return shifted interval.
val shifted = Interval(
operations.unsafeShift( start, toShift, invertDirection ),
isStartIncluded,
operations.unsafeShift( end, toShift, invertDirection ),
isEndIncluded,
operations
)
val shiftedStart = operations.unsafeShift( start, toShift, invertDirection )
val shiftedEnd = operations.unsafeShift( end, toShift, invertDirection )
val shifted = safeInterval( shiftedStart, isStartIncluded, shiftedEnd, isEndIncluded )
return ShiftResult( shifted, toShift )
}

Expand Down
Loading