This repository has been archived by the owner on Jun 20, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 496
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Derive check-ins time (EXPOSUREAPP-5708) (#2623)
* 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
1 parent
709ff32
commit 2fee6d3
Showing
3 changed files
with
359 additions
and
1 deletion.
There are no files selected for viewing
77 changes: 77 additions & 0 deletions
77
...in/java/de/rki/coronawarnapp/eventregistration/checkins/derivetime/TimeIntervalDeriver.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
281 changes: 281 additions & 0 deletions
281
...ava/de/rki/coronawarnapp/eventregistration/checkins/derivetime/TimeIntervalDeriverTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |