Skip to content

Commit

Permalink
use factor for exponential interval (#2076)
Browse files Browse the repository at this point in the history
* use factor for exponential interval

* fix tests to respect more gentle exponential curve

* rename cap to max
  • Loading branch information
jschneidereit committed Feb 9, 2021
1 parent 1c662b8 commit 5640de8
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,31 @@ package io.kotest.assertions.until
import kotlin.math.pow
import kotlin.time.Duration
import kotlin.time.hours
import kotlin.time.milliseconds

class ExponentialInterval(private val base: Duration, private val cap: Duration?) : Interval {
override fun toString() = "ExponentialInterval(${::base.name}=$base, ${::cap.name}=$cap)"
/**
* Exponential interval implements a delay where each duration is calculated as a multiplier
* of an exponent of the default [ExponentialInterval.defaultFactor] or a user specified factor.
*
* You should start at 0 to get the base value back and at 1 for the second value in the series, e.g.:
* val interval = 2.seconds.exponential(max = Duration.INFINITE) which will produce 2s, 4s, 8s, etc.
*
* @param base the duration that is multiplied by the exponentiated factor
* @param factor the factor to exponentiate by the current iteration value
* @param max the maximum duration to clamp the resulting duration to defaults to [ExponentialInterval.defaultMax]
*/
class ExponentialInterval(private val base: Duration, private val factor: Double, private val max: Duration?) : Interval {
override fun toString() = "ExponentialInterval(${::base.name}=$base, ${::factor.name}=$factor, ${::max.name}=$max)"

override fun next(count: Int): Duration {
val amount = base.inMilliseconds.pow(count.toDouble()).toLong()
val result = amount.milliseconds
return if (cap == null) result else minOf(cap, result)
val result = base * factor.pow(count)
return if (max == null) result else minOf(max, result)
}

companion object {
val defaultCap = 2.hours
val defaultMax = 2.hours
const val defaultFactor = 2.0
}
}

fun Duration.exponential(cap: Duration? = ExponentialInterval.defaultCap) = ExponentialInterval(this, cap)
fun Duration.exponential(factor: Double = ExponentialInterval.defaultFactor, max: Duration? = ExponentialInterval.defaultMax) =
ExponentialInterval(this, factor, max)
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,29 @@ import kotlin.time.milliseconds
*
* @param offset Added to the count, so if the offset is 4, then the first value will be the 4th fib number.
* @param base The duration that is multiplied by the fibonacci value
* @param max the maximum duration to clamp the resulting duration to defaults to [FibonacciInterval.defaultMax]
*/
class FibonacciInterval(private val base: Duration, private val offset: Int, private val cap: Duration?) : Interval {
class FibonacciInterval(private val base: Duration, private val offset: Int, private val max: Duration?) : Interval {

init {
require(offset >= 0) { "Offset must be greater than or equal to 0" }
}

override fun toString() = "FibonacciInterval(${::base.name}=$base, ${::offset.name}=$offset, ${::cap.name}=${cap?.toString()})"
override fun toString() = "FibonacciInterval(${::base.name}=$base, ${::offset.name}=$offset, ${::max.name}=${max?.toString()})"

override fun next(count: Int): Duration {
val baseMs = base.toLongMilliseconds()
val total = baseMs * fibonacci(offset + count)
val result = total.milliseconds
return if (cap == null) result else minOf(cap, result)
return if (max == null) result else minOf(max, result)
}

companion object {
val defaultCap = 2.hours
val defaultMax = 2.hours
}
}

fun Duration.fibonacci(cap: Duration? = FibonacciInterval.defaultCap) = FibonacciInterval(this, 0, cap)
fun Duration.fibonacci(max: Duration? = FibonacciInterval.defaultMax) = FibonacciInterval(this, 0, max)

fun fibonacci(n: Int): Int {
tailrec fun fib(k: Int, current: Int, previous: Int): Int = when (k) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,83 @@
package com.sksamuel.kotest.assertions.until

import io.kotest.assertions.assertSoftly
import io.kotest.assertions.until.ExponentialInterval
import io.kotest.assertions.until.exponential
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.comparables.shouldBeGreaterThan
import io.kotest.matchers.comparables.shouldBeGreaterThanOrEqualTo
import io.kotest.matchers.comparables.shouldBeLessThan
import io.kotest.matchers.shouldBe
import kotlin.time.hours
import kotlin.math.pow
import kotlin.time.milliseconds
import kotlin.time.seconds

class ExponentialIntervalTest : FunSpec() {
init {
test("exponential interval should have a reasonable default cap") {
val cap = ExponentialInterval.defaultCap
test("exponential interval should have a reasonable default next") {
val identity = 2.seconds

assertSoftly(identity.exponential()) {
next(0) shouldBe identity * 1
next(1) shouldBe identity * 2
next(2) shouldBe identity * 4
}

assertSoftly(identity.exponential(factor = 3.0)) {
next(0) shouldBe identity * 1
next(1) shouldBe identity * 3
next(2) shouldBe identity * 9
}
}

test("exponential interval should have a reasonable default max") {
val max = ExponentialInterval.defaultMax
val default = 25.milliseconds.exponential()
val unbounded = 25.milliseconds.exponential(null)
val unbounded = 25.milliseconds.exponential(max = null)

val first = 0
val last = 20

unbounded.next(first) shouldBeLessThan cap
unbounded.next(last) shouldBeGreaterThan cap
unbounded.next(first) shouldBeLessThan max
unbounded.next(last) shouldBeGreaterThan max

for (i in first..last) {
val u = unbounded.next(i)
val d = default.next(i)

if (u < cap) {
if (u < max) {
d shouldBe u
} else {
d shouldBe cap
u shouldBeGreaterThan cap
d shouldBe max
u shouldBeGreaterThan max
}
}
}

test("exponential interval should respect user specified cap") {
val cap = 278.hours
val bounded = 25.milliseconds.exponential(cap = cap)
val unbounded = 25.milliseconds.exponential(null)
test("exponential interval should respect user specified max") {
val base = 25.milliseconds
val n = 5
val max = base * ExponentialInterval.defaultFactor.pow(n)
val bounded = base.exponential(max = max)
val unbounded = base.exponential(max = null)

val first = 0
val last = 20

unbounded.next(first) shouldBeLessThan cap
unbounded.next(last) shouldBeGreaterThan cap
unbounded.next(first) shouldBeLessThan max
unbounded.next(last) shouldBeGreaterThan max

for (i in first..last) {
val u = unbounded.next(i)
val b = bounded.next(i)

if (u < cap) {
if (u < max) {
b shouldBe u
i shouldBeLessThan n
} else {
b shouldBe cap
u shouldBeGreaterThan cap
i shouldBeGreaterThanOrEqualTo n
b shouldBe max
u shouldBeGreaterThanOrEqualTo max
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,50 +22,50 @@ class FibonacciIntervalTest : FunSpec() {
fibonacci(7) shouldBe 13
}

test("fibonacci interval should have a reasonable default cap") {
val cap = FibonacciInterval.defaultCap
test("fibonacci interval should have a reasonable default max") {
val max= FibonacciInterval.defaultMax
val default = 10.minutes.fibonacci()
val unbounded = 10.minutes.fibonacci(null)

val first = 0
val last = 20

unbounded.next(first) shouldBeLessThan cap
unbounded.next(last) shouldBeGreaterThan cap
unbounded.next(first) shouldBeLessThan max
unbounded.next(last) shouldBeGreaterThan max

for (i in first..last) {
val u = unbounded.next(i)
val d = default.next(i)

if (u < cap) {
if (u < max) {
d shouldBe u
} else {
d shouldBe cap
u shouldBeGreaterThan cap
d shouldBe max
u shouldBeGreaterThan max
}
}
}

test("fibonacci interval should respect user specified cap") {
val cap = FibonacciInterval.defaultCap.plus(15.minutes)
val bounded = 10.minutes.fibonacci(cap)
test("fibonacci interval should respect user specified max") {
val max = FibonacciInterval.defaultMax.plus(15.minutes)
val bounded = 10.minutes.fibonacci(max)
val unbounded = 10.minutes.fibonacci(null)

val first = 0
val last = 20

unbounded.next(first) shouldBeLessThan cap
unbounded.next(last) shouldBeGreaterThan cap
unbounded.next(first) shouldBeLessThan max
unbounded.next(last) shouldBeGreaterThan max

for (i in first..last) {
val u = unbounded.next(i)
val b = bounded.next(i)

if (u < cap) {
if (u < max) {
b shouldBe u
} else {
b shouldBe cap
u shouldBeGreaterThan cap
b shouldBe max
u shouldBeGreaterThan max
}
}
}
Expand Down

0 comments on commit 5640de8

Please sign in to comment.