Skip to content

Commit eca7854

Browse files
committed
Add IntervalUnion.shift
The maximum range of some interval types has been significantly restricted to guarantee that all calculations maintain the maximum amount of precision. This is something the current test suite relies on, but should be changed later. The restricted range of values isn't practical.
1 parent fa83032 commit eca7854

File tree

8 files changed

+251
-17
lines changed

8 files changed

+251
-17
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ The following operations are available for any `IntervalUnion<T, TSize>`:
6969
| `getBounds()` | Gets the upper and lower bound of the set. |
7070
| `contains()` (`in`) | Determines whether a value lies in the set. |
7171
| `minus()` (`-`) | Subtract an interval from the set. |
72-
| `plus()` (`+`) | Add an interval from the set. |
72+
| `plus()` (`+`) | Add an interval to the set. |
73+
| `shift()` | Move the interval by a specified offset. |
7374
| `intersects()` | Determines whether another interval intersects with this set. |
7475
| `setEquals()` | Determines whether a set represents the same values. |
7576
| `iterator()` | Iterate over all intervals in the union, in order. |

kotlinx.interval.datetime/src/commonMain/kotlin/InstantInterval.kt

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,14 @@ class InstantInterval( start: Instant, isStartIncluded: Boolean, end: Instant, i
2323
unsafeValueAt = { InstantOperations.additiveIdentity + it.absoluteValue }
2424
)
2525
{
26-
// Maximum positive/negative value to ensure the interval size can be represented by Duration.
27-
// One is subtracted to exclude `Duration.Infinity`.
28-
private val MAX = (DurationOperations.MAX_MILLIS - 1) / 2
26+
// Maximum positive/negative value to ensure the interval size can be represented by Duration
27+
// without losing precision (large durations can only be represented in milliseconds, not nanos).
28+
val NANOS_IN_MILLIS = 1_000_000
29+
val MAX_NANOS = Long.MAX_VALUE / 2 / NANOS_IN_MILLIS * NANOS_IN_MILLIS - 1 // Copied from Duration sources.
30+
val MAX_NANOS_IN_MILLIS = (MAX_NANOS / NANOS_IN_MILLIS) / 2
2931

30-
// Some platforms have a smaller range than `MAX` duration, and values are clamped on initialization.
31-
private val COERCED_MAX_SECONDS = minOf(
32-
Instant.fromEpochMilliseconds( -MAX ).epochSeconds.absoluteValue,
33-
Instant.fromEpochMilliseconds( MAX ).epochSeconds.absoluteValue
34-
)
35-
36-
override val minValue: Instant = Instant.fromEpochSeconds( -COERCED_MAX_SECONDS, 0 )
37-
override val maxValue: Instant = Instant.fromEpochSeconds( COERCED_MAX_SECONDS, 0 )
32+
override val minValue: Instant = Instant.fromEpochMilliseconds( -MAX_NANOS_IN_MILLIS )
33+
override val maxValue: Instant = Instant.fromEpochMilliseconds( MAX_NANOS_IN_MILLIS )
3834
}
3935
}
4036
}

kotlinx.interval.testcases/src/commonMain/kotlin/IntervalTest.kt

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import kotlin.test.*
1010
* Tests for [Interval] which creates intervals for testing using [a], which should be smaller than [b],
1111
* which should be smaller than [c].
1212
* For evenly-spaced types of [T], the distance between [a] and [b], and [b] and [c], should be greater than the spacing
13-
* between subsequent values in the set.
13+
* between any two subsequent values in the set.
1414
*/
1515
@Suppress( "FunctionName" )
1616
abstract class IntervalTest<T : Comparable<T>, TSize : Comparable<TSize>>(
@@ -410,6 +410,112 @@ abstract class IntervalTest<T : Comparable<T>, TSize : Comparable<TSize>>(
410410
assertEquals( expected, bNextC + ab )
411411
}
412412

413+
@Test
414+
fun shift_succeeds()
415+
{
416+
val bcIntervals = createAllInclusionTypeIntervals( b, c )
417+
val shiftSize = abSize
418+
val expectedShiftSize: T = valueOperations.unsafeSubtract( b, a )
419+
420+
for ( bc in bcIntervals )
421+
{
422+
val shifted = bc.shift( shiftSize )
423+
val expectedInterval = createInterval(
424+
valueOperations.unsafeAdd( b, expectedShiftSize ),
425+
bc.isStartIncluded,
426+
valueOperations.unsafeAdd( c, expectedShiftSize ),
427+
bc.isEndIncluded
428+
)
429+
assertEquals( expectedInterval, shifted.shiftedInterval )
430+
assertEquals( abSize, shifted.offsetAmount )
431+
assertEquals( bc.size, shifted.shiftedInterval.size )
432+
}
433+
}
434+
435+
@Test
436+
fun shift_using_invertedDirection_succeeds()
437+
{
438+
val bcIntervals = createAllInclusionTypeIntervals( b, c )
439+
val shiftSize = abSize
440+
val expectedShiftSize: T = valueOperations.unsafeSubtract( b, a )
441+
442+
for ( bc in bcIntervals )
443+
{
444+
val shifted = bc.shift( shiftSize, invertDirection = true )
445+
val expectedInterval = createInterval(
446+
a,
447+
bc.isStartIncluded,
448+
valueOperations.unsafeSubtract( c, expectedShiftSize ),
449+
bc.isEndIncluded
450+
)
451+
assertEquals( expectedInterval, shifted.shiftedInterval )
452+
assertEquals( abSize, shifted.offsetAmount )
453+
assertEquals( bc.size, shifted.shiftedInterval.size )
454+
}
455+
}
456+
457+
@Test
458+
fun shift_by_zero_returns_same_interval()
459+
{
460+
val toShift = createAllInclusionTypeIntervals( a, b )
461+
for ( original in toShift )
462+
{
463+
val zero = sizeOperations.additiveIdentity
464+
465+
val shiftResult = original.shift( zero )
466+
assertSame( original, shiftResult.shiftedInterval )
467+
468+
val shiftResultInverted = original.shift( zero, invertDirection = true )
469+
assertSame( original, shiftResultInverted.shiftedInterval )
470+
}
471+
}
472+
473+
@Test
474+
fun shift_with_overflow_shifts_up_to_maximum_value()
475+
{
476+
val abIntervals = createAllInclusionTypeIntervals( a, b )
477+
val aToMaxSize = operations.getDistance( a, operations.maxValue )
478+
val expectedShift: T = valueOperations.unsafeSubtract( operations.maxValue, b )
479+
val expectedShiftSize: TSize = sizeOperations.unsafeSubtract( aToMaxSize, abSize )
480+
481+
for ( ab in abIntervals )
482+
{
483+
val shifted = ab.shift( aToMaxSize )
484+
val expectedInterval = createInterval(
485+
valueOperations.unsafeAdd( a, expectedShift ),
486+
ab.isStartIncluded,
487+
operations.maxValue,
488+
ab.isEndIncluded
489+
)
490+
assertEquals( expectedInterval, shifted.shiftedInterval )
491+
assertEquals( expectedShiftSize, shifted.offsetAmount )
492+
assertEquals( ab.size, shifted.shiftedInterval.size )
493+
}
494+
}
495+
496+
@Test
497+
fun shift_using_invertedDirection_with_overflow_shifts_down_to_minimum_value()
498+
{
499+
val bcIntervals = createAllInclusionTypeIntervals( b, c )
500+
val minToCSize = operations.getDistance( operations.minValue, c )
501+
val expectedShift: T = valueOperations.unsafeSubtract( b, operations.minValue )
502+
val expectedShiftSize: TSize = operations.getDistance( operations.minValue, b )
503+
504+
for ( bc in bcIntervals )
505+
{
506+
val shifted = bc.shift( minToCSize, invertDirection = true )
507+
val expectedInterval = createInterval(
508+
operations.minValue,
509+
bc.isStartIncluded,
510+
valueOperations.unsafeSubtract( c, expectedShift ),
511+
bc.isEndIncluded
512+
)
513+
assertEquals( expectedInterval, shifted.shiftedInterval )
514+
assertEquals( expectedShiftSize, shifted.offsetAmount )
515+
assertEquals( bc.size, shifted.shiftedInterval.size )
516+
}
517+
}
518+
413519
@Test
414520
fun intersects_for_fully_contained_intervals()
415521
{

kotlinx.interval/src/commonMain/kotlin/BasicTypeIntervals.kt

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,17 @@ class FloatInterval( start: Float, isStartIncluded: Boolean, end: Float, isEndIn
114114
{
115115
companion object
116116
{
117-
internal val Operations = createIntervalTypeOperations<Float, Double>(
117+
internal val Operations = object : IntervalTypeOperations<Float, Double>(
118+
FloatOperations,
119+
DoubleOperations,
118120
getDistance = { a, b -> (b.toDouble() - a.toDouble()).absoluteValue },
119121
unsafeValueAt = { it.absoluteValue.toFloat() }
120122
)
123+
{
124+
private val MAX = 9_999_999f
125+
override val minValue: Float = -MAX
126+
override val maxValue: Float = MAX
127+
}
121128
}
122129
}
123130

@@ -132,18 +139,23 @@ fun interval( start: Float, end: Float, isStartIncluded: Boolean = true, isEndIn
132139
/**
133140
* An [Interval] representing the set of all [Double] values lying between a provided [start] and [end] value.
134141
* The interval can be closed, open, or half-open, as determined by [isStartIncluded] and [isEndIncluded].
135-
*
136-
* The [size] of [Double] intervals which exceed [Double.MAX_VALUE] will be [Double.POSITIVE_INFINITY].
137142
*/
138143
class DoubleInterval( start: Double, isStartIncluded: Boolean, end: Double, isEndIncluded: Boolean )
139144
: Interval<Double, Double>( start, isStartIncluded, end, isEndIncluded, Operations )
140145
{
141146
companion object
142147
{
143-
internal val Operations = createIntervalTypeOperations<Double, Double>(
148+
internal val Operations = object : IntervalTypeOperations<Double, Double>(
149+
DoubleOperations,
150+
DoubleOperations,
144151
getDistance = { a, b -> (b - a).absoluteValue },
145152
unsafeValueAt = { it.absoluteValue }
146153
)
154+
{
155+
private val MAX = 8_999_999_999_999_999.0
156+
override val minValue: Double = -MAX
157+
override val maxValue: Double = MAX
158+
}
147159
}
148160
}
149161

kotlinx.interval/src/commonMain/kotlin/Interval.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,46 @@ open class Interval<T : Comparable<T>, TSize : Comparable<TSize>>(
160160
pairCompare.lower.operations )
161161
}
162162

163+
override fun shift( amount: TSize, invertDirection: Boolean ): ShiftResult<Interval<T, TSize>, TSize>
164+
{
165+
val sizeZero = sizeOperations.additiveIdentity
166+
if ( amount == sizeZero ) return ShiftResult( this, amount )
167+
168+
// Clamp maximum amount to shift to min/max value which can be represented by values of T.
169+
val shiftRight = if ( amount >= sizeZero ) !invertDirection else invertDirection
170+
val valueZero = valueOperations.additiveIdentity
171+
val (max, bound) =
172+
if ( shiftRight ) Pair( operations.maxValue, upperBound )
173+
else Pair( operations.minValue, lowerBound )
174+
var maxSize = operations.getDistance( valueZero, max )
175+
val boundSize = operations.getDistance( valueZero, bound )
176+
if ( amount < sizeZero )
177+
{
178+
maxSize = sizeOperations.unsafeSubtract( sizeZero, maxSize )
179+
}
180+
val maxShift =
181+
if ( (bound < valueZero && shiftRight) || (bound > valueZero && !shiftRight) )
182+
{
183+
sizeOperations.unsafeAdd( maxSize, boundSize )
184+
}
185+
else
186+
{
187+
sizeOperations.unsafeSubtract( maxSize, boundSize )
188+
}
189+
val overflows = if ( amount < sizeZero ) amount <= maxShift else amount >= maxShift
190+
val toShift = if ( overflows ) maxShift else amount
191+
192+
// Return shifted interval.
193+
val shifted = Interval(
194+
operations.unsafeShift( start, toShift, invertDirection ),
195+
isStartIncluded,
196+
operations.unsafeShift( end, toShift, invertDirection ),
197+
isEndIncluded,
198+
operations
199+
)
200+
return ShiftResult( shifted, toShift )
201+
}
202+
163203
/**
164204
* Determines whether [interval] has at least one value in common with this interval.
165205
*/

kotlinx.interval/src/commonMain/kotlin/IntervalUnion.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ sealed interface IntervalUnion<T : Comparable<T>, TSize : Comparable<TSize>> : I
3838
*/
3939
operator fun plus( toAdd: Interval<T, TSize> ): IntervalUnion<T, TSize>
4040

41+
/**
42+
* Returns an [IntervalUnion] offset from this interval union by the specified [amount],
43+
* or as much as possible before the minimum or maximum value that can be represented by [T] is reached.
44+
* In case the interval couldn't be offset the full [amount], the final [ShiftResult.offsetAmount] will differ.
45+
*
46+
* @param invertDirection Inverts the direction by which the interval is offset. I.e., if [amount] is positive,
47+
* shift left instead of shift right, and vice verse. This can be used to shift intervals with unsigned types,
48+
* which can't represent a negative [amount], left.
49+
*/
50+
fun shift( amount: TSize, invertDirection: Boolean = false ): ShiftResult<IntervalUnion<T, TSize>, TSize>
51+
4152
/**
4253
* Determines whether [interval] has at least one value in common with this set.
4354
*/
@@ -50,6 +61,24 @@ sealed interface IntervalUnion<T : Comparable<T>, TSize : Comparable<TSize>> : I
5061
}
5162

5263

64+
/**
65+
* The result of an [IntervalUnion.shift] operation on an interval union of type [TInterval].
66+
*/
67+
data class ShiftResult<out TInterval : IntervalUnion<*, TSize>, TSize : Comparable<TSize>>(
68+
/**
69+
* A new [IntervalUnion], offset from the interval on which the [IntervalUnion.shift] operation was performed
70+
* by [offsetAmount].
71+
*/
72+
val shiftedInterval: TInterval,
73+
/**
74+
* The final amount by which [shiftedInterval] is offset from the interval on which the [IntervalUnion.shift]
75+
* operation was performed. This may be smaller than the originally requested offset in case a larger offset would
76+
* result in a value which can't be represented by values in the interval.
77+
*/
78+
val offsetAmount: TSize
79+
)
80+
81+
5382
/**
5483
* Create an [IntervalUnion] which represents a set which contains no values.
5584
*/
@@ -70,6 +99,10 @@ private data object EmptyIntervalUnion : IntervalUnion<Comparable<Any>, Comparab
7099
toAdd: Interval<Comparable<Any>, Comparable<Any>>
71100
): IntervalUnion<Comparable<Any>, Comparable<Any>> = toAdd
72101

102+
override fun shift(
103+
amount: Comparable<Any>,
104+
invertDirection: Boolean
105+
): ShiftResult<EmptyIntervalUnion, Comparable<Any>> = ShiftResult( this, amount )
73106

74107
override fun intersects( interval: Interval<Comparable<Any>, Comparable<Any>> ): Boolean = false
75108

@@ -182,6 +215,14 @@ private class IntervalUnionPair<T : Comparable<T>, TSize : Comparable<TSize>, TU
182215
return upper.fold( lower + toAdd ) { result, upperInterval -> result + upperInterval }
183216
}
184217

218+
override fun shift(
219+
amount: TSize,
220+
invertDirection: Boolean
221+
): ShiftResult<IntervalUnionPair<T, TSize, TUnion>, TSize>
222+
{
223+
return ShiftResult( this, amount )
224+
}
225+
185226
override fun intersects( interval: Interval<T, TSize> ): Boolean
186227
{
187228
if ( lower is Interval<*, *> && lower.intersects( interval ) ) return true

kotlinx.interval/src/commonTest/kotlin/IntervalUnionTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ class EmptyIntervalUnionTest
3333
assertEquals( toAdd, (empty + toAdd).singleOrNull() )
3434
}
3535

36+
@Test
37+
fun shift_returns_empty_interval_and_never_overflows()
38+
{
39+
val toShift = UInt.MAX_VALUE
40+
41+
val emptyShifted = empty.shift( toShift )
42+
43+
assertEquals( empty, emptyShifted.shiftedInterval )
44+
assertEquals( toShift, emptyShifted.offsetAmount )
45+
}
46+
3647
@Test
3748
fun intersects_is_always_false() = assertFalse( empty.intersects( interval( 0, 10 ) ) )
3849

@@ -352,6 +363,12 @@ class IntervalUnionPairTest
352363
)
353364
}
354365

366+
@Test
367+
fun shift_TODO()
368+
{
369+
// TODO: test pair shifts
370+
}
371+
355372
@Test
356373
fun intersects_succeeds()
357374
{
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.github.whathecode.kotlinx.interval
2+
3+
import kotlin.test.*
4+
5+
6+
/**
7+
* Additional tests for [Interval] operations which are only possible on signed types.
8+
*/
9+
class SignedTypeIntervalsTest
10+
{
11+
@Test
12+
fun shift_negative_amount_equals_shift_positive_amount_using_invertedDirection()
13+
{
14+
val toShift = interval( 10.0, 20.0 )
15+
16+
val shiftedNegative = toShift.shift( -5.0 ).shiftedInterval
17+
val shiftedInverted = toShift.shift( 5.0, invertDirection = true ).shiftedInterval
18+
19+
assertEquals( shiftedInverted, shiftedNegative )
20+
}
21+
}

0 commit comments

Comments
 (0)