Skip to content

Commit fc39bc9

Browse files
committed
Add Interval.getValueAt()
Test prerequisites of `IntervalTest` had to be updated to facilitate testing this.
1 parent 774542d commit fc39bc9

File tree

4 files changed

+118
-13
lines changed

4 files changed

+118
-13
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ The following operations are specific to `Interval<T, TSize>`:
9191
| `lowerBound`, `upperBound` | Corresponds to `start` and `end`, but swapped if `isReversed`. |
9292
| `isLowerBoundIncluded`, `isUpperBoundIncluded` | Corresponds to `isStartIncluded` and `isEndIncluded`, but swapped if `isReversed`. |
9393
| `size` | The absolute difference between `start` and `end`. |
94+
| `getValueAt()` | Get the value at a given percentage inside (0.0–1.0) or outside (< 0.0, > 1.0) the interval. |
9495
| `nonReversed()` | `reverse()` the interval in case it `isReversed`. |
9596
| `reverse()` | Return an interval which swaps `start` with `end`, as well as boundary inclusions. |
9697
| `canonicalize()` | Return the interval in canonical form. E.g., The canonical form of `[5, 1)` is `[2, 5]` for integer types. |

kotlinx.interval.datetime/src/commonTest/kotlin/InstantIntervalTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ import kotlin.time.Duration
77

88
private val a = Instant.fromEpochSeconds( 0, 50 )
99
private val b = Instant.fromEpochSeconds( 0, 100 )
10-
private val c = Instant.fromEpochSeconds( 100, 50 )
10+
private val c = Instant.fromEpochSeconds( 0, 150 )
1111

1212
object InstantIntervalTest : IntervalTest<Instant, Duration>( a, b, c, b - a, InstantInterval.Operations )

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

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import kotlin.test.*
88

99
/**
1010
* Tests for [Interval] which creates intervals for testing using [a], which should be smaller than [b],
11-
* which should be smaller than [c].
12-
* For evenly-spaced types of [T], the distance between [a] and [b], and [b] and [c], should be greater than the spacing
13-
* between any two subsequent values in the set.
14-
* For non-evenly-spaced types, the distance between [a] and [b] should be small enough so that there isn't sufficient
15-
* precision to represent them as individual values when shifted close to the max possible values represented by [T].
11+
* which should be smaller than [c]. The distance between [a] and [b], and [b] and [c], should be equal.
12+
* In addition, for evenly-spaced types of [T], this distance should be greater than the spacing between any two
13+
* subsequent values in the set.
14+
* And, for non-evenly-spaced types, this distance should be small enough so that there isn't sufficient precision to
15+
* represent them as individual values when shifted close to the max possible values represented by [T].
1616
*/
1717
@Suppress( "FunctionName" )
1818
abstract class IntervalTest<T : Comparable<T>, TSize : Comparable<TSize>>(
@@ -59,14 +59,14 @@ abstract class IntervalTest<T : Comparable<T>, TSize : Comparable<TSize>>(
5959
assertTrue( a < b && b < c )
6060
assertTrue( abSize > sizeOperations.additiveIdentity )
6161

62-
// For evenly-spaced types, the distance between a-b, and b-c, should be greater than the spacing.
62+
// The distance between a-b and b-c should be equal.
63+
val abSize = valueOperations.unsafeSubtract( b, a )
64+
val bcSize = valueOperations.unsafeSubtract( c, b )
65+
assertTrue( abSize == bcSize )
66+
67+
// For evenly-spaced types, the distance should be e greater than the spacing.
6368
val spacing = valueOperations.spacing
64-
if ( spacing != null )
65-
{
66-
val abSize = valueOperations.unsafeSubtract( b, a )
67-
val bcSize = valueOperations.unsafeSubtract( c, b )
68-
assertTrue( abSize > spacing && bcSize > spacing )
69-
}
69+
if ( spacing != null ) assertTrue( abSize > spacing )
7070
}
7171

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

182+
@Test
183+
fun getValueAt_inside_interval()
184+
{
185+
val acIntervals = createAllInclusionTypeIntervals( a, c )
186+
187+
for ( ac in acIntervals )
188+
{
189+
assertEquals( a,ac.getValueAt( 0.0 ) )
190+
assertEquals( b, ac.getValueAt( 0.5 ) )
191+
assertEquals( c, ac.getValueAt( 1.0 ) )
192+
}
193+
}
194+
195+
@Test
196+
fun getValueAt_outside_interval()
197+
{
198+
val abIntervals = createAllInclusionTypeIntervals( a, b )
199+
for ( ab in abIntervals )
200+
{
201+
assertEquals( c, ab.getValueAt( 2.0 ) )
202+
}
203+
204+
val bcIntervals = createAllInclusionTypeIntervals( b, c )
205+
for ( bc in bcIntervals )
206+
{
207+
assertEquals( a, bc.getValueAt( -1.0 ) )
208+
}
209+
}
210+
211+
@Test
212+
fun getValueAt_reverse_intervals()
213+
{
214+
val adIntervals = createAllInclusionTypeIntervals( a, d )
215+
216+
for ( ad in adIntervals )
217+
{
218+
val nonReversed = ad.getValueAt( 0.2 )
219+
val reversed = ad.reverse().getValueAt( 0.8 )
220+
assertEquals(
221+
valueOperations.toDouble( nonReversed ),
222+
valueOperations.toDouble( reversed ),
223+
absoluteTolerance = 0.000000001
224+
)
225+
}
226+
}
227+
228+
@Test
229+
fun getValueAt_returned_value_overflows()
230+
{
231+
val maxIntervals = createAllInclusionTypeIntervals( operations.minValue, operations.maxValue )
232+
233+
for ( max in maxIntervals )
234+
{
235+
assertEquals( max.start, max.getValueAt( 0.0 ) )
236+
// Loss of precision for large values as part of double conversion is expected.
237+
// Therefore, compare double-converted values which have the same loss of precision.
238+
assertEquals(
239+
valueOperations.toDouble(max.end ),
240+
valueOperations.toDouble( max.getValueAt( 1.0 ) )
241+
)
242+
assertFailsWith<ArithmeticException> { max.getValueAt( -0.1 ) }
243+
assertFailsWith<ArithmeticException> { max.getValueAt( 1.1 ) }
244+
}
245+
}
246+
247+
@Test
248+
fun getValueAt_percentage_overflows()
249+
{
250+
val ab = createClosedInterval( a, b )
251+
252+
val maxPercentage = sizeOperations.toDouble( sizeOperations.maxValue ) / sizeOperations.toDouble( ab.size )
253+
val tooBigPercentage = maxPercentage * 2
254+
255+
assertFailsWith<ArithmeticException> { ab.getValueAt( tooBigPercentage ) }
256+
}
257+
182258
@Test
183259
fun minus_for_interval_lying_within()
184260
{

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,34 @@ open class Interval<T : Comparable<T>, TSize : Comparable<TSize>>(
122122
&& ( upperCompare < 0 || (upperCompare == 0 && isUpperBoundIncluded) )
123123
}
124124

125+
/**
126+
* Get the value at a given percentage within (0.0–1.0) or outside (< 0.0, > 1.0) of the interval.
127+
* 0.0 corresponds to [start] and 1.0 to [end].
128+
* The calculation is performed using [Double] arithmetic and the result is rounded to the to nearest value of [T].
129+
*
130+
* @throws ArithmeticException if the resulting value falls outside the range which can be represented by [T].
131+
*/
132+
fun getValueAt( percentage: Double ): T
133+
{
134+
val normalizedPercentage = if ( isReversed ) 1.0 - percentage else percentage
135+
val shiftLeft = normalizedPercentage < 0
136+
val absPercentage = if ( shiftLeft ) -normalizedPercentage else normalizedPercentage
137+
val addToLowerBoundDouble = sizeOperations.toDouble( size ) * absPercentage
138+
139+
// Throw when the resulting value can't be represented by T.
140+
if ( shiftLeft || normalizedPercentage > 1 )
141+
{
142+
val max = if ( shiftLeft ) operations.minValue else operations.maxValue
143+
val toMax = operations.getDistance( lowerBound, max )
144+
val toMaxDouble = sizeOperations.toDouble( toMax )
145+
if ( addToLowerBoundDouble > toMaxDouble )
146+
throw ArithmeticException( "The resulting value is out of bounds for this type." )
147+
}
148+
149+
val addToLowerBoundSize = sizeOperations.fromDouble( addToLowerBoundDouble )
150+
return operations.unsafeShift( lowerBound, addToLowerBoundSize, shiftLeft )
151+
}
152+
125153
/**
126154
* Return an [IntervalUnion] representing all [T] values in this interval,
127155
* excluding all [T] values in the specified interval [toSubtract].

0 commit comments

Comments
 (0)