diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/CalculateAdjacentActivitiesInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/CalculateAdjacentActivitiesInteractor.kt new file mode 100644 index 000000000..8127fa614 --- /dev/null +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/CalculateAdjacentActivitiesInteractor.kt @@ -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, + ): List { + val counts = mutableMapOf() + + 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, + ): List { + val counts = mutableMapOf() + + 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 + } +} \ No newline at end of file diff --git a/domain/src/test/java/com/example/util/simpletimetracker/domain/mapper/CalculateAdjacentActivitiesInteractorTest.kt b/domain/src/test/java/com/example/util/simpletimetracker/domain/mapper/CalculateAdjacentActivitiesInteractorTest.kt new file mode 100644 index 000000000..86c012d16 --- /dev/null +++ b/domain/src/test/java/com/example/util/simpletimetracker/domain/mapper/CalculateAdjacentActivitiesInteractorTest.kt @@ -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>, + private val output: List, +) { + + 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(), + emptyList(), + ), + arrayOf( + 0L to listOf(record), + emptyList(), + ), + arrayOf( + 0L to listOf( + record.copy(typeId = 1), + record.copy(typeId = 2), + ), + emptyList(), + ), + // 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(), + ), + // 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(), + ), + // 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), + ), + ), + ) + } +} \ No newline at end of file diff --git a/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailAdjacentActivitiesInteractor.kt b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailAdjacentActivitiesInteractor.kt index dbb7d7ea4..df4cfc4d9 100644 --- a/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailAdjacentActivitiesInteractor.kt +++ b/features/feature_statistics_detail/src/main/java/com/example/util/simpletimetracker/feature_statistics_detail/interactor/StatisticsDetailAdjacentActivitiesInteractor.kt @@ -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 @@ -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( @@ -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): List { val total = typeToCounts.sumOf(CalculationResult::count) @@ -121,81 +126,7 @@ class StatisticsDetailAdjacentActivitiesInteractor @Inject constructor( ?.firstOrNull() } - // TODO make more precise calculations? - private fun calculateNextActivities( - typeId: Long, - records: List, - ): List { - val counts = mutableMapOf() - - 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, - ): List { - val counts = mutableMapOf() - - 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 { return emptyList() } - - private data class CalculationResult( - val typeId: Long, - val count: Long, - ) - - companion object { - private const val MAX_COUNT = 5 - } } \ No newline at end of file