Skip to content

Commit

Permalink
add test for adjacent activities calculation
Browse files Browse the repository at this point in the history
  • Loading branch information
Razeeman committed Dec 22, 2024
1 parent ba4f8eb commit 51c2375
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 76 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.example.util.simpletimetracker.domain.interactor

import com.example.util.simpletimetracker.domain.extension.orZero
import com.example.util.simpletimetracker.domain.model.RecordBase
import javax.inject.Inject

class CalculateAdjacentActivitiesInteractor @Inject constructor() {

// Doesn't count multitasked activities.
// Only whose that started after current ended.
fun calculateNextActivities(
typeId: Long,
records: List<RecordBase>,
): List<CalculationResult> {
val counts = mutableMapOf<Long, Long>()

val recordsSorted = records.sortedBy { it.timeStarted }
var currentRecord: RecordBase? = null
recordsSorted.forEach { record ->
val currentTimeEnded = currentRecord?.timeEnded
if (currentTimeEnded != null &&
currentTimeEnded <= record.timeStarted
) {
record.typeIds.firstOrNull()?.let { id ->
counts[id] = counts[id].orZero() + 1
}
currentRecord = null
}
if (currentRecord == null && typeId in record.typeIds) {
currentRecord = record
}
}

return counts.keys
.sortedByDescending { counts[it].orZero() }
.take(MAX_COUNT)
.map { CalculationResult(it, counts[it].orZero()) }
}

// TODO make more precise calculations?
fun calculateMultitasking(
typeId: Long,
records: List<RecordBase>,
): List<CalculationResult> {
val counts = mutableMapOf<Long, Long>()

val recordsSorted = records.sortedBy { it.timeStarted }
var currentRecord: RecordBase? = null
recordsSorted.forEach { record ->
val currentTimeStarted = currentRecord?.timeStarted
val currentTimeEnded = currentRecord?.timeEnded
if (currentTimeStarted != null &&
currentTimeEnded != null &&
// Find next records that was started after this one but before this one ends.
currentTimeStarted <= record.timeStarted &&
currentTimeEnded > record.timeStarted &&
// Cutoff short intersections.
currentTimeEnded - record.timeStarted > 1_000L
) {
record.typeIds.firstOrNull()?.let { id ->
counts[id] = counts[id].orZero() + 1
}
}
if (typeId in record.typeIds) {
currentRecord = record
}
}

return counts.keys
.sortedByDescending { counts[it].orZero() }
.take(MAX_COUNT)
.map { CalculationResult(it, counts[it].orZero()) }
}

data class CalculationResult(
val typeId: Long,
val count: Long,
)

companion object {
private const val MAX_COUNT = 5
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.example.util.simpletimetracker.domain.mapper

import com.example.util.simpletimetracker.domain.interactor.CalculateAdjacentActivitiesInteractor
import com.example.util.simpletimetracker.domain.interactor.CalculateAdjacentActivitiesInteractor.CalculationResult
import com.example.util.simpletimetracker.domain.model.Record
import com.example.util.simpletimetracker.domain.model.RecordBase
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

@RunWith(Parameterized::class)
class CalculateAdjacentActivitiesInteractorTest(
private val input: Pair<Long, List<RecordBase>>,
private val output: List<CalculationResult>,
) {

private val subject = CalculateAdjacentActivitiesInteractor()

@Suppress("UNCHECKED_CAST")
@Test
fun map() {
val expected = output
val actual = subject.calculateNextActivities(
typeId = input.first,
records = input.second,
)

assertEquals(
"Test failed for params $input",
expected,
actual,
)
}

companion object {
private val record: Record = Record(
id = 0L,
typeId = 0L,
timeStarted = 0L,
timeEnded = 0L,
comment = "",
tagIds = emptyList(),
)

@JvmStatic
@Parameterized.Parameters
fun data() = listOf(
// Empty.
arrayOf(
0L to emptyList<RecordBase>(),
emptyList<CalculationResult>(),
),
arrayOf(
0L to listOf(record),
emptyList<CalculationResult>(),
),
arrayOf(
0L to listOf(
record.copy(typeId = 1),
record.copy(typeId = 2),
),
emptyList<CalculationResult>(),
),
// Multitasked.
arrayOf(
0L to listOf(
record.copy(typeId = 0, timeStarted = 1, timeEnded = 10),
record.copy(typeId = 1, timeStarted = 2, timeEnded = 3),
record.copy(typeId = 2, timeStarted = 4, timeEnded = 15),
),
emptyList<CalculationResult>(),
),
// Only before.
arrayOf(
0L to listOf(
record.copy(typeId = 1, timeStarted = 1, timeEnded = 2),
record.copy(typeId = 2, timeStarted = 2, timeEnded = 3),
record.copy(typeId = 0, timeStarted = 3, timeEnded = 4),
),
emptyList<CalculationResult>(),
),
// One after.
arrayOf(
0L to listOf(
record.copy(typeId = 0, timeStarted = 0, timeEnded = 1),
record.copy(typeId = 1, timeStarted = 2, timeEnded = 3),
),
listOf(
CalculationResult(1, 1),
),
),
// Several.
arrayOf(
0L to listOf(
record.copy(typeId = 1, timeStarted = 0, timeEnded = 1),
record.copy(typeId = 0, timeStarted = 1, timeEnded = 2),
record.copy(typeId = 1, timeStarted = 2, timeEnded = 3),
record.copy(typeId = 2, timeStarted = 3, timeEnded = 4),
record.copy(typeId = 0, timeStarted = 4, timeEnded = 5),
record.copy(typeId = 2, timeStarted = 5, timeEnded = 6),
record.copy(typeId = 0, timeStarted = 6, timeEnded = 7),
record.copy(typeId = 1, timeStarted = 10, timeEnded = 11),
),
listOf(
CalculationResult(1, 2),
CalculationResult(2, 1),
),
),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.example.util.simpletimetracker.core.mapper.TimeMapper
import com.example.util.simpletimetracker.core.repo.ResourceRepo
import com.example.util.simpletimetracker.domain.extension.getTypeIds
import com.example.util.simpletimetracker.domain.extension.orZero
import com.example.util.simpletimetracker.domain.interactor.CalculateAdjacentActivitiesInteractor
import com.example.util.simpletimetracker.domain.interactor.CalculateAdjacentActivitiesInteractor.CalculationResult
import com.example.util.simpletimetracker.domain.interactor.PrefsInteractor
import com.example.util.simpletimetracker.domain.interactor.RecordInteractor
import com.example.util.simpletimetracker.domain.interactor.RecordTypeInteractor
Expand All @@ -26,6 +28,7 @@ class StatisticsDetailAdjacentActivitiesInteractor @Inject constructor(
private val recordInteractor: RecordInteractor,
private val timeMapper: TimeMapper,
private val statisticsDetailViewDataMapper: StatisticsDetailViewDataMapper,
private val calculateAdjacentActivitiesInteractor: CalculateAdjacentActivitiesInteractor,
) {

suspend fun getNextActivitiesViewData(
Expand All @@ -38,8 +41,10 @@ class StatisticsDetailAdjacentActivitiesInteractor @Inject constructor(
val isDarkTheme = prefsInteractor.getDarkMode()
val recordTypes = recordTypeInteractor.getAll().associateBy(RecordType::id)
val actualRecords = getRecords(rangeLength, rangePosition)
val nextActivitiesIds = calculateNextActivities(typeId, actualRecords)
val multitaskingActivitiesIds = calculateMultitasking(typeId, actualRecords)
val nextActivitiesIds = calculateAdjacentActivitiesInteractor
.calculateNextActivities(typeId, actualRecords)
val multitaskingActivitiesIds = calculateAdjacentActivitiesInteractor
.calculateMultitasking(typeId, actualRecords)

fun mapPreviews(typeToCounts: List<CalculationResult>): List<ViewHolderType> {
val total = typeToCounts.sumOf(CalculationResult::count)
Expand Down Expand Up @@ -121,81 +126,7 @@ class StatisticsDetailAdjacentActivitiesInteractor @Inject constructor(
?.firstOrNull()
}

// TODO make more precise calculations?
private fun calculateNextActivities(
typeId: Long,
records: List<RecordBase>,
): List<CalculationResult> {
val counts = mutableMapOf<Long, Long>()

val recordsSorted = records.sortedBy { it.timeStarted }
var currentRecord: RecordBase? = null
recordsSorted.forEach { record ->
val currentTimeEnded = currentRecord?.timeEnded
if (currentTimeEnded != null &&
currentTimeEnded <= record.timeStarted
) {
record.typeIds.firstOrNull()?.let { id ->
counts[id] = counts[id].orZero() + 1
}
currentRecord = null
}
if (currentRecord == null && typeId in record.typeIds) {
currentRecord = record
}
}

return counts.keys
.sortedByDescending { counts[it].orZero() }
.take(MAX_COUNT)
.map { CalculationResult(it, counts[it].orZero()) }
}

// TODO make more precise calculations?
private fun calculateMultitasking(
typeId: Long,
records: List<RecordBase>,
): List<CalculationResult> {
val counts = mutableMapOf<Long, Long>()

val recordsSorted = records.sortedBy { it.timeStarted }
var currentRecord: RecordBase? = null
recordsSorted.forEach { record ->
val currentTimeStarted = currentRecord?.timeStarted
val currentTimeEnded = currentRecord?.timeEnded
if (currentTimeStarted != null &&
currentTimeEnded != null &&
// Find next records that was started after this one but before this one ends.
currentTimeStarted <= record.timeStarted &&
currentTimeEnded > record.timeStarted &&
// Cutoff short intersections.
currentTimeEnded - record.timeStarted > 1_000L
) {
record.typeIds.firstOrNull()?.let { id ->
counts[id] = counts[id].orZero() + 1
}
}
if (typeId in record.typeIds) {
currentRecord = record
}
}

return counts.keys
.sortedByDescending { counts[it].orZero() }
.take(MAX_COUNT)
.map { CalculationResult(it, counts[it].orZero()) }
}

private fun getEmptyViewData(): List<ViewHolderType> {
return emptyList()
}

private data class CalculationResult(
val typeId: Long,
val count: Long,
)

companion object {
private const val MAX_COUNT = 5
}
}

0 comments on commit 51c2375

Please sign in to comment.