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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ The following operations are specific to `Interval<T, TSize>`:
| `lowerBound`, `upperBound` | Corresponds to `start` and `end`, but swapped if `isReversed`. |
| `isLowerBoundIncluded`, `isUpperBoundIncluded` | Corresponds to `isStartIncluded` and `isEndIncluded`, but swapped if `isReversed`. |
| `size` | The absolute difference between `start` and `end`. |
| `getValueAt()` | Get the value at a given percentage inside (0.0–1.0) or outside (< 0.0, > 1.0) the interval. |
| `nonReversed()` | `reverse()` the interval in case it `isReversed`. |
| `reverse()` | Return an interval which swaps `start` with `end`, as well as boundary inclusions. |
| `canonicalize()` | Return the interval in canonical form. E.g., The canonical form of `[5, 1)` is `[2, 5]` for integer types. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ import kotlin.time.Duration

private val a = Instant.fromEpochSeconds( 0, 50 )
private val b = Instant.fromEpochSeconds( 0, 100 )
private val c = Instant.fromEpochSeconds( 100, 50 )
private val c = Instant.fromEpochSeconds( 0, 150 )

object InstantIntervalTest : IntervalTest<Instant, Duration>( a, b, c, b - a, InstantInterval.Operations )
100 changes: 88 additions & 12 deletions kotlinx.interval.testcases/src/commonMain/kotlin/IntervalTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import kotlin.test.*

/**
* Tests for [Interval] which creates intervals for testing using [a], which should be smaller than [b],
* 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].
* which should be smaller than [c]. The distance between [a] and [b], and [b] and [c], should be equal.
* In addition, for evenly-spaced types of [T], this distance should be greater than the spacing between any two
* subsequent values in the set.
* And, for non-evenly-spaced types, this distance 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 @@ -59,14 +59,14 @@ abstract class IntervalTest<T : Comparable<T>, TSize : Comparable<TSize>>(
assertTrue( a < b && b < c )
assertTrue( abSize > sizeOperations.additiveIdentity )

// For evenly-spaced types, the distance between a-b, and b-c, should be greater than the spacing.
// The distance between a-b and b-c should be equal.
val abSize = valueOperations.unsafeSubtract( b, a )
val bcSize = valueOperations.unsafeSubtract( c, b )
assertTrue( abSize == bcSize )

// For evenly-spaced types, the distance should be e greater than the spacing.
val spacing = valueOperations.spacing
if ( spacing != null )
{
val abSize = valueOperations.unsafeSubtract( b, a )
val bcSize = valueOperations.unsafeSubtract( c, b )
assertTrue( abSize > spacing && bcSize > spacing )
}
if ( spacing != null ) assertTrue( abSize > spacing )
}

@Test
Expand Down Expand Up @@ -179,6 +179,82 @@ abstract class IntervalTest<T : Comparable<T>, TSize : Comparable<TSize>>(
openIntervals.forEach { assertTrue( a !in it && b !in it ) }
}

@Test
fun getValueAt_inside_interval()
{
val acIntervals = createAllInclusionTypeIntervals( a, c )

for ( ac in acIntervals )
{
assertEquals( a,ac.getValueAt( 0.0 ) )
assertEquals( b, ac.getValueAt( 0.5 ) )
assertEquals( c, ac.getValueAt( 1.0 ) )
}
}

@Test
fun getValueAt_outside_interval()
{
val abIntervals = createAllInclusionTypeIntervals( a, b )
for ( ab in abIntervals )
{
assertEquals( c, ab.getValueAt( 2.0 ) )
}

val bcIntervals = createAllInclusionTypeIntervals( b, c )
for ( bc in bcIntervals )
{
assertEquals( a, bc.getValueAt( -1.0 ) )
}
}

@Test
fun getValueAt_reverse_intervals()
{
val adIntervals = createAllInclusionTypeIntervals( a, d )

for ( ad in adIntervals )
{
val nonReversed = ad.getValueAt( 0.2 )
val reversed = ad.reverse().getValueAt( 0.8 )
assertEquals(
valueOperations.toDouble( nonReversed ),
valueOperations.toDouble( reversed ),
absoluteTolerance = 0.000000001
)
}
}

@Test
fun getValueAt_returned_value_overflows()
{
val maxIntervals = createAllInclusionTypeIntervals( operations.minValue, operations.maxValue )

for ( max in maxIntervals )
{
assertEquals( max.start, max.getValueAt( 0.0 ) )
// Loss of precision for large values as part of double conversion is expected.
// Therefore, compare double-converted values which have the same loss of precision.
assertEquals(
valueOperations.toDouble(max.end ),
valueOperations.toDouble( max.getValueAt( 1.0 ) )
)
assertFailsWith<ArithmeticException> { max.getValueAt( -0.1 ) }
assertFailsWith<ArithmeticException> { max.getValueAt( 1.1 ) }
}
}

@Test
fun getValueAt_percentage_overflows()
{
val ab = createClosedInterval( a, b )

val maxPercentage = sizeOperations.toDouble( sizeOperations.maxValue ) / sizeOperations.toDouble( ab.size )
val tooBigPercentage = maxPercentage * 2

assertFailsWith<ArithmeticException> { ab.getValueAt( tooBigPercentage ) }
}

@Test
fun minus_for_interval_lying_within()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,24 @@ abstract class TypeOperationsTest<T : Comparable<T>>(
}
}

@Test
fun fromDouble_rounds_to_nearest_value()
{
// Rounding only needed for evenly-spaced types.
val spacing = typeOperations.spacing
if ( spacing == null ) return

val next = typeOperations.unsafeAdd( a, spacing )
val aDouble = typeOperations.toDouble( a )
val nextDouble = typeOperations.toDouble( next )
val oneThird = (nextDouble - aDouble) / 3
val closerToA = aDouble + oneThird
val closerToNext = nextDouble - oneThird

assertEquals( a, typeOperations.fromDouble( closerToA ) )
assertEquals( next, typeOperations.fromDouble( closerToNext ) )
}

@Test
fun fromDouble_overflows_past_max()
{
Expand Down
12 changes: 10 additions & 2 deletions kotlinx.interval/src/commonMain/kotlin/BasicTypeOperations.kt
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,11 @@ internal object UIntOperations : TypeOperations<UInt>
override fun unsafeAdd( a: UInt, b: UInt ): UInt = a + b
override fun unsafeSubtract( a: UInt, b: UInt ): UInt = a - b

override fun fromDouble( double: Double ): UInt = double.toUInt()
override fun fromDouble( double: Double ): UInt
{
val uInt = double.toUInt()
return if ( double - uInt.toDouble() >= 0.5 ) uInt + 1.toUInt() else uInt
}
override fun toDouble( value: UInt ): Double = value.toDouble()
}

Expand All @@ -174,7 +178,11 @@ internal object ULongOperations : TypeOperations<ULong>
override fun unsafeAdd( a: ULong, b: ULong ): ULong = a + b
override fun unsafeSubtract( a: ULong, b: ULong ): ULong = a - b

override fun fromDouble( double: Double ): ULong = double.toULong()
override fun fromDouble( double: Double ): ULong
{
val uLong = double.toULong()
return if ( double - uLong.toDouble() >= 0.5 ) uLong + 1.toULong() else uLong
}
override fun toDouble( value: ULong ): Double = value.toDouble()
}

Expand Down
28 changes: 28 additions & 0 deletions kotlinx.interval/src/commonMain/kotlin/Interval.kt
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,34 @@ open class Interval<T : Comparable<T>, TSize : Comparable<TSize>>(
&& ( upperCompare < 0 || (upperCompare == 0 && isUpperBoundIncluded) )
}

/**
* Get the value at a given percentage within (0.0–1.0) or outside (< 0.0, > 1.0) of the interval.
* 0.0 corresponds to [start] and 1.0 to [end].
* The calculation is performed using [Double] arithmetic and the result is rounded to the to nearest value of [T].
*
* @throws ArithmeticException if the resulting value falls outside the range which can be represented by [T].
*/
fun getValueAt( percentage: Double ): T
{
val normalizedPercentage = if ( isReversed ) 1.0 - percentage else percentage
val shiftLeft = normalizedPercentage < 0
val absPercentage = if ( shiftLeft ) -normalizedPercentage else normalizedPercentage
val addToLowerBoundDouble = sizeOperations.toDouble( size ) * absPercentage

// Throw when the resulting value can't be represented by T.
if ( shiftLeft || normalizedPercentage > 1 )
{
val max = if ( shiftLeft ) operations.minValue else operations.maxValue
val toMax = operations.getDistance( lowerBound, max )
val toMaxDouble = sizeOperations.toDouble( toMax )
if ( addToLowerBoundDouble > toMaxDouble )
throw ArithmeticException( "The resulting value is out of bounds for this type." )
}

val addToLowerBoundSize = sizeOperations.fromDouble( addToLowerBoundDouble )
return operations.unsafeShift( lowerBound, addToLowerBoundSize, shiftLeft )
}

/**
* Return an [IntervalUnion] representing all [T] values in this interval,
* excluding all [T] values in the specified interval [toSubtract].
Expand Down