Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

Commit

Permalink
Derive check-ins time (EXPOSUREAPP-5708) (#2623)
Browse files Browse the repository at this point in the history
* Map presence tracing configuration parameters

* Give priority to config values

* Use presence tracing config in QR Code creation

* lint

* Fix tests

* Create TimeIntervalDeriver.kt

* Implement time deriver

* Add test class

* Adjustment

* Add test scenarios

* detekt

* Specify locale

* Set time zone

* Make inRange public

* Use common in Range - specify the timestamp in seconds

* lint

* Explicit types

Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com>
Co-authored-by: Ralf Gehrer <ralfgehrer@users.noreply.github.com>
  • Loading branch information
3 people authored Mar 17, 2021
1 parent 709ff32 commit 2fee6d3
Show file tree
Hide file tree
Showing 3 changed files with 359 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package de.rki.coronawarnapp.eventregistration.checkins.derivetime

import de.rki.coronawarnapp.appconfig.PresenceTracingSubmissionParamContainer
import de.rki.coronawarnapp.risk.DefaultRiskLevels.Companion.inRange
import timber.log.Timber
import java.util.concurrent.TimeUnit
import kotlin.math.max
import kotlin.math.roundToLong

private val INTERVAL_LENGTH_IN_SECONDS = TimeUnit.MINUTES.toSeconds(10L)

private fun alignToInterval(timestamp: Long) =
(timestamp / INTERVAL_LENGTH_IN_SECONDS) * INTERVAL_LENGTH_IN_SECONDS

/**
* Derive CheckIn start and end times
* @param startTimestampInSeconds [Long] timestamp in seconds
* @param endTimestampInSeconds [Long] timestamp in seconds
*/
fun PresenceTracingSubmissionParamContainer.deriveTime(
startTimestampInSeconds: Long,
endTimestampInSeconds: Long
): Pair<Long, Long>? {
val durationInSeconds = max(0, endTimestampInSeconds - startTimestampInSeconds)
Timber.d("durationInSeconds: $durationInSeconds")

val durationInMinutes = TimeUnit.SECONDS.toMinutes(durationInSeconds)
Timber.d("durationInMinutes: $durationInMinutes")

val dropDueToDuration: Boolean = durationFilters.any { durationFilter ->
durationFilter.dropIfMinutesInRange.inRange(durationInMinutes)
}
Timber.d("dropDueToDuration: $dropDueToDuration")
if (dropDueToDuration) return null

val aerosoleDecays: List<Double> = aerosoleDecayLinearFunctions.filter { aerosole ->
aerosole.minutesRange.inRange(durationInMinutes)
}.map { aerosole ->
aerosole.slope * durationInSeconds + TimeUnit.MINUTES.toSeconds(aerosole.intercept.toLong())
}
Timber.d("aerosoleDecays:$aerosoleDecays")
val aerosoleDecayInSeconds: Double = aerosoleDecays.firstOrNull() ?: 0.0 // Default: zero, i.e. 'no decay'
Timber.d("aerosoleDecayInSeconds: $aerosoleDecayInSeconds")

val relevantEndTimestamp = endTimestampInSeconds + aerosoleDecayInSeconds.toLong()
val relevantStartIntervalTimestamp = alignToInterval(startTimestampInSeconds)
val relevantEndIntervalTimestamp = alignToInterval(relevantEndTimestamp)
val overlapWithStartInterval = relevantStartIntervalTimestamp + INTERVAL_LENGTH_IN_SECONDS - startTimestampInSeconds
val overlapWithEndInterval = relevantEndTimestamp - relevantEndIntervalTimestamp
Timber.d("overlapWithStartInterval: $overlapWithStartInterval")
Timber.d("overlapWithEndInterval: $overlapWithEndInterval")

val targetDurationInSeconds =
((durationInSeconds + aerosoleDecayInSeconds) / INTERVAL_LENGTH_IN_SECONDS).roundToLong() *
INTERVAL_LENGTH_IN_SECONDS

Timber.d("targetDurationInSeconds:$targetDurationInSeconds")

return if (overlapWithEndInterval > overlapWithStartInterval) {
Timber.d(
"overlapWithEndInterval:%s > overlapWithStartInterval:%s",
overlapWithEndInterval,
overlapWithStartInterval
)
val newEndTimestamp = relevantEndIntervalTimestamp + INTERVAL_LENGTH_IN_SECONDS
val newStartTimestamp = newEndTimestamp - targetDurationInSeconds
newStartTimestamp to newEndTimestamp
} else {
Timber.d(
"overlapWithEndInterval:%s, overlapWithStartInterval:%s",
overlapWithEndInterval,
overlapWithStartInterval
)
val newEndTimestamp = relevantStartIntervalTimestamp + targetDurationInSeconds
relevantStartIntervalTimestamp to newEndTimestamp
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ class DefaultRiskLevels @Inject constructor() : RiskLevels {
"The Report Type returned by the ENF is not known"
)

private fun <T : Number> RiskCalculationParametersOuterClass.Range.inRange(value: T): Boolean =
fun <T : Number> RiskCalculationParametersOuterClass.Range.inRange(value: T): Boolean =
when {
minExclusive && value.toDouble() <= min -> false
!minExclusive && value.toDouble() < min -> false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
package de.rki.coronawarnapp.eventregistration.checkins.derivetime

import de.rki.coronawarnapp.appconfig.PresenceTracingSubmissionParamContainer
import de.rki.coronawarnapp.server.protocols.internal.v2
.PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters.DurationFilter
import de.rki.coronawarnapp.server.protocols.internal.v2
.PresenceTracingParametersOuterClass.PresenceTracingSubmissionParameters.AerosoleDecayFunctionLinear
import de.rki.coronawarnapp.server.protocols.internal.v2.RiskCalculationParametersOuterClass.Range
import io.kotest.matchers.shouldBe
import org.joda.time.DateTime
import org.joda.time.DateTimeZone
import org.joda.time.format.DateTimeFormat
import org.joda.time.format.DateTimeFormatter
import org.junit.jupiter.api.Test

import testhelpers.BaseTest
import java.util.concurrent.TimeUnit

/**
* Test scenarios reference: [https://github.com/corona-warn-app/cwa-app-tech-spec/blob/
* proposal/event-registration-mvp/test-cases/pt-derive-time-interval-data.json]
*/
internal class TimeIntervalDeriverTest : BaseTest() {

/* "defaultConfiguration": {
"durationFilters": [
{
"dropIfDurationInRange": {
"min": 0,
"max": 10,
"maxExclusive": true
}
}
],
"aerosoleDecayTime": [
{
"durationRange": {
"min": 0,
"max": 30
},
"slope": 1,
"intercept": 0
},
{
"durationRange": {
"min": 30,
"max": 9999,
"minExclusive": true
},
"slope": 0,
"intercept": 30
}
]
} */

private val timeFormat: DateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd HH:mm Z")
.withZone(DateTimeZone.forID("Europe/Berlin"))

private val presenceTracingConfig = PresenceTracingSubmissionParamContainer(
durationFilters = listOf(
DurationFilter.newBuilder()
.setDropIfMinutesInRange(
Range.newBuilder()
.setMin(0.0)
.setMax(10.0)
.setMaxExclusive(true)
.build()
)
.build()
),
aerosoleDecayLinearFunctions = listOf(
AerosoleDecayFunctionLinear.newBuilder()
.setMinutesRange(
Range.newBuilder()
.setMin(0.0)
.setMax(30.0)
.build()
)
.setSlope(1.0)
.setIntercept(0.0)
.build(),
AerosoleDecayFunctionLinear.newBuilder()
.setMinutesRange(
Range.newBuilder()
.setMin(30.0)
.setMax(9999.0)
.setMinExclusive(true)
.build()
)
.setSlope(0.0)
.setIntercept(30.0)
.build()
)
)

@Test
fun `Scenario 1`() {
/*
"description": "Scenario 1",
"startDateStr": "2021-03-04 10:21+01:00",
"endDateStr": "2021-03-04 10:29+01:00",
"expStartDateStr": null,
"expEndDateStr": null
*/
presenceTracingConfig.deriveTime(
timeInSeconds("2021-03-04 10:21 +01:00"),
timeInSeconds("2021-03-04 10:29 +01:00")
) shouldBe null
}

@Test
fun `Scenario 2`() {
/*
"description": "Scenario 2",
"startDateStr": "2021-03-04 10:20+01:00",
"endDateStr": "2021-03-04 10:30+01:00",
"expStartDateStr": "2021-03-04 10:20+01:00",
"expEndDateStr": "2021-03-04 10:40+01:00"
*/
val (startTime, endTime) = presenceTracingConfig.deriveTime(
timeInSeconds("2021-03-04 10:20 +01:00"),
timeInSeconds("2021-03-04 10:30 +01:00")
)!!

timeToString(startTime) shouldBe "2021-03-04 10:20 +0100"
timeToString(endTime) shouldBe "2021-03-04 10:40 +0100"
}

@Test
fun `Scenario 3`() {
/*
"description": "Scenario 3",
"startDateStr": "2021-03-04 10:21+01:00",
"endDateStr": "2021-03-04 10:31+01:00",
"expStartDateStr": "2021-03-04 10:20+01:00",
"expEndDateStr": "2021-03-04 10:40+01:00"
*/
val (startTime, endTime) = presenceTracingConfig.deriveTime(
timeInSeconds("2021-03-04 10:21 +01:00"),
timeInSeconds("2021-03-04 10:31 +01:00")
)!!

timeToString(startTime) shouldBe "2021-03-04 10:20 +0100"
timeToString(endTime) shouldBe "2021-03-04 10:40 +0100"
}

@Test
fun `Scenario 4`() {
/*
"description": "Scenario 4",
"startDateStr": "2021-03-04 10:26+01:00",
"endDateStr": "2021-03-04 10:36+01:00",
"expStartDateStr": "2021-03-04 10:30+01:00",
"expEndDateStr": "2021-03-04 10:50+01:00"
*/
val (startTime, endTime) = presenceTracingConfig.deriveTime(
timeInSeconds("2021-03-04 10:26 +01:00"),
timeInSeconds("2021-03-04 10:36 +01:00")
)!!

timeToString(startTime) shouldBe "2021-03-04 10:30 +0100"
timeToString(endTime) shouldBe "2021-03-04 10:50 +0100"
}

@Test
fun `Scenario 5`() {
/*
"description": "Scenario 5",
"startDateStr": "2021-03-04 10:21+01:00",
"endDateStr": "2021-03-04 10:33+01:00",
"expStartDateStr": "2021-03-04 10:20+01:00",
"expEndDateStr": "2021-03-04 10:40+01:00"
*/
val (startTime, endTime) = presenceTracingConfig.deriveTime(
timeInSeconds("2021-03-04 10:21 +01:00"),
timeInSeconds("2021-03-04 10:33 +01:00")
)!!

timeToString(startTime) shouldBe "2021-03-04 10:20 +0100"
timeToString(endTime) shouldBe "2021-03-04 10:40 +0100"
}

@Test
fun `Scenario 6`() {
/*
"description": "Scenario 6",
"startDateStr": "2021-03-04 10:24+01:00",
"endDateStr": "2021-03-04 10:36+01:00",
"expStartDateStr": "2021-03-04 10:30+01:00",
"expEndDateStr": "2021-03-04 10:50+01:00"
*/
val (startTime, endTime) = presenceTracingConfig.deriveTime(
timeInSeconds("2021-03-04 10:24 +01:00"),
timeInSeconds("2021-03-04 10:36 +01:00")
)!!

timeToString(startTime) shouldBe "2021-03-04 10:30 +0100"
timeToString(endTime) shouldBe "2021-03-04 10:50 +0100"
}

@Test
fun `Scenario 7`() {
/*
"description": "Scenario 7",
"startDateStr": "2021-03-04 10:25+01:00",
"endDateStr": "2021-03-04 10:39+01:00",
"expStartDateStr": "2021-03-04 10:20+01:00",
"expEndDateStr": "2021-03-04 10:50+01:00"
*/
val (startTime, endTime) = presenceTracingConfig.deriveTime(
timeInSeconds("2021-03-04 10:25 +01:00"),
timeInSeconds("2021-03-04 10:39 +01:00")
)!!

timeToString(startTime) shouldBe "2021-03-04 10:20 +0100"
timeToString(endTime) shouldBe "2021-03-04 10:50 +0100"
}

@Test
fun `Scenario 8`() {
/*
"description": "Scenario 8",
"startDateStr": "2021-03-04 10:28+01:00",
"endDateStr": "2021-03-04 10:42+01:00",
"expStartDateStr": "2021-03-04 10:30+01:00",
"expEndDateStr": "2021-03-04 11:00+01:00"
*/
val (startTime, endTime) = presenceTracingConfig.deriveTime(
timeInSeconds("2021-03-04 10:28 +01:00"),
timeInSeconds("2021-03-04 10:42 +01:00")
)!!

timeToString(startTime) shouldBe "2021-03-04 10:30 +0100"
timeToString(endTime) shouldBe "2021-03-04 11:00 +0100"
}

@Test
fun `Scenario 9`() {
/*
"description": "Scenario 9",
"startDateStr": "2021-03-04 10:25+01:00",
"endDateStr": "2021-03-04 10:40+01:00",
"expStartDateStr": "2021-03-04 10:20+01:00",
"expEndDateStr": "2021-03-04 10:50+01:00"
*/
val (startTime, endTime) = presenceTracingConfig.deriveTime(
timeInSeconds("2021-03-04 10:25 +01:00"),
timeInSeconds("2021-03-04 10:40 +01:00")
)!!

timeToString(startTime) shouldBe "2021-03-04 10:20 +0100"
timeToString(endTime) shouldBe "2021-03-04 10:50 +0100"
}

@Test
fun `Scenario 10`() {
/*
"description": "Scenario 10",
"startDateStr": "2021-03-04 10:22+01:00",
"endDateStr": "2021-03-04 11:12+01:00",
"expStartDateStr": "2021-03-04 10:20+01:00",
"expEndDateStr": "2021-03-04 11:40+01:00"
*/
val (startTime, endTime) = presenceTracingConfig.deriveTime(
timeInSeconds("2021-03-04 10:22 +01:00"),
timeInSeconds("2021-03-04 11:12 +01:00")
)!!

timeToString(startTime) shouldBe "2021-03-04 10:20 +0100"
timeToString(endTime) shouldBe "2021-03-04 11:40 +0100"
}

private fun timeInSeconds(dateTime: String): Long {
val millis = timeFormat.parseDateTime(dateTime).millis
return TimeUnit.MILLISECONDS.toSeconds(millis)
}

private fun timeToString(timeInSecond: Long): String {
return DateTime(TimeUnit.SECONDS.toMillis(timeInSecond)).toString(timeFormat)
}
}

0 comments on commit 2fee6d3

Please sign in to comment.