Cleaned up TimeWindow and added a bit more docs.

This commit is contained in:
Shams Asari
2017-07-20 13:32:53 +01:00
parent bf98f64269
commit e702025f62
18 changed files with 189 additions and 96 deletions

View File

@ -21,7 +21,6 @@ import java.math.BigDecimal
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import java.time.temporal.Temporal
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
@ -95,9 +94,6 @@ fun <A> ListenableFuture<out A>.toObservable(): Observable<A> {
}
}
// Simple infix function to add back null safety that the JDK lacks: timeA until timeB
infix fun Temporal.until(endExclusive: Temporal): Duration = Duration.between(this, endExclusive)
/** Returns the index of the given item or throws [IllegalArgumentException] if not found. */
fun <T> List<T>.indexOfOrThrow(item: T): Int {
val i = indexOf(item)

View File

@ -17,7 +17,6 @@ import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.security.PublicKey
import java.time.Duration
import java.time.Instant
import java.util.jar.JarInputStream
@ -328,63 +327,6 @@ data class AuthenticatedObject<out T : Any>(
)
// DOCEND 6
/**
* A time-window is required for validation/notarization purposes.
* If present in a transaction, contains a time that was verified by the uniqueness service. The true time must be
* between (fromTime, untilTime).
* Usually, a time-window is required to have both sides set (fromTime, untilTime).
* However, some apps may require that a time-window has a start [Instant] (fromTime), but no end [Instant] (untilTime) and vice versa.
* TODO: Consider refactoring using TimeWindow abstraction like TimeWindow.From, TimeWindow.Until, TimeWindow.Between.
*/
@CordaSerializable
class TimeWindow private constructor(
/** The time at which this transaction is said to have occurred is after this moment. */
val fromTime: Instant?,
/** The time at which this transaction is said to have occurred is before this moment. */
val untilTime: Instant?
) {
companion object {
/** Use when the left-side [fromTime] of a [TimeWindow] is only required and we don't need an end instant (untilTime). */
@JvmStatic
fun fromOnly(fromTime: Instant) = TimeWindow(fromTime, null)
/** Use when the right-side [untilTime] of a [TimeWindow] is only required and we don't need a start instant (fromTime). */
@JvmStatic
fun untilOnly(untilTime: Instant) = TimeWindow(null, untilTime)
/** Use when both sides of a [TimeWindow] must be set ([fromTime], [untilTime]). */
@JvmStatic
fun between(fromTime: Instant, untilTime: Instant): TimeWindow {
require(fromTime < untilTime) { "fromTime should be earlier than untilTime" }
return TimeWindow(fromTime, untilTime)
}
/** Use when we have a start time and a period of validity. */
@JvmStatic
fun fromStartAndDuration(fromTime: Instant, duration: Duration): TimeWindow = between(fromTime, fromTime + duration)
/**
* When we need to create a [TimeWindow] based on a specific time [Instant] and some tolerance in both sides of this instant.
* The result will be the following time-window: ([time] - [tolerance], [time] + [tolerance]).
*/
@JvmStatic
fun withTolerance(time: Instant, tolerance: Duration) = between(time - tolerance, time + tolerance)
}
/** The midpoint is calculated as fromTime + (untilTime - fromTime)/2. Note that it can only be computed if both sides are set. */
val midpoint: Instant get() = fromTime!! + Duration.between(fromTime, untilTime!!).dividedBy(2)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TimeWindow) return false
return (fromTime == other.fromTime && untilTime == other.untilTime)
}
override fun hashCode() = 31 * (fromTime?.hashCode() ?: 0) + (untilTime?.hashCode() ?: 0)
override fun toString() = "TimeWindow(fromTime=$fromTime, untilTime=$untilTime)"
}
// DOCSTART 5
/**
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for

View File

@ -0,0 +1,87 @@
package net.corda.core.contracts
import net.corda.core.internal.div
import net.corda.core.internal.until
import net.corda.core.serialization.CordaSerializable
import java.time.Duration
import java.time.Instant
/**
* A time-window is required for validation/notarization purposes. If present in a transaction, contains a time that was
* verified by the uniqueness service. The true time must be in the time interval `[fromTime, untilTime)`.
*
* Usually a time-window is required to have both sides defined. However some apps may require a time-window which is
* open-ended on one of the two sides.
*/
@CordaSerializable
abstract class TimeWindow {
companion object {
/** Creates a [TimeWindow] with null [untilTime], i.e. the time interval `[fromTime, ∞]`. [midpoint] will return null. */
@JvmStatic
fun fromOnly(fromTime: Instant): TimeWindow = From(fromTime)
/** Creates a [TimeWindow] with null [fromTime], i.e. the time interval `[∞, untilTime)`. [midpoint] will return null. */
@JvmStatic
fun untilOnly(untilTime: Instant): TimeWindow = Until(untilTime)
/**
* Creates a [TimeWindow] with the time interval `[fromTime, untilTime)`. [midpoint] will return
* `fromTime + (untilTime - fromTime) / 2`.
* @throws IllegalArgumentException If [fromTime] ≥ [untilTime]
*/
@JvmStatic
fun between(fromTime: Instant, untilTime: Instant): TimeWindow = Between(fromTime, untilTime)
/**
* Creates a [TimeWindow] with the time interval `[fromTime, fromTime + duration)`. [midpoint] will return
* `fromTime + duration / 2`
*/
@JvmStatic
fun fromStartAndDuration(fromTime: Instant, duration: Duration): TimeWindow = between(fromTime, fromTime + duration)
/**
* Creates a [TimeWindow] which is centered around [instant] with the given [tolerance] on both sides, i.e the
* time interval `[instant - tolerance, instant + tolerance)`. [midpoint] will return [instant].
*/
@JvmStatic
fun withTolerance(instant: Instant, tolerance: Duration) = between(instant - tolerance, instant + tolerance)
}
/** Returns the inclusive lower-bound of this [TimeWindow]'s interval, with null implying infinity. */
abstract val fromTime: Instant?
/** Returns the exclusive upper-bound of this [TimeWindow]'s interval, with null implying infinity. */
abstract val untilTime: Instant?
/**
* Returns the midpoint of [fromTime] and [untilTime] if both are non-null, calculated as
* `fromTime + (untilTime - fromTime)/2`, otherwise returns null.
*/
abstract val midpoint: Instant?
/** Returns true iff the given [instant] is within the time interval of this [TimeWindow]. */
abstract operator fun contains(instant: Instant): Boolean
private data class From(override val fromTime: Instant) : TimeWindow() {
override val untilTime: Instant? get() = null
override val midpoint: Instant? get() = null
override fun contains(instant: Instant): Boolean = instant >= fromTime
override fun toString(): String = "[$fromTime, ∞]"
}
private data class Until(override val untilTime: Instant) : TimeWindow() {
override val fromTime: Instant? get() = null
override val midpoint: Instant? get() = null
override fun contains(instant: Instant): Boolean = instant < untilTime
override fun toString(): String = "[∞, $untilTime)"
}
private data class Between(override val fromTime: Instant, override val untilTime: Instant) : TimeWindow() {
init {
require(fromTime < untilTime) { "fromTime must be earlier than untilTime" }
}
override val midpoint: Instant get() = fromTime + (fromTime until untilTime) / 2
override fun contains(instant: Instant): Boolean = instant >= fromTime && instant < untilTime
override fun toString(): String = "[$fromTime, $untilTime)"
}
}

View File

@ -7,9 +7,16 @@ import java.nio.charset.Charset
import java.nio.charset.StandardCharsets.UTF_8
import java.nio.file.*
import java.nio.file.attribute.FileAttribute
import java.time.Duration
import java.time.temporal.Temporal
import java.util.stream.Stream
import kotlin.reflect.KClass
infix fun Temporal.until(endExclusive: Temporal): Duration = Duration.between(this, endExclusive)
operator fun Duration.div(divider: Long): Duration = dividedBy(divider)
operator fun Duration.times(multiplicand: Long): Duration = multipliedBy(multiplicand)
/**
* Allows you to write code like: Paths.get("someDir") / "subdir" / "filename" but using the Paths API to avoid platform
* separator problems.

View File

@ -7,16 +7,5 @@ import java.time.Clock
* Checks if the current instant provided by the input clock falls within the provided time-window.
*/
class TimeWindowChecker(val clock: Clock = Clock.systemUTC()) {
fun isValid(timeWindow: TimeWindow): Boolean {
val fromTime = timeWindow.fromTime
val untilTime = timeWindow.untilTime
val now = clock.instant()
// We don't need to test for (fromTime == null && untilTime == null) or backwards bounds because the TimeWindow
// constructor already checks that.
if (fromTime != null && now < fromTime) return false
if (untilTime != null && now > untilTime) return false
return true
}
fun isValid(timeWindow: TimeWindow): Boolean = clock.instant() in timeWindow
}

View File

@ -0,0 +1,67 @@
package net.corda.core.contracts
import net.corda.core.utilities.millis
import net.corda.core.utilities.minutes
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset.UTC
class TimeWindowTest {
private val now = Instant.now()
@Test
fun fromOnly() {
val timeWindow = TimeWindow.fromOnly(now)
assertThat(timeWindow.fromTime).isEqualTo(now)
assertThat(timeWindow.untilTime).isNull()
assertThat(timeWindow.midpoint).isNull()
assertThat(timeWindow.contains(now - 1.millis)).isFalse()
assertThat(timeWindow.contains(now)).isTrue()
assertThat(timeWindow.contains(now + 1.millis)).isTrue()
}
@Test
fun untilOnly() {
val timeWindow = TimeWindow.untilOnly(now)
assertThat(timeWindow.fromTime).isNull()
assertThat(timeWindow.untilTime).isEqualTo(now)
assertThat(timeWindow.midpoint).isNull()
assertThat(timeWindow.contains(now - 1.millis)).isTrue()
assertThat(timeWindow.contains(now)).isFalse()
assertThat(timeWindow.contains(now + 1.millis)).isFalse()
}
@Test
fun between() {
val today = LocalDate.now()
val fromTime = today.atTime(12, 0).toInstant(UTC)
val untilTime = today.atTime(12, 30).toInstant(UTC)
val timeWindow = TimeWindow.between(fromTime, untilTime)
assertThat(timeWindow.fromTime).isEqualTo(fromTime)
assertThat(timeWindow.untilTime).isEqualTo(untilTime)
assertThat(timeWindow.midpoint).isEqualTo(today.atTime(12, 15).toInstant(UTC))
assertThat(timeWindow.contains(fromTime - 1.millis)).isFalse()
assertThat(timeWindow.contains(fromTime)).isTrue()
assertThat(timeWindow.contains(fromTime + 1.millis)).isTrue()
assertThat(timeWindow.contains(untilTime)).isFalse()
assertThat(timeWindow.contains(untilTime + 1.millis)).isFalse()
}
@Test
fun fromStartAndDuration() {
val timeWindow = TimeWindow.fromStartAndDuration(now, 10.minutes)
assertThat(timeWindow.fromTime).isEqualTo(now)
assertThat(timeWindow.untilTime).isEqualTo(now + 10.minutes)
assertThat(timeWindow.midpoint).isEqualTo(now + 5.minutes)
}
@Test
fun withTolerance() {
val timeWindow = TimeWindow.withTolerance(now, 10.minutes)
assertThat(timeWindow.fromTime).isEqualTo(now - 10.minutes)
assertThat(timeWindow.untilTime).isEqualTo(now + 10.minutes)
assertThat(timeWindow.midpoint).isEqualTo(now)
}
}

View File

@ -5,12 +5,12 @@ import net.corda.core.utilities.seconds
import org.junit.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
import java.time.ZoneOffset
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class TimeWindowCheckerTests {
val clock = Clock.fixed(Instant.now(), ZoneId.systemDefault())
val clock: Clock = Clock.fixed(Instant.now(), ZoneOffset.UTC)
val timeWindowChecker = TimeWindowChecker(clock)
@Test