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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,22 @@ val size: Duration = interval.size // 100 seconds
val shifted = interval shr 24.hours // 100 seconds 24 hours from now
```

Intervals support common math operations which allow concisely expressing common use cases.

```kotlin
// Two intervals of different types: a timeline visualized at some screen coordinates.
val start2025 = LocalDateTime( 2025, 1, 1, 0, 0 ).toInstant( TimeZone.UTC )
val end2025 = LocalDateTime( 2026, 1, 1, 0, 0 ).toInstant( TimeZone.UTC )
val year2025: InstantInterval = interval( start2025, end2025 )
val timelineUi: IntInterval = interval( 0, 800 ) // UI element 800 pixels wide

// Find the selected time at a given UI coordinate using linear interpolation.
val mouseX = 400
val uiPercentage: Double = timelineUi.getPercentageFor( mouseX )
val selectedTime: Instant = year2025.getValueAt( uiPercentage ) // July 2nd at noon.
```


## Interval Unions

Intervals are a subset of _interval unions_, which represent a collection of intervals.
Expand Down Expand Up @@ -92,6 +108,7 @@ The following operations are specific to `Interval<T, TSize>`:
| `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. |
| `getPercentageFor()` | Gets the percentage how far within (0.0-1.0) or outside (< 0.0, > 1.0) of the interval a value lies. |
| `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
2 changes: 1 addition & 1 deletion kotlinx.interval.datetime/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ kotlin {
commonMain {
dependencies {
api(project(":kotlinx-interval"))
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
}
}
commonTest {
Expand Down
21 changes: 21 additions & 0 deletions kotlinx.interval.datetime/src/commonTest/kotlin/Readme.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

package io.github.whathecode.kotlinx.interval.datetime

import io.github.whathecode.kotlinx.interval.IntInterval
import io.github.whathecode.kotlinx.interval.interval
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlin.test.*
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
Expand All @@ -20,4 +26,19 @@ class Readme
val size: Duration = interval.size // 100 seconds
val shifted = interval shr 24.hours // 100 seconds 24 hours from now
}

@Test
fun introduction_common_math()
{
// Two intervals of different types.
val start2025 = LocalDateTime( 2025, 1, 1, 0, 0 ).toInstant( TimeZone.UTC )
val end2025 = LocalDateTime( 2026, 1, 1, 0, 0 ).toInstant( TimeZone.UTC )
val year2025: InstantInterval = interval( start2025, end2025 )
val timelineUi: IntInterval = interval( 0, 800 ) // UI element 800 pixels wide

// Find the selected time at a given UI coordinate using linear interpolation.
val mouseX = 400
val uiPercentage: Double = timelineUi.getPercentageFor( mouseX )
val selectedTime: Instant = year2025.getValueAt( uiPercentage ) // July 2nd at noon.
}
}
47 changes: 39 additions & 8 deletions kotlinx.interval.testcases/src/commonMain/kotlin/IntervalTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,10 @@ abstract class IntervalTest<T : Comparable<T>, TSize : Comparable<TSize>>(
fun getValueAt_outside_interval()
{
val abIntervals = createAllInclusionTypeIntervals( a, b )
for ( ab in abIntervals )
{
assertEquals( c, ab.getValueAt( 2.0 ) )
}
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 ) )
}
for ( bc in bcIntervals ) assertEquals( a, bc.getValueAt( -1.0 ) )
}

@Test
Expand Down Expand Up @@ -255,6 +249,43 @@ abstract class IntervalTest<T : Comparable<T>, TSize : Comparable<TSize>>(
assertFailsWith<ArithmeticException> { ab.getValueAt( tooBigPercentage ) }
}

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

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

@Test
fun getPercentageFor_outside_interval()
{
val bcIntervals = createAllInclusionTypeIntervals( b, c )
for ( bc in bcIntervals ) assertEquals( -1.0, bc.getPercentageFor( a ) )

val abIntervals = createAllInclusionTypeIntervals( a, b )
for ( ab in abIntervals ) assertEquals( 2.0, ab.getPercentageFor( c ) )
}

@Test
fun getPercentageFor_reverse_intervals()
{
val adIntervals = createAllInclusionTypeIntervals( a, d )
for ( ad in adIntervals )
{
// a, b, c, and d all lie the same distance apart, so b and c are 1/4th of the bounds away.
val nonReversed = ad.getPercentageFor( b )
assertEquals( 0.333, nonReversed, absoluteTolerance = 0.001 )
val reversed = ad.reverse().getPercentageFor( b )
assertEquals( 0.666, reversed, absoluteTolerance = 0.001 )
}
}

@Test
fun minus_for_interval_lying_within()
{
Expand Down
36 changes: 25 additions & 11 deletions kotlinx.interval/src/commonMain/kotlin/Interval.kt
Original file line number Diff line number Diff line change
Expand Up @@ -131,23 +131,37 @@ open class Interval<T : Comparable<T>, TSize : Comparable<TSize>>(
*/
fun getValueAt( percentage: Double ): T
{
val lowerBoundDouble = valueOperations.toDouble( lowerBound )
val upperBoundDouble = valueOperations.toDouble( upperBound )
val size = upperBoundDouble - lowerBoundDouble

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
val valueDouble = lowerBoundDouble + (normalizedPercentage * size)

// Throw when the resulting value can't be represented by T.
if ( shiftLeft || normalizedPercentage > 1 )
if ( normalizedPercentage < 0.0 )
{
val min = valueOperations.toDouble( operations.minValue )
if ( valueDouble < min )
throw ArithmeticException( "The resulting value $valueDouble is out of bounds for this type." )
}
else if ( normalizedPercentage > 1.0 )
{
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 max = valueOperations.toDouble( operations.maxValue )
if ( valueDouble > max )
throw ArithmeticException( "The resulting value $valueDouble is out of bounds for this type." )
}

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

fun getPercentageFor( value: T ): Double
{
val valueDouble = valueOperations.toDouble( value )
val startDouble = valueOperations.toDouble( start )
val endDouble = valueOperations.toDouble( end )

return (valueDouble - startDouble) / (endDouble - startDouble)
}

/**
Expand Down