diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 4126794d..7128b437 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -2,7 +2,19 @@ + + + + + + + + + + + + diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 00000000..a3aebdb1 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 1d01d2c1..46bd927d 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -16,10 +16,8 @@ - + diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 00000000..f8051a6f --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 00000000..f3d4a2e5 --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b137891..3b113ca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,6 @@ +## 2024-01-07 +- Updated the core functionality of the app to compute habit statuses on the go instead of caching them in the database. This change improves the robustness and clarity of the code. +- Refactored 'Routine' to 'Habit' across the application for better semantics. +- Improved performance by computing each date in a separate coroutine and introduced pagination for the RoutineCalendarScreen. +- Fixed various minor bugs and performance issues. \ No newline at end of file diff --git a/app/src/main/java/com/rendox/routinetracker/app/RoutineTrackerApp.kt b/app/src/main/java/com/rendox/routinetracker/app/RoutineTrackerApp.kt index 13946990..24d86245 100644 --- a/app/src/main/java/com/rendox/routinetracker/app/RoutineTrackerApp.kt +++ b/app/src/main/java/com/rendox/routinetracker/app/RoutineTrackerApp.kt @@ -4,11 +4,10 @@ import android.app.Application import com.rendox.routinetracker.core.data.di.completionHistoryDataModule import com.rendox.routinetracker.core.data.di.completionTimeDataModule import com.rendox.routinetracker.core.data.di.routineDataModule -import com.rendox.routinetracker.core.data.di.streakDataModule +import com.rendox.routinetracker.core.data.di.vacationDataModule import com.rendox.routinetracker.core.database.di.localDataSourceModule import com.rendox.routinetracker.core.domain.di.completionHistoryDomainModule -import com.rendox.routinetracker.core.domain.di.routineDomainModule -import com.rendox.routinetracker.core.domain.di.streakDomainModule +import com.rendox.routinetracker.core.domain.di.habitDomainModule import com.rendox.routinetracker.feature.agenda.di.agendaScreenModule import com.rendox.routinetracker.routine_details.di.routineDetailsModule import org.koin.android.ext.koin.androidContext @@ -23,11 +22,10 @@ class RoutineTrackerApp: Application() { localDataSourceModule, routineDataModule, completionHistoryDataModule, - streakDataModule, completionTimeDataModule, - routineDomainModule, + vacationDataModule, + habitDomainModule, completionHistoryDomainModule, - streakDomainModule, agendaScreenModule, routineDetailsModule, ) diff --git a/build-logic/convention/build/kotlin/compileKotlin/cacheable/last-build.bin b/build-logic/convention/build/kotlin/compileKotlin/cacheable/last-build.bin index 1a406be2..6f844a75 100644 Binary files a/build-logic/convention/build/kotlin/compileKotlin/cacheable/last-build.bin and b/build-logic/convention/build/kotlin/compileKotlin/cacheable/last-build.bin differ diff --git a/build-logic/convention/build/libs/convention.jar b/build-logic/convention/build/libs/convention.jar index 12b27fef..e668423f 100644 Binary files a/build-logic/convention/build/libs/convention.jar and b/build-logic/convention/build/libs/convention.jar differ diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/completion_history/CompletionHistoryRepository.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/completion_history/CompletionHistoryRepository.kt index 0a9e6331..0de01b4a 100644 --- a/core/data/src/main/java/com/rendox/routinetracker/core/data/completion_history/CompletionHistoryRepository.kt +++ b/core/data/src/main/java/com/rendox/routinetracker/core/data/completion_history/CompletionHistoryRepository.kt @@ -1,66 +1,41 @@ package com.rendox.routinetracker.core.data.completion_history -import com.rendox.routinetracker.core.logic.time.LocalDateRange -import com.rendox.routinetracker.core.model.CompletionHistoryEntry -import com.rendox.routinetracker.core.model.HistoricalStatus +import com.rendox.routinetracker.core.model.Habit import kotlinx.datetime.LocalDate interface CompletionHistoryRepository { + suspend fun getNumOfTimesCompletedInPeriod( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate?, + ): Double - suspend fun getHistoryEntries( - routineId: Long, - dates: LocalDateRange, - ): List - - suspend fun getHistoryEntryByDate( - routineId: Long, - date: LocalDate, - ): CompletionHistoryEntry? - - suspend fun insertHistoryEntry( - id: Long? = null, - routineId: Long, - entry: CompletionHistoryEntry, - ) - - suspend fun deleteHistoryEntry( - routineId: Long, - date: LocalDate, - ) - - suspend fun updateHistoryEntryByDate( - routineId: Long, - date: LocalDate, - newStatus: HistoricalStatus? = null, - newScheduleDeviation: Float? = null, - newTimesCompleted: Float? = null, - ) - - suspend fun getFirstHistoryEntry(routineId: Long): CompletionHistoryEntry? - suspend fun getLastHistoryEntry(routineId: Long): CompletionHistoryEntry? - - suspend fun checkIfStatusWasCompletedLater(routineId: Long, date: LocalDate): Boolean - suspend fun deleteCompletedLaterBackupEntry(routineId: Long, date: LocalDate) - - suspend fun getFirstHistoryEntryByStatus( - routineId: Long, - matchingStatuses: List, + suspend fun getRecordByDate(habitId: Long, date: LocalDate): Habit.CompletionRecord? + suspend fun getLastCompletedRecord( + habitId: Long, minDate: LocalDate? = null, maxDate: LocalDate? = null, - ): CompletionHistoryEntry? + ): Habit.CompletionRecord? - suspend fun getLastHistoryEntryByStatus( - routineId: Long, - matchingStatuses: List, + suspend fun getFirstCompletedRecord( + habitId: Long, minDate: LocalDate? = null, maxDate: LocalDate? = null, - ): CompletionHistoryEntry? + ): Habit.CompletionRecord? - suspend fun getTotalTimesCompletedInPeriod( - routineId: Long, startDate: LocalDate, endDate: LocalDate - ): Double + suspend fun getRecordsInPeriod( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate?, + ): List - suspend fun getScheduleDeviationInPeriod( - routineId: Long, startDate: LocalDate, endDate: LocalDate - ): Double + suspend fun insertCompletion( + habitId: Long, + completionRecord: Habit.CompletionRecord, + ) + + suspend fun deleteCompletionByDate( + habitId: Long, + date: LocalDate, + ) } \ No newline at end of file diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/completion_history/CompletionHistoryRepositoryImpl.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/completion_history/CompletionHistoryRepositoryImpl.kt index b3064203..bc6d39dd 100644 --- a/core/data/src/main/java/com/rendox/routinetracker/core/data/completion_history/CompletionHistoryRepositoryImpl.kt +++ b/core/data/src/main/java/com/rendox/routinetracker/core/data/completion_history/CompletionHistoryRepositoryImpl.kt @@ -1,116 +1,59 @@ package com.rendox.routinetracker.core.data.completion_history import com.rendox.routinetracker.core.database.completion_history.CompletionHistoryLocalDataSource -import com.rendox.routinetracker.core.logic.time.LocalDateRange -import com.rendox.routinetracker.core.model.CompletionHistoryEntry -import com.rendox.routinetracker.core.model.HistoricalStatus +import com.rendox.routinetracker.core.model.Habit import kotlinx.datetime.LocalDate class CompletionHistoryRepositoryImpl( - private val localDataSource: CompletionHistoryLocalDataSource, -) : CompletionHistoryRepository { - - override suspend fun getHistoryEntries( - routineId: Long, - dates: LocalDateRange - ): List { - return localDataSource.getHistoryEntries(routineId, dates) - } - - override suspend fun getHistoryEntryByDate( - routineId: Long, - date: LocalDate - ): CompletionHistoryEntry? { - return localDataSource.getHistoryEntryByDate(routineId, date) - } - - override suspend fun insertHistoryEntry( - id: Long?, - routineId: Long, - entry: CompletionHistoryEntry, - ) { - localDataSource.insertHistoryEntry( - id = id, - routineId = routineId, - entry = entry, + private val localDataSource: CompletionHistoryLocalDataSource +): CompletionHistoryRepository { + override suspend fun getNumOfTimesCompletedInPeriod( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate? + ): Double { + return localDataSource.getNumOfTimesCompletedInPeriod( + habitId, minDate, maxDate ) } - override suspend fun deleteHistoryEntry(routineId: Long, date: LocalDate) { - localDataSource.deleteHistoryEntry(routineId, date) - } - - override suspend fun updateHistoryEntryByDate( - routineId: Long, - date: LocalDate, - newStatus: HistoricalStatus?, - newScheduleDeviation: Float?, - newTimesCompleted: Float?, - ) { - localDataSource.updateHistoryEntryByDate( - routineId = routineId, - date = date, - newStatus = newStatus, - newScheduleDeviation = newScheduleDeviation, - newTimesCompleted = newTimesCompleted, + override suspend fun getRecordByDate(habitId: Long, date: LocalDate): Habit.CompletionRecord? { + return localDataSource.getRecordByDate( + habitId, date ) } - override suspend fun getFirstHistoryEntry(routineId: Long): CompletionHistoryEntry? { - return localDataSource.getFirstHistoryEntry(routineId) - } - - override suspend fun getLastHistoryEntry(routineId: Long): CompletionHistoryEntry? { - return localDataSource.getLastHistoryEntry(routineId) - } - - override suspend fun checkIfStatusWasCompletedLater(routineId: Long, date: LocalDate): Boolean { - return localDataSource.checkIfStatusWasCompletedLater(routineId, date) - } - - override suspend fun deleteCompletedLaterBackupEntry(routineId: Long, date: LocalDate) { - localDataSource.deleteCompletedLaterBackupEntry(routineId, date) + override suspend fun getLastCompletedRecord( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate?, + ): Habit.CompletionRecord? { + return localDataSource.getLastCompletedRecord(habitId, minDate, maxDate) } - override suspend fun getFirstHistoryEntryByStatus( - routineId: Long, - matchingStatuses: List, + override suspend fun getFirstCompletedRecord( + habitId: Long, minDate: LocalDate?, maxDate: LocalDate?, - ): CompletionHistoryEntry? { - return localDataSource.getFirstHistoryEntryByStatus( - routineId = routineId, - matchingStatuses = matchingStatuses, - minDate = minDate, - maxDate = maxDate, - ) + ): Habit.CompletionRecord? { + return localDataSource.getFirstCompletedRecord(habitId, minDate, maxDate) } - override suspend fun getLastHistoryEntryByStatus( - routineId: Long, - matchingStatuses: List, + override suspend fun getRecordsInPeriod( + habitId: Long, minDate: LocalDate?, maxDate: LocalDate?, - ): CompletionHistoryEntry? = localDataSource.getLastHistoryEntryByStatus( - routineId = routineId, - matchingStatuses = matchingStatuses, - minDate = minDate, - maxDate = maxDate, - ) - - override suspend fun getTotalTimesCompletedInPeriod( - routineId: Long, startDate: LocalDate, endDate: LocalDate - ): Double { - return localDataSource.getTotalTimesCompletedInPeriod( - routineId, startDate, endDate + ): List { + return localDataSource.getRecordsInPeriod( + habitId, minDate, maxDate ) } - override suspend fun getScheduleDeviationInPeriod( - routineId: Long, startDate: LocalDate, endDate: LocalDate - ): Double { - return localDataSource.getScheduleDeviationInPeriod( - routineId, startDate, endDate - ) + override suspend fun insertCompletion(habitId: Long, completionRecord: Habit.CompletionRecord) { + localDataSource.insertCompletion(habitId, completionRecord) + } + + override suspend fun deleteCompletionByDate(habitId: Long, date: LocalDate) { + localDataSource.deleteCompletionByDate(habitId, date) } } \ No newline at end of file diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/di/CompletionHistoryDataModule.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/di/CompletionHistoryDataModule.kt index 6d3ac020..672f39aa 100644 --- a/core/data/src/main/java/com/rendox/routinetracker/core/data/di/CompletionHistoryDataModule.kt +++ b/core/data/src/main/java/com/rendox/routinetracker/core/data/di/CompletionHistoryDataModule.kt @@ -7,9 +7,8 @@ import com.rendox.routinetracker.core.database.completion_history.CompletionHist import org.koin.dsl.module val completionHistoryDataModule = module { - single { - CompletionHistoryLocalDataSourceImpl(db = get(), dispatcher = get()) + CompletionHistoryLocalDataSourceImpl(db = get()) } single { diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/di/CompletionTimeDataModule.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/di/CompletionTimeDataModule.kt index 36c98fa2..fa18007d 100644 --- a/core/data/src/main/java/com/rendox/routinetracker/core/data/di/CompletionTimeDataModule.kt +++ b/core/data/src/main/java/com/rendox/routinetracker/core/data/di/CompletionTimeDataModule.kt @@ -9,7 +9,7 @@ import org.koin.dsl.module val completionTimeDataModule = module { single { - CompletionTimeLocalDataSourceImpl(db = get(), dispatcher = get()) + CompletionTimeLocalDataSourceImpl(db = get()) } single { diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/di/RoutineDataModule.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/di/RoutineDataModule.kt index d7b94954..233b233f 100644 --- a/core/data/src/main/java/com/rendox/routinetracker/core/data/di/RoutineDataModule.kt +++ b/core/data/src/main/java/com/rendox/routinetracker/core/data/di/RoutineDataModule.kt @@ -1,18 +1,18 @@ package com.rendox.routinetracker.core.data.di -import com.rendox.routinetracker.core.data.routine.RoutineRepository -import com.rendox.routinetracker.core.data.routine.RoutineRepositoryImpl -import com.rendox.routinetracker.core.database.routine.RoutineLocalDataSource -import com.rendox.routinetracker.core.database.routine.RoutineLocalDataSourceImpl +import com.rendox.routinetracker.core.data.habit.HabitRepository +import com.rendox.routinetracker.core.data.habit.HabitRepositoryImpl +import com.rendox.routinetracker.core.database.habit.HabitLocalDataSource +import com.rendox.routinetracker.core.database.habit.HabitLocalDataSourceImpl import org.koin.dsl.module val routineDataModule = module { - single { - RoutineLocalDataSourceImpl(db = get(), dispatcher = get()) + single { + HabitLocalDataSourceImpl(db = get()) } - single { - RoutineRepositoryImpl(localDataSource = get()) + single { + HabitRepositoryImpl(localDataSource = get()) } } \ No newline at end of file diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/di/StreakDataModule.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/di/StreakDataModule.kt deleted file mode 100644 index 77f9b3bc..00000000 --- a/core/data/src/main/java/com/rendox/routinetracker/core/data/di/StreakDataModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.rendox.routinetracker.core.data.di - -import com.rendox.routinetracker.core.data.streak.StreakRepository -import com.rendox.routinetracker.core.data.streak.StreakRepositoryImpl -//import com.rendox.routinetracker.core.data.streak.StreakRepositoryImpl -import com.rendox.routinetracker.core.database.streak.StreakLocalDataSource -import com.rendox.routinetracker.core.database.streak.StreakLocalDataSourceImpl -import org.koin.dsl.module - -val streakDataModule = module { - - single { - StreakLocalDataSourceImpl(db = get(), dispatcher = get()) - } - - single { - StreakRepositoryImpl(localDataSource = get()) - } -} \ No newline at end of file diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/di/VacationDataModule.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/di/VacationDataModule.kt new file mode 100644 index 00000000..b4374281 --- /dev/null +++ b/core/data/src/main/java/com/rendox/routinetracker/core/data/di/VacationDataModule.kt @@ -0,0 +1,18 @@ +package com.rendox.routinetracker.core.data.di + +import com.rendox.routinetracker.core.database.vacation.VacationLocalDataSourceImpl +import com.rendox.routinetracker.core.data.vacation.VacationRepository +import com.rendox.routinetracker.core.data.vacation.VacationRepositoryImpl +import com.rendox.routinetracker.core.database.vacation.VacationLocalDataSource +import org.koin.dsl.module + +val vacationDataModule = module { + single { + VacationLocalDataSourceImpl(db = get()) + } + single { + VacationRepositoryImpl( + localDataSource = get(), + ) + } +} \ No newline at end of file diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/habit/HabitRepository.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/habit/HabitRepository.kt new file mode 100644 index 00000000..f02a5d58 --- /dev/null +++ b/core/data/src/main/java/com/rendox/routinetracker/core/data/habit/HabitRepository.kt @@ -0,0 +1,21 @@ +package com.rendox.routinetracker.core.data.habit + +import com.rendox.routinetracker.core.model.Habit +import kotlinx.datetime.LocalTime + +interface HabitRepository { + + suspend fun getHabitById(id: Long): Habit + + suspend fun insertHabit(habit: Habit) + + suspend fun getAllHabits(): List + + suspend fun updateDueDateSpecificCompletionTime( + time: LocalTime, routineId: Long, dueDateNumber: Int + ) + + suspend fun getDueDateSpecificCompletionTime( + routineId: Long, dueDateNumber: Int + ): LocalTime? +} \ No newline at end of file diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/habit/HabitRepositoryImpl.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/habit/HabitRepositoryImpl.kt new file mode 100644 index 00000000..92fbc59e --- /dev/null +++ b/core/data/src/main/java/com/rendox/routinetracker/core/data/habit/HabitRepositoryImpl.kt @@ -0,0 +1,38 @@ +package com.rendox.routinetracker.core.data.habit + +import com.rendox.routinetracker.core.database.habit.HabitLocalDataSource +import com.rendox.routinetracker.core.model.Habit +import kotlinx.datetime.LocalTime + +class HabitRepositoryImpl( + private val localDataSource: HabitLocalDataSource, +) : HabitRepository { + + override suspend fun getHabitById(id: Long): Habit { + return localDataSource.getHabitById(habitId = id) + } + + override suspend fun insertHabit(habit: Habit) { + localDataSource.insertHabit(habit) + } + + override suspend fun getAllHabits(): List { + return localDataSource.getAllHabits() + } + + override suspend fun updateDueDateSpecificCompletionTime( + time: LocalTime, routineId: Long, dueDateNumber: Int + ) { + localDataSource.updateDueDateSpecificCompletionTime( + time, routineId, dueDateNumber + ) + } + + override suspend fun getDueDateSpecificCompletionTime( + routineId: Long, dueDateNumber: Int + ): LocalTime? { + return localDataSource.getDueDateSpecificCompletionTime( + routineId, dueDateNumber + ) + } +} \ No newline at end of file diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/routine/RoutineRepository.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/routine/RoutineRepository.kt deleted file mode 100644 index 1a8221f3..00000000 --- a/core/data/src/main/java/com/rendox/routinetracker/core/data/routine/RoutineRepository.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.rendox.routinetracker.core.data.routine - -import com.rendox.routinetracker.core.model.Routine -import kotlinx.datetime.LocalTime - -interface RoutineRepository { - - suspend fun getRoutineById(id: Long): Routine - - suspend fun insertRoutine(routine: Routine) - - suspend fun getAllRoutines(): List - - suspend fun updateDueDateSpecificCompletionTime( - time: LocalTime, routineId: Long, dueDateNumber: Int - ) - - suspend fun getDueDateSpecificCompletionTime( - routineId: Long, dueDateNumber: Int - ): LocalTime? -} \ No newline at end of file diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/routine/RoutineRepositoryImpl.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/routine/RoutineRepositoryImpl.kt deleted file mode 100644 index 8871ead3..00000000 --- a/core/data/src/main/java/com/rendox/routinetracker/core/data/routine/RoutineRepositoryImpl.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.rendox.routinetracker.core.data.routine - -import com.rendox.routinetracker.core.database.routine.RoutineLocalDataSource -import com.rendox.routinetracker.core.model.Routine -import kotlinx.datetime.LocalTime - -class RoutineRepositoryImpl( - private val localDataSource: RoutineLocalDataSource, -) : RoutineRepository { - - override suspend fun getRoutineById(id: Long): Routine { - return localDataSource.getRoutineById(routineId = id) - } - - override suspend fun insertRoutine(routine: Routine) { - localDataSource.insertRoutine(routine) - } - - override suspend fun getAllRoutines(): List { - return localDataSource.getAllRoutines() - } - - override suspend fun updateDueDateSpecificCompletionTime( - time: LocalTime, routineId: Long, dueDateNumber: Int - ) { - localDataSource.updateDueDateSpecificCompletionTime( - time, routineId, dueDateNumber - ) - } - - override suspend fun getDueDateSpecificCompletionTime( - routineId: Long, dueDateNumber: Int - ): LocalTime? { - return localDataSource.getDueDateSpecificCompletionTime( - routineId, dueDateNumber - ) - } -} \ No newline at end of file diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/streak/StreakRepository.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/streak/StreakRepository.kt deleted file mode 100644 index ec02b80b..00000000 --- a/core/data/src/main/java/com/rendox/routinetracker/core/data/streak/StreakRepository.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.rendox.routinetracker.core.data.streak - -import com.rendox.routinetracker.core.model.Streak -import kotlinx.datetime.LocalDate - -interface StreakRepository { - suspend fun insertStreak(streak: Streak, routineId: Long) - suspend fun getStreakByDate(routineId: Long, dateWithinStreak: LocalDate): Streak? - suspend fun getAllStreaks( - routineId: Long, - afterDateInclusive: LocalDate? = null, - beforeDateInclusive: LocalDate? = null, - ): List - suspend fun getLastStreak(routineId: Long): Streak? - suspend fun deleteStreakById(id: Long) - suspend fun updateStreakById(id: Long, start: LocalDate, end: LocalDate?) -} \ No newline at end of file diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/streak/StreakRepositoryImpl.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/streak/StreakRepositoryImpl.kt deleted file mode 100644 index 78365ebf..00000000 --- a/core/data/src/main/java/com/rendox/routinetracker/core/data/streak/StreakRepositoryImpl.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.rendox.routinetracker.core.data.streak - -import com.rendox.routinetracker.core.database.streak.StreakLocalDataSource -import com.rendox.routinetracker.core.model.Streak -import kotlinx.datetime.LocalDate - -class StreakRepositoryImpl( - private val localDataSource: StreakLocalDataSource -) : StreakRepository { - override suspend fun insertStreak(streak: Streak, routineId: Long) { - return localDataSource.insertStreak(streak, routineId) - } - - override suspend fun getStreakByDate(routineId: Long, dateWithinStreak: LocalDate): Streak? { - return try { - localDataSource.getStreakByDate(routineId, dateWithinStreak) - } catch (e: Exception) { - e.printStackTrace() - null - } - } - - override suspend fun getAllStreaks( - routineId: Long, - afterDateInclusive: LocalDate?, - beforeDateInclusive: LocalDate?, - ): List { - return localDataSource.getAllStreaks( - routineId = routineId, - afterDateInclusive = afterDateInclusive, - beforeDateInclusive = beforeDateInclusive - ) - } - - override suspend fun getLastStreak(routineId: Long): Streak? { - return localDataSource.getLastStreak(routineId) - } - - override suspend fun deleteStreakById(id: Long) { - localDataSource.deleteStreakById(id) - } - - override suspend fun updateStreakById(id: Long, start: LocalDate, end: LocalDate?) { - localDataSource.updateStreakById(id, start, end) - } -} \ No newline at end of file diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/vacation/VacationRepository.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/vacation/VacationRepository.kt new file mode 100644 index 00000000..126b1638 --- /dev/null +++ b/core/data/src/main/java/com/rendox/routinetracker/core/data/vacation/VacationRepository.kt @@ -0,0 +1,28 @@ +package com.rendox.routinetracker.core.data.vacation + +import com.rendox.routinetracker.core.model.Vacation +import kotlinx.datetime.LocalDate + +interface VacationRepository { + + /** Returns a [Vacation] that includes [date]. */ + suspend fun getVacationByDate( + habitId: Long, date: LocalDate + ): Vacation? + + /** Returns a [Vacation] that was ended before the [currentDate]. */ + suspend fun getPreviousVacation( + habitId: Long, + currentDate: LocalDate, + ): Vacation? + + suspend fun getVacationsInPeriod( + habitId: Long, + minDate: LocalDate? = null, + maxDate: LocalDate? = null, + ): List + + suspend fun getLastVacation(habitId: Long): Vacation? + suspend fun insertVacation(habitId: Long, vacation: Vacation) + suspend fun deleteVacationById(id: Long) +} \ No newline at end of file diff --git a/core/data/src/main/java/com/rendox/routinetracker/core/data/vacation/VacationRepositoryImpl.kt b/core/data/src/main/java/com/rendox/routinetracker/core/data/vacation/VacationRepositoryImpl.kt new file mode 100644 index 00000000..14e765de --- /dev/null +++ b/core/data/src/main/java/com/rendox/routinetracker/core/data/vacation/VacationRepositoryImpl.kt @@ -0,0 +1,37 @@ +package com.rendox.routinetracker.core.data.vacation + +import com.rendox.routinetracker.core.database.vacation.VacationLocalDataSource +import com.rendox.routinetracker.core.model.Vacation +import kotlinx.datetime.LocalDate + +class VacationRepositoryImpl( + private val localDataSource: VacationLocalDataSource +): VacationRepository { + override suspend fun getVacationByDate(habitId: Long, date: LocalDate): Vacation? { + return localDataSource.getVacationByDate(habitId, date) + } + + override suspend fun getPreviousVacation(habitId: Long, currentDate: LocalDate): Vacation? { + return localDataSource.getPreviousVacation(habitId, currentDate) + } + + override suspend fun getLastVacation(habitId: Long): Vacation? { + return localDataSource.getLastVacation(habitId) + } + + override suspend fun getVacationsInPeriod( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate?, + ): List { + return localDataSource.getVacationsInPeriod(habitId, minDate, maxDate) + } + + override suspend fun insertVacation(habitId: Long, vacation: Vacation) { + return localDataSource.insertVacation(habitId, vacation) + } + + override suspend fun deleteVacationById(id: Long) { + localDataSource.deleteVacationById(id) + } +} \ No newline at end of file diff --git a/core/database/src/main/java/com/rendox/routinetracker/core/database/completion_history/CompletionHistoryLocalDataSource.kt b/core/database/src/main/java/com/rendox/routinetracker/core/database/completion_history/CompletionHistoryLocalDataSource.kt index c894d754..2484f021 100644 --- a/core/database/src/main/java/com/rendox/routinetracker/core/database/completion_history/CompletionHistoryLocalDataSource.kt +++ b/core/database/src/main/java/com/rendox/routinetracker/core/database/completion_history/CompletionHistoryLocalDataSource.kt @@ -1,63 +1,41 @@ package com.rendox.routinetracker.core.database.completion_history -import com.rendox.routinetracker.core.logic.time.LocalDateRange -import com.rendox.routinetracker.core.model.CompletionHistoryEntry -import com.rendox.routinetracker.core.model.HistoricalStatus +import com.rendox.routinetracker.core.model.Habit import kotlinx.datetime.LocalDate interface CompletionHistoryLocalDataSource { + suspend fun getNumOfTimesCompletedInPeriod( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate?, + ): Double - suspend fun getHistoryEntries( - routineId: Long, - dates: LocalDateRange, - ): List - - suspend fun getHistoryEntryByDate( - routineId: Long, - date: LocalDate - ): CompletionHistoryEntry? - - suspend fun insertHistoryEntry( - id: Long? = null, - routineId: Long, - entry: CompletionHistoryEntry, + suspend fun getRecordByDate(habitId: Long, date: LocalDate): Habit.CompletionRecord? + suspend fun getLastCompletedRecord( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate?, + ): Habit.CompletionRecord? + + suspend fun getFirstCompletedRecord( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate?, + ): Habit.CompletionRecord? + + suspend fun getRecordsInPeriod( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate?, + ): List + + suspend fun insertCompletion( + habitId: Long, + completionRecord: Habit.CompletionRecord, ) - suspend fun deleteHistoryEntry(routineId: Long, date: LocalDate) - - suspend fun updateHistoryEntryByDate( - routineId: Long, + suspend fun deleteCompletionByDate( + habitId: Long, date: LocalDate, - newStatus: HistoricalStatus?, - newScheduleDeviation: Float?, - newTimesCompleted: Float?, ) - - suspend fun getFirstHistoryEntry(routineId: Long): CompletionHistoryEntry? - suspend fun getLastHistoryEntry(routineId: Long): CompletionHistoryEntry? - - suspend fun getFirstHistoryEntryByStatus( - routineId: Long, - matchingStatuses: List, - minDate: LocalDate? = null, - maxDate: LocalDate? = null, - ): CompletionHistoryEntry? - - suspend fun getLastHistoryEntryByStatus( - routineId: Long, - matchingStatuses: List, - minDate: LocalDate? = null, - maxDate: LocalDate? = null, - ): CompletionHistoryEntry? - - suspend fun checkIfStatusWasCompletedLater(routineId: Long, date: LocalDate): Boolean - suspend fun deleteCompletedLaterBackupEntry(routineId: Long, date: LocalDate) - - suspend fun getTotalTimesCompletedInPeriod( - routineId: Long, startDate: LocalDate, endDate: LocalDate - ): Double - - suspend fun getScheduleDeviationInPeriod( - routineId: Long, startDate: LocalDate, endDate: LocalDate - ): Double } \ No newline at end of file diff --git a/core/database/src/main/java/com/rendox/routinetracker/core/database/completion_history/CompletionHistoryLocalDataSourceImpl.kt b/core/database/src/main/java/com/rendox/routinetracker/core/database/completion_history/CompletionHistoryLocalDataSourceImpl.kt index 791aa82d..de1f8d78 100644 --- a/core/database/src/main/java/com/rendox/routinetracker/core/database/completion_history/CompletionHistoryLocalDataSourceImpl.kt +++ b/core/database/src/main/java/com/rendox/routinetracker/core/database/completion_history/CompletionHistoryLocalDataSourceImpl.kt @@ -1,204 +1,104 @@ package com.rendox.routinetracker.core.database.completion_history +import com.rendox.routinetracker.core.database.CompletionHistoryEntity import com.rendox.routinetracker.core.database.RoutineTrackerDatabase -import com.rendox.routinetracker.core.database.completionhistory.CompletedLaterHistoryEntity -import com.rendox.routinetracker.core.database.completionhistory.CompletionHistoryEntity -import com.rendox.routinetracker.core.logic.time.LocalDateRange -import com.rendox.routinetracker.core.model.CompletionHistoryEntry -import com.rendox.routinetracker.core.model.HistoricalStatus +import com.rendox.routinetracker.core.database.habit.model.HabitType +import com.rendox.routinetracker.core.model.Habit import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.datetime.LocalDate class CompletionHistoryLocalDataSourceImpl( private val db: RoutineTrackerDatabase, - private val dispatcher: CoroutineDispatcher, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, ) : CompletionHistoryLocalDataSource { - - override suspend fun getHistoryEntries( - routineId: Long, - dates: LocalDateRange, - ): List { + override suspend fun getNumOfTimesCompletedInPeriod( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate?, + ): Double { return withContext(dispatcher) { - db.completionHistoryEntityQueries.getHistoryEntriesByIndices( - routineId = routineId, - start = dates.start, - end = dates.endInclusive, - ).executeAsList().map { it.toExternalModel() } + db.completionHistoryEntityQueries.getNumOfTimesCompletedInPeriod( + habitId, minDate, maxDate + ).executeAsOne() } } - override suspend fun getHistoryEntryByDate( - routineId: Long, - date: LocalDate - ): CompletionHistoryEntry? { + override suspend fun getRecordByDate( + habitId: Long, date: LocalDate + ): Habit.CompletionRecord? { return withContext(dispatcher) { - db.completionHistoryEntityQueries.getHistoryEntryByDate( - routineId = routineId, - date = date, + db.completionHistoryEntityQueries.getRecordByDate( + habitId, date ).executeAsOneOrNull()?.toExternalModel() } } - private fun CompletionHistoryEntity.toExternalModel() = CompletionHistoryEntry( - date = date, - status = status, - scheduleDeviation = scheduleDeviation, - timesCompleted = timesCompleted, - ) - - override suspend fun insertHistoryEntry( - id: Long?, - routineId: Long, - entry: CompletionHistoryEntry, - ) { - withContext(dispatcher) { - db.routineEntityQueries.transaction { - db.completionHistoryEntityQueries.insertHistoryEntry( - id = id, - routineId = routineId, - date = entry.date, - status = entry.status, - scheduleDeviation = entry.scheduleDeviation, - timesCompleted = entry.timesCompleted, - ) - if (entry.status == HistoricalStatus.CompletedLater) { - insertCompletedLaterDate(routineId, entry.date) - } - } - } - } - - override suspend fun deleteHistoryEntry(routineId: Long, date: LocalDate) { - return withContext(dispatcher) { - db.completionHistoryEntityQueries.deleteHistoryEntry(routineId, date) - } - } - - override suspend fun updateHistoryEntryByDate( - routineId: Long, - date: LocalDate, - newStatus: HistoricalStatus?, - newScheduleDeviation: Float?, - newTimesCompleted: Float?, - ) { - withContext(dispatcher) { - db.routineEntityQueries.transaction { - val oldValue = db.completionHistoryEntityQueries.getHistoryEntriesByIndices( - routineId, date, date - ).executeAsOne() - db.completionHistoryEntityQueries.updateHistoryEntryStatusByDate( - status = newStatus ?: oldValue.status, - routineId = routineId, - date = date, - scheduleDeviation = newScheduleDeviation ?: oldValue.scheduleDeviation, - timesCompleted = newTimesCompleted ?: oldValue.timesCompleted, - ) - if (newStatus == HistoricalStatus.CompletedLater) { - insertCompletedLaterDate(routineId, date) - } - } - } - } - - override suspend fun getFirstHistoryEntry(routineId: Long): CompletionHistoryEntry? { - return withContext(dispatcher) { - db.completionHistoryEntityQueries.getFirstHistoryEntry(routineId) - .executeAsOneOrNull()?.toExternalModel() - } - } - - override suspend fun getLastHistoryEntry(routineId: Long): CompletionHistoryEntry? { + override suspend fun getLastCompletedRecord( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate? + ): Habit.CompletionRecord? { return withContext(dispatcher) { - db.completionHistoryEntityQueries.getLastHistoryEntry(routineId) + db.completionHistoryEntityQueries.getLastRecord(habitId, minDate, maxDate) .executeAsOneOrNull()?.toExternalModel() } } - override suspend fun getFirstHistoryEntryByStatus( - routineId: Long, - matchingStatuses: List, + override suspend fun getFirstCompletedRecord( + habitId: Long, minDate: LocalDate?, maxDate: LocalDate?, - ): CompletionHistoryEntry? { + ): Habit.CompletionRecord? { return withContext(dispatcher) { - db.completionHistoryEntityQueries.getFirstHistoryEntryByStatus( - routineId = routineId, - matchingStatuses = matchingStatuses, - minDate = minDate, - maxDate = maxDate, - ).executeAsOneOrNull()?.toExternalModel() + db.completionHistoryEntityQueries.getFirstRecord(habitId, minDate, maxDate) + .executeAsOneOrNull()?.toExternalModel() } } - override suspend fun getLastHistoryEntryByStatus( - routineId: Long, - matchingStatuses: List, + override suspend fun getRecordsInPeriod( + habitId: Long, minDate: LocalDate?, maxDate: LocalDate?, - ): CompletionHistoryEntry? { + ): List { return withContext(dispatcher) { - db.completionHistoryEntityQueries.getLastHistoryEntryByStatus( - routineId = routineId, - matchingStatuses = matchingStatuses, - minDate = minDate, - maxDate = maxDate, - ).executeAsOneOrNull()?.toExternalModel() + db.completionHistoryEntityQueries.getRecordsInPeriod( + habitId, minDate, maxDate + ).executeAsList().map { it.toExternalModel()!! } } } - override suspend fun checkIfStatusWasCompletedLater(routineId: Long, date: LocalDate): Boolean { - return withContext(dispatcher) { - checkCompletedLater(routineId, date) - } - } - - override suspend fun deleteCompletedLaterBackupEntry( - routineId: Long, date: LocalDate + override suspend fun insertCompletion( + habitId: Long, + completionRecord: Habit.CompletionRecord, ) { withContext(dispatcher) { - db.completedLaterHistoryEntityQueries.deleteCompletedLaterDate( - routineId, date + db.completionHistoryEntityQueries.insertCompletion( + habitId = habitId, + date = completionRecord.date, + numOfTimesCompleted = completionRecord.numOfTimesCompleted, ) } } - private fun checkCompletedLater(routineId: Long, date: LocalDate): Boolean { - val completedLaterEntity: CompletedLaterHistoryEntity? = - db.completedLaterHistoryEntityQueries.getEntityByDate( - routineId, date - ).executeAsOneOrNull() - return completedLaterEntity != null - } - - private fun insertCompletedLaterDate(routineId: Long, date: LocalDate) { - val alreadyInserted = checkCompletedLater(routineId, date) - if (!alreadyInserted) { - db.completedLaterHistoryEntityQueries.insertCompletedLaterDate( - id = null, - routineId = routineId, - date = date, - ) + override suspend fun deleteCompletionByDate(habitId: Long, date: LocalDate) { + withContext(dispatcher) { + db.completionHistoryEntityQueries.deleteCompletionByDate(habitId, date) } } - override suspend fun getTotalTimesCompletedInPeriod( - routineId: Long, startDate: LocalDate, endDate: LocalDate - ): Double { - return withContext(dispatcher) { - db.completionHistoryEntityQueries - .getTotalTimesCompletedAtTheMomentOfDate(routineId, startDate, endDate) - .executeAsOne() + private suspend fun CompletionHistoryEntity.toExternalModel(): Habit.CompletionRecord? { + val habitType = withContext(dispatcher) { + db.habitEntityQueries.getHabitById(habitId).executeAsOneOrNull()?.type } - } + return when (habitType) { + HabitType.YesNoHabit -> Habit.YesNoHabit.CompletionRecord( + date = date, + numOfTimesCompleted = numOfTimesCompleted, + ) - override suspend fun getScheduleDeviationInPeriod( - routineId: Long, startDate: LocalDate, endDate: LocalDate - ): Double { - return withContext(dispatcher) { - db.completionHistoryEntityQueries - .getScheduleDeviationAtTheMomentOfDate(routineId, startDate, endDate) - .executeAsOne() + null -> null } } } \ No newline at end of file diff --git a/core/database/src/main/java/com/rendox/routinetracker/core/database/completion_time/CompletionTimeLocalDataSourceImpl.kt b/core/database/src/main/java/com/rendox/routinetracker/core/database/completion_time/CompletionTimeLocalDataSourceImpl.kt index c29975ae..fbac2815 100644 --- a/core/database/src/main/java/com/rendox/routinetracker/core/database/completion_time/CompletionTimeLocalDataSourceImpl.kt +++ b/core/database/src/main/java/com/rendox/routinetracker/core/database/completion_time/CompletionTimeLocalDataSourceImpl.kt @@ -1,15 +1,16 @@ package com.rendox.routinetracker.core.database.completion_time +import com.rendox.routinetracker.core.database.GetCompletionTime import com.rendox.routinetracker.core.database.RoutineTrackerDatabase -import com.rendox.routinetracker.core.database.completiontime.GetCompletionTime import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalTime class CompletionTimeLocalDataSourceImpl( private val db: RoutineTrackerDatabase, - private val dispatcher: CoroutineDispatcher, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, ) : CompletionTimeLocalDataSource { override suspend fun getCompletionTime(routineId: Long, date: LocalDate): LocalTime? { @@ -31,7 +32,10 @@ class CompletionTimeLocalDataSourceImpl( override suspend fun insertCompletionTime(id: Long?, routineId: Long, date: LocalDate, time: LocalTime) { withContext(dispatcher) { db.specificDateCustomCompletionTimeQueries.insertCompletiontime( - id, routineId, date, time.hour, time.minute + routineId = routineId, + date = date, + completionTimeHour = time.hour, + completionTimeMinute = time.minute, ) } } diff --git a/core/database/src/main/java/com/rendox/routinetracker/core/database/di/LocalDataSourceModule.kt b/core/database/src/main/java/com/rendox/routinetracker/core/database/di/LocalDataSourceModule.kt index 107c0a3f..94dc13bf 100644 --- a/core/database/src/main/java/com/rendox/routinetracker/core/database/di/LocalDataSourceModule.kt +++ b/core/database/src/main/java/com/rendox/routinetracker/core/database/di/LocalDataSourceModule.kt @@ -6,19 +6,17 @@ import app.cash.sqldelight.adapter.primitive.FloatColumnAdapter import app.cash.sqldelight.adapter.primitive.IntColumnAdapter import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver +import com.rendox.routinetracker.core.database.CompletionHistoryEntity import com.rendox.routinetracker.core.database.RoutineTrackerDatabase -import com.rendox.routinetracker.core.database.completionhistory.CompletedLaterHistoryEntity -import com.rendox.routinetracker.core.database.completionhistory.CompletionHistoryEntity -import com.rendox.routinetracker.core.database.completiontime.SpecificDateCustomCompletionTime -import com.rendox.routinetracker.core.database.routine.RoutineEntity +import com.rendox.routinetracker.core.database.SpecificDateCustomCompletionTime +import com.rendox.routinetracker.core.database.VacationEntity +import com.rendox.routinetracker.core.database.habit.HabitEntity import com.rendox.routinetracker.core.database.schedule.DueDateEntity import com.rendox.routinetracker.core.database.schedule.ScheduleEntity import com.rendox.routinetracker.core.database.schedule.WeekDayMonthRelatedEntity -import com.rendox.routinetracker.core.database.streak.StreakEntity import com.rendox.routinetracker.core.logic.time.AnnualDate import com.rendox.routinetracker.core.logic.time.epochDate import com.rendox.routinetracker.core.logic.time.plusDays -import kotlinx.coroutines.Dispatchers import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate import kotlinx.datetime.Month @@ -26,11 +24,6 @@ import kotlinx.datetime.daysUntil import org.koin.dsl.module val localDataSourceModule = module { - - single { - Dispatchers.IO - } - single { AndroidSqliteDriver( schema = RoutineTrackerDatabase.Schema, @@ -42,7 +35,7 @@ val localDataSourceModule = module { single { RoutineTrackerDatabase( driver = get(), - routineEntityAdapter = RoutineEntity.Adapter( + habitEntityAdapter = HabitEntity.Adapter( typeAdapter = EnumColumnAdapter(), sessionDurationMinutesAdapter = IntColumnAdapter, progressAdapter = FloatColumnAdapter, @@ -51,8 +44,8 @@ val localDataSourceModule = module { ), scheduleEntityAdapter = ScheduleEntity.Adapter( typeAdapter = EnumColumnAdapter(), - routineStartDateAdapter = localDateAdapter, - routineEndDateAdapter = localDateAdapter, + startDateAdapter = localDateAdapter, + endDateAdapter = localDateAdapter, vacationStartDateAdapter = localDateAdapter, vacationEndDateAdapter = localDateAdapter, startDayOfWeekInWeeklyScheduleAdapter = dayOfWeekAdapter, @@ -69,24 +62,19 @@ val localDataSourceModule = module { weekDayIndexAdapter = IntColumnAdapter, weekDayNumberMonthRelatedAdapter = EnumColumnAdapter(), ), - completionHistoryEntityAdapter = CompletionHistoryEntity.Adapter( - statusAdapter = EnumColumnAdapter(), - dateAdapter = localDateAdapter, - scheduleDeviationAdapter = FloatColumnAdapter, - timesCompletedAdapter = FloatColumnAdapter, - ), specificDateCustomCompletionTimeAdapter = SpecificDateCustomCompletionTime.Adapter( dateAdapter = localDateAdapter, completionTimeHourAdapter = IntColumnAdapter, completionTimeMinuteAdapter = IntColumnAdapter, ), - completedLaterHistoryEntityAdapter = CompletedLaterHistoryEntity.Adapter( + completionHistoryEntityAdapter = CompletionHistoryEntity.Adapter( dateAdapter = localDateAdapter, + numOfTimesCompletedAdapter = FloatColumnAdapter, ), - streakEntityAdapter = StreakEntity.Adapter( + vacationEntityAdapter = VacationEntity.Adapter( startDateAdapter = localDateAdapter, endDateAdapter = localDateAdapter, - ), + ) ) } } diff --git a/core/database/src/main/java/com/rendox/routinetracker/core/database/habit/HabitLocalDataSource.kt b/core/database/src/main/java/com/rendox/routinetracker/core/database/habit/HabitLocalDataSource.kt new file mode 100644 index 00000000..1534ada5 --- /dev/null +++ b/core/database/src/main/java/com/rendox/routinetracker/core/database/habit/HabitLocalDataSource.kt @@ -0,0 +1,21 @@ +package com.rendox.routinetracker.core.database.habit + +import com.rendox.routinetracker.core.model.Habit +import kotlinx.datetime.LocalTime + +interface HabitLocalDataSource { + + suspend fun getHabitById(habitId: Long): Habit + + suspend fun insertHabit(habit: Habit) + + suspend fun getAllHabits(): List + + suspend fun updateDueDateSpecificCompletionTime( + newTime: LocalTime, habitId: Long, dueDateNumber: Int + ) + + suspend fun getDueDateSpecificCompletionTime( + habitId: Long, dueDateNumber: Int + ): LocalTime? +} \ No newline at end of file diff --git a/core/database/src/main/java/com/rendox/routinetracker/core/database/routine/RoutineLocalDataSourceImpl.kt b/core/database/src/main/java/com/rendox/routinetracker/core/database/habit/HabitLocalDataSourceImpl.kt similarity index 71% rename from core/database/src/main/java/com/rendox/routinetracker/core/database/routine/RoutineLocalDataSourceImpl.kt rename to core/database/src/main/java/com/rendox/routinetracker/core/database/habit/HabitLocalDataSourceImpl.kt index 331ef288..f6250874 100644 --- a/core/database/src/main/java/com/rendox/routinetracker/core/database/routine/RoutineLocalDataSourceImpl.kt +++ b/core/database/src/main/java/com/rendox/routinetracker/core/database/habit/HabitLocalDataSourceImpl.kt @@ -1,46 +1,50 @@ -package com.rendox.routinetracker.core.database.routine +package com.rendox.routinetracker.core.database.habit import app.cash.sqldelight.TransactionWithoutReturn import com.rendox.routinetracker.core.database.RoutineTrackerDatabase import com.rendox.routinetracker.core.database.di.toDayOfWeek import com.rendox.routinetracker.core.database.di.toInt -import com.rendox.routinetracker.core.database.routine.model.RoutineType -import com.rendox.routinetracker.core.database.routine.model.ScheduleType -import com.rendox.routinetracker.core.database.routine.model.toExternalModel +import com.rendox.routinetracker.core.database.habit.model.HabitType +import com.rendox.routinetracker.core.database.habit.model.ScheduleType +import com.rendox.routinetracker.core.database.habit.model.toExternalModel import com.rendox.routinetracker.core.database.schedule.GetCompletionTime import com.rendox.routinetracker.core.database.schedule.ScheduleEntity import com.rendox.routinetracker.core.logic.time.WeekDayMonthRelated -import com.rendox.routinetracker.core.model.Routine +import com.rendox.routinetracker.core.model.Habit import com.rendox.routinetracker.core.model.Schedule import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.datetime.LocalTime -class RoutineLocalDataSourceImpl( +class HabitLocalDataSourceImpl( private val db: RoutineTrackerDatabase, - private val dispatcher: CoroutineDispatcher, -) : RoutineLocalDataSource { + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, +) : HabitLocalDataSource { - override suspend fun getRoutineById(routineId: Long): Routine { + override suspend fun getHabitById(habitId: Long): Habit { return withContext(dispatcher) { - db.routineEntityQueries.transactionWithResult { - val schedule = getScheduleEntity(routineId).toExternalModel( + db.habitEntityQueries.transactionWithResult { + val schedule = getScheduleEntity(habitId).toExternalModel( dueDatesProvider = { getDueDates(it) }, weekDaysMonthRelatedProvider = { getWeekDayMonthRelatedDays(it) } ) - getRoutineEntity(routineId).toExternalModel(schedule) + getHabitEntity(habitId).toExternalModel(schedule) } } } - private fun getRoutineEntity(id: Long): RoutineEntity = - db.routineEntityQueries.getRoutineById(id).executeAsOne() + private fun getHabitEntity(id: Long): HabitEntity = + db.habitEntityQueries.getHabitById(id).executeAsOne() private fun getScheduleEntity(id: Long): ScheduleEntity = db.scheduleEntityQueries.getScheduleById(id).executeAsOne() - private fun getDueDates(scheduleId: Long): List = - db.dueDateEntityQueries.getDueDates(scheduleId).executeAsList() + private fun getDueDates(scheduleId: Long): List { + val result = db.dueDateEntityQueries.getDueDates(scheduleId).executeAsList() + println("uniquelog $result") + return result + } private fun getWeekDayMonthRelatedDays(scheduleId: Long): List = db.weekDayMonthRelatedEntityQueries @@ -53,13 +57,13 @@ class RoutineLocalDataSourceImpl( ) } - override suspend fun insertRoutine(routine: Routine) { + override suspend fun insertHabit(habit: Habit) { return withContext(dispatcher) { - db.routineEntityQueries.transaction { - when (routine) { - is Routine.YesNoRoutine -> { - insertYesNoRoutine(routine) - insertSchedule(routine.schedule) + db.habitEntityQueries.transaction { + when (habit) { + is Habit.YesNoHabit -> { + insertYesNoHabit(habit) + insertSchedule(habit.schedule) } } } @@ -67,16 +71,16 @@ class RoutineLocalDataSourceImpl( } @Suppress("UnusedReceiverParameter") - private fun TransactionWithoutReturn.insertYesNoRoutine(routine: Routine.YesNoRoutine) { - db.routineEntityQueries.insertRoutine( - id = routine.id, - type = RoutineType.YesNoRoutine, - name = routine.name, - description = routine.description, - sessionDurationMinutes = routine.sessionDurationMinutes, - progress = routine.progress, - defaultCompletionTimeHour = routine.defaultCompletionTime?.hour, - defaultCompletionTimeMinute = routine.defaultCompletionTime?.minute, + private fun TransactionWithoutReturn.insertYesNoHabit(habit: Habit.YesNoHabit) { + db.habitEntityQueries.insertHabit( + id = habit.id, + type = HabitType.YesNoHabit, + name = habit.name, + description = habit.description, + sessionDurationMinutes = habit.sessionDurationMinutes, + progress = habit.progress, + defaultCompletionTimeHour = habit.defaultCompletionTime?.hour, + defaultCompletionTimeMinute = habit.defaultCompletionTime?.minute, ) } @@ -101,11 +105,9 @@ class RoutineLocalDataSourceImpl( } is Schedule.MonthlyScheduleByNumOfDueDays -> - insertMonthlyScheduleByNumOfDueDays( - schedule - ) + insertMonthlyScheduleByNumOfDueDays(schedule) - is Schedule.PeriodicCustomSchedule -> + is Schedule.AlternateDaysSchedule -> insertPeriodicCustomSchedule(schedule) is Schedule.CustomDateSchedule -> { @@ -127,14 +129,14 @@ class RoutineLocalDataSourceImpl( db.scheduleEntityQueries.insertSchedule( id = null, type = ScheduleType.EveryDaySchedule, - routineStartDate = schedule.routineStartDate, - routineEndDate = schedule.routineEndDate, + startDate = schedule.startDate, + endDate = schedule.endDate, backlogEnabled = schedule.backlogEnabled, - cancelDuenessIfDoneAhead = schedule.cancelDuenessIfDoneAhead, + cancelDuenessIfDoneAhead = schedule.completingAheadEnabled, vacationStartDate = schedule.vacationStartDate, vacationEndDate = schedule.vacationEndDate, startDayOfWeekInWeeklySchedule = null, - startFromRoutineStartInMonthlyAndAnnualSchedule = null, + startFromHabitStartInMonthlyAndAnnualSchedule = null, includeLastDayOfMonthInMonthlySchedule = null, periodicSeparationEnabledInPeriodicSchedule = null, numOfDueDaysInByNumOfDueDaysSchedule = null, @@ -147,14 +149,14 @@ class RoutineLocalDataSourceImpl( db.scheduleEntityQueries.insertSchedule( id = null, type = ScheduleType.WeeklyScheduleByDueDaysOfWeek, - routineStartDate = schedule.routineStartDate, - routineEndDate = schedule.routineEndDate, + startDate = schedule.startDate, + endDate = schedule.endDate, backlogEnabled = schedule.backlogEnabled, - cancelDuenessIfDoneAhead = schedule.cancelDuenessIfDoneAhead, + cancelDuenessIfDoneAhead = schedule.completingAheadEnabled, vacationStartDate = schedule.vacationStartDate, vacationEndDate = schedule.vacationEndDate, startDayOfWeekInWeeklySchedule = schedule.startDayOfWeek, - startFromRoutineStartInMonthlyAndAnnualSchedule = null, + startFromHabitStartInMonthlyAndAnnualSchedule = null, includeLastDayOfMonthInMonthlySchedule = null, periodicSeparationEnabledInPeriodicSchedule = schedule.periodSeparationEnabled, numOfDueDaysInByNumOfDueDaysSchedule = null, @@ -167,14 +169,14 @@ class RoutineLocalDataSourceImpl( db.scheduleEntityQueries.insertSchedule( id = null, type = ScheduleType.WeeklyScheduleByNumOfDueDays, - routineStartDate = schedule.routineStartDate, - routineEndDate = schedule.routineEndDate, + startDate = schedule.startDate, + endDate = schedule.endDate, backlogEnabled = schedule.backlogEnabled, - cancelDuenessIfDoneAhead = schedule.cancelDuenessIfDoneAhead, + cancelDuenessIfDoneAhead = schedule.completingAheadEnabled, vacationStartDate = schedule.vacationStartDate, vacationEndDate = schedule.vacationEndDate, startDayOfWeekInWeeklySchedule = schedule.startDayOfWeek, - startFromRoutineStartInMonthlyAndAnnualSchedule = null, + startFromHabitStartInMonthlyAndAnnualSchedule = null, includeLastDayOfMonthInMonthlySchedule = null, periodicSeparationEnabledInPeriodicSchedule = schedule.periodSeparationEnabled, numOfDueDaysInByNumOfDueDaysSchedule = schedule.numOfDueDays, @@ -187,14 +189,14 @@ class RoutineLocalDataSourceImpl( db.scheduleEntityQueries.insertSchedule( id = null, type = ScheduleType.MonthlyScheduleByDueDatesIndices, - routineStartDate = schedule.routineStartDate, - routineEndDate = schedule.routineEndDate, + startDate = schedule.startDate, + endDate = schedule.endDate, backlogEnabled = schedule.backlogEnabled, - cancelDuenessIfDoneAhead = schedule.cancelDuenessIfDoneAhead, + cancelDuenessIfDoneAhead = schedule.completingAheadEnabled, vacationStartDate = schedule.vacationStartDate, vacationEndDate = schedule.vacationEndDate, startDayOfWeekInWeeklySchedule = null, - startFromRoutineStartInMonthlyAndAnnualSchedule = schedule.startFromRoutineStart, + startFromHabitStartInMonthlyAndAnnualSchedule = schedule.startFromHabitStart, includeLastDayOfMonthInMonthlySchedule = schedule.includeLastDayOfMonth, periodicSeparationEnabledInPeriodicSchedule = schedule.periodSeparationEnabled, numOfDueDaysInByNumOfDueDaysSchedule = null, @@ -207,14 +209,14 @@ class RoutineLocalDataSourceImpl( db.scheduleEntityQueries.insertSchedule( id = null, type = ScheduleType.MonthlyScheduleByNumOfDueDays, - routineStartDate = schedule.routineStartDate, - routineEndDate = schedule.routineEndDate, + startDate = schedule.startDate, + endDate = schedule.endDate, backlogEnabled = schedule.backlogEnabled, - cancelDuenessIfDoneAhead = schedule.cancelDuenessIfDoneAhead, + cancelDuenessIfDoneAhead = schedule.completingAheadEnabled, vacationStartDate = schedule.vacationStartDate, vacationEndDate = schedule.vacationEndDate, startDayOfWeekInWeeklySchedule = null, - startFromRoutineStartInMonthlyAndAnnualSchedule = schedule.startFromRoutineStart, + startFromHabitStartInMonthlyAndAnnualSchedule = schedule.startFromHabitStart, includeLastDayOfMonthInMonthlySchedule = null, periodicSeparationEnabledInPeriodicSchedule = schedule.periodSeparationEnabled, numOfDueDaysInByNumOfDueDaysSchedule = schedule.numOfDueDays, @@ -234,18 +236,18 @@ class RoutineLocalDataSourceImpl( } } - private fun insertPeriodicCustomSchedule(schedule: Schedule.PeriodicCustomSchedule) { + private fun insertPeriodicCustomSchedule(schedule: Schedule.AlternateDaysSchedule) { db.scheduleEntityQueries.insertSchedule( id = null, type = ScheduleType.PeriodicCustomSchedule, - routineStartDate = schedule.routineStartDate, - routineEndDate = schedule.routineEndDate, + startDate = schedule.startDate, + endDate = schedule.endDate, backlogEnabled = schedule.backlogEnabled, - cancelDuenessIfDoneAhead = schedule.cancelDuenessIfDoneAhead, + cancelDuenessIfDoneAhead = schedule.completingAheadEnabled, vacationStartDate = schedule.vacationStartDate, vacationEndDate = schedule.vacationEndDate, startDayOfWeekInWeeklySchedule = null, - startFromRoutineStartInMonthlyAndAnnualSchedule = null, + startFromHabitStartInMonthlyAndAnnualSchedule = null, includeLastDayOfMonthInMonthlySchedule = null, periodicSeparationEnabledInPeriodicSchedule = schedule.periodSeparationEnabled, numOfDueDaysInByNumOfDueDaysSchedule = schedule.numOfDueDays, @@ -258,14 +260,14 @@ class RoutineLocalDataSourceImpl( db.scheduleEntityQueries.insertSchedule( id = null, type = ScheduleType.CustomDateSchedule, - routineStartDate = schedule.routineStartDate, - routineEndDate = schedule.routineEndDate, + startDate = schedule.startDate, + endDate = schedule.endDate, backlogEnabled = schedule.backlogEnabled, - cancelDuenessIfDoneAhead = schedule.cancelDuenessIfDoneAhead, + cancelDuenessIfDoneAhead = schedule.completingAheadEnabled, vacationStartDate = schedule.vacationStartDate, vacationEndDate = schedule.vacationEndDate, startDayOfWeekInWeeklySchedule = null, - startFromRoutineStartInMonthlyAndAnnualSchedule = null, + startFromHabitStartInMonthlyAndAnnualSchedule = null, includeLastDayOfMonthInMonthlySchedule = null, periodicSeparationEnabledInPeriodicSchedule = null, numOfDueDaysInByNumOfDueDaysSchedule = null, @@ -278,14 +280,14 @@ class RoutineLocalDataSourceImpl( db.scheduleEntityQueries.insertSchedule( id = null, type = ScheduleType.AnnualScheduleByDueDates, - routineStartDate = schedule.routineStartDate, - routineEndDate = schedule.routineEndDate, + startDate = schedule.startDate, + endDate = schedule.endDate, backlogEnabled = schedule.backlogEnabled, - cancelDuenessIfDoneAhead = schedule.cancelDuenessIfDoneAhead, + cancelDuenessIfDoneAhead = schedule.completingAheadEnabled, vacationStartDate = schedule.vacationStartDate, vacationEndDate = schedule.vacationEndDate, startDayOfWeekInWeeklySchedule = null, - startFromRoutineStartInMonthlyAndAnnualSchedule = schedule.startFromRoutineStart, + startFromHabitStartInMonthlyAndAnnualSchedule = schedule.startFromHabitStart, includeLastDayOfMonthInMonthlySchedule = null, periodicSeparationEnabledInPeriodicSchedule = schedule.periodSeparationEnabled, numOfDueDaysInByNumOfDueDaysSchedule = null, @@ -298,14 +300,14 @@ class RoutineLocalDataSourceImpl( db.scheduleEntityQueries.insertSchedule( id = null, type = ScheduleType.AnnualScheduleByNumOfDueDays, - routineStartDate = schedule.routineStartDate, - routineEndDate = schedule.routineEndDate, + startDate = schedule.startDate, + endDate = schedule.endDate, backlogEnabled = schedule.backlogEnabled, - cancelDuenessIfDoneAhead = schedule.cancelDuenessIfDoneAhead, + cancelDuenessIfDoneAhead = schedule.completingAheadEnabled, vacationStartDate = schedule.vacationStartDate, vacationEndDate = schedule.vacationEndDate, startDayOfWeekInWeeklySchedule = null, - startFromRoutineStartInMonthlyAndAnnualSchedule = schedule.startFromRoutineStart, + startFromHabitStartInMonthlyAndAnnualSchedule = schedule.startFromHabitStart, includeLastDayOfMonthInMonthlySchedule = null, periodicSeparationEnabledInPeriodicSchedule = schedule.periodSeparationEnabled, numOfDueDaysInByNumOfDueDaysSchedule = schedule.numOfDueDays, @@ -318,7 +320,6 @@ class RoutineLocalDataSourceImpl( val lastInsertScheduleId = db.scheduleEntityQueries.lastInsertRowId().executeAsOne() for (dueDate in dueDates) { db.dueDateEntityQueries.insertDueDate( - id = null, scheduleId = lastInsertScheduleId, dueDateNumber = dueDate, completionTimeHour = null, @@ -327,39 +328,39 @@ class RoutineLocalDataSourceImpl( } } - override suspend fun getAllRoutines(): List { + override suspend fun getAllHabits(): List { return withContext(dispatcher) { - db.routineEntityQueries.getAllRoutines() + db.habitEntityQueries.getAllHabits() .executeAsList() - .map { routineEntity -> - val schedule = getScheduleEntity(routineEntity.id).toExternalModel( + .map { habitEntity -> + val schedule = getScheduleEntity(habitEntity.id).toExternalModel( dueDatesProvider = { getDueDates(it) }, weekDaysMonthRelatedProvider = { getWeekDayMonthRelatedDays(it) } ) - routineEntity.toExternalModel(schedule) + habitEntity.toExternalModel(schedule) } } } override suspend fun updateDueDateSpecificCompletionTime( - newTime: LocalTime, routineId: Long, dueDateNumber: Int + newTime: LocalTime, habitId: Long, dueDateNumber: Int ) { withContext(dispatcher) { db.dueDateEntityQueries.updateCompletionTime( completionTimeHour = newTime.hour, completionTimeMinute = newTime.minute, - scheduleId = routineId, + scheduleId = habitId, dueDateNumber = dueDateNumber, ) } } override suspend fun getDueDateSpecificCompletionTime( - routineId: Long, dueDateNumber: Int + habitId: Long, dueDateNumber: Int ): LocalTime? { return withContext(dispatcher) { db.dueDateEntityQueries - .getCompletionTime(routineId, dueDateNumber) + .getCompletionTime(habitId, dueDateNumber) .executeAsOneOrNull() ?.toExternalModel() } diff --git a/core/database/src/main/java/com/rendox/routinetracker/core/database/routine/model/RoutineEntityConvertions.kt b/core/database/src/main/java/com/rendox/routinetracker/core/database/habit/model/RoutineEntityConvertions.kt similarity index 60% rename from core/database/src/main/java/com/rendox/routinetracker/core/database/routine/model/RoutineEntityConvertions.kt rename to core/database/src/main/java/com/rendox/routinetracker/core/database/habit/model/RoutineEntityConvertions.kt index c8e012bd..d1ee305f 100644 --- a/core/database/src/main/java/com/rendox/routinetracker/core/database/routine/model/RoutineEntityConvertions.kt +++ b/core/database/src/main/java/com/rendox/routinetracker/core/database/habit/model/RoutineEntityConvertions.kt @@ -1,31 +1,31 @@ -package com.rendox.routinetracker.core.database.routine.model +package com.rendox.routinetracker.core.database.habit.model -import com.rendox.routinetracker.core.database.routine.RoutineEntity -import com.rendox.routinetracker.core.model.Routine +import com.rendox.routinetracker.core.database.habit.HabitEntity +import com.rendox.routinetracker.core.model.Habit import com.rendox.routinetracker.core.model.Schedule import kotlinx.datetime.LocalTime -enum class RoutineType { - YesNoRoutine, +enum class HabitType { + YesNoHabit, // MeasurableRoutine, // TasksRoutine, } -fun RoutineEntity.toExternalModel(schedule: Schedule) = when (this.type) { - RoutineType.YesNoRoutine -> this.toYesNoRoutine( +fun HabitEntity.toExternalModel(schedule: Schedule) = when (this.type) { + HabitType.YesNoHabit -> this.toYesNoRoutine( schedule = schedule, ) } -internal fun RoutineEntity.toYesNoRoutine( +internal fun HabitEntity.toYesNoRoutine( schedule: Schedule, -): Routine { +): Habit { val defaultCompletionTime = if (defaultCompletionTimeHour != null && defaultCompletionTimeMinute != null) { LocalTime(hour = defaultCompletionTimeHour, minute = defaultCompletionTimeMinute) } else null - return Routine.YesNoRoutine( + return Habit.YesNoHabit( id = id, name = name, description = description, diff --git a/core/database/src/main/java/com/rendox/routinetracker/core/database/routine/model/ScheduleEntityConvertions.kt b/core/database/src/main/java/com/rendox/routinetracker/core/database/habit/model/ScheduleEntityConvertions.kt similarity index 75% rename from core/database/src/main/java/com/rendox/routinetracker/core/database/routine/model/ScheduleEntityConvertions.kt rename to core/database/src/main/java/com/rendox/routinetracker/core/database/habit/model/ScheduleEntityConvertions.kt index 54db517b..7014382a 100644 --- a/core/database/src/main/java/com/rendox/routinetracker/core/database/routine/model/ScheduleEntityConvertions.kt +++ b/core/database/src/main/java/com/rendox/routinetracker/core/database/habit/model/ScheduleEntityConvertions.kt @@ -1,4 +1,4 @@ -package com.rendox.routinetracker.core.database.routine.model +package com.rendox.routinetracker.core.database.habit.model import com.rendox.routinetracker.core.database.di.toAnnualDate import com.rendox.routinetracker.core.database.di.toDayOfWeek @@ -54,8 +54,8 @@ fun ScheduleEntity.toExternalModel( } internal fun ScheduleEntity.toEveryDaySchedule() = Schedule.EveryDaySchedule( - routineStartDate = routineStartDate, - routineEndDate = routineEndDate, + startDate = startDate, + endDate = endDate, vacationStartDate = vacationStartDate, vacationEndDate = vacationEndDate, ) @@ -66,23 +66,22 @@ internal fun ScheduleEntity.toWeeklyScheduleByDueDaysOfWeek( dueDaysOfWeek = dueDates.map { it.toDayOfWeek() }, startDayOfWeek = startDayOfWeekInWeeklySchedule, periodSeparationEnabled = periodicSeparationEnabledInPeriodicSchedule!!, - routineStartDate = routineStartDate, - routineEndDate = routineEndDate, + startDate = startDate, + endDate = endDate, vacationStartDate = vacationStartDate, vacationEndDate = vacationEndDate, backlogEnabled = backlogEnabled, - cancelDuenessIfDoneAhead = cancelDuenessIfDoneAhead, + completingAheadEnabled = cancelDuenessIfDoneAhead, ) internal fun ScheduleEntity.toWeeklyScheduleByNumOfDueDays() = Schedule.WeeklyScheduleByNumOfDueDays( numOfDueDays = numOfDueDaysInByNumOfDueDaysSchedule!!, numOfDueDaysInFirstPeriod = numOfDueDaysInFirstPeriodInByNumOfDueDaysSchedule, - routineStartDate = routineStartDate, - routineEndDate = routineEndDate, + startDate = startDate, + endDate = endDate, vacationStartDate = vacationStartDate, vacationEndDate = vacationEndDate, - backlogEnabled = backlogEnabled, - cancelDuenessIfDoneAhead = cancelDuenessIfDoneAhead, + periodSeparationEnabled = periodicSeparationEnabledInPeriodicSchedule!!, ) internal fun ScheduleEntity.toMonthlyScheduleByDueDatesIndices( @@ -92,73 +91,72 @@ internal fun ScheduleEntity.toMonthlyScheduleByDueDatesIndices( dueDatesIndices = dueDatesIndices, includeLastDayOfMonth = includeLastDayOfMonthInMonthlySchedule!!, weekDaysMonthRelated = weekDaysMonthRelated, - startFromRoutineStart = startFromRoutineStartInMonthlyAndAnnualSchedule!!, + startFromHabitStart = startFromHabitStartInMonthlyAndAnnualSchedule!!, periodSeparationEnabled = periodicSeparationEnabledInPeriodicSchedule!!, - routineStartDate = routineStartDate, - routineEndDate = routineEndDate, + startDate = startDate, + endDate = endDate, vacationStartDate = vacationStartDate, vacationEndDate = vacationEndDate, backlogEnabled = backlogEnabled, - cancelDuenessIfDoneAhead = cancelDuenessIfDoneAhead, + completingAheadEnabled = cancelDuenessIfDoneAhead, ) internal fun ScheduleEntity.toMonthlyScheduleByNumOfDueDays() = Schedule.MonthlyScheduleByNumOfDueDays( numOfDueDays = numOfDueDaysInByNumOfDueDaysSchedule!!, numOfDueDaysInFirstPeriod = numOfDueDaysInFirstPeriodInByNumOfDueDaysSchedule, - startFromRoutineStart = startFromRoutineStartInMonthlyAndAnnualSchedule!!, - routineStartDate = routineStartDate, - routineEndDate = routineEndDate, + startFromHabitStart = startFromHabitStartInMonthlyAndAnnualSchedule!!, + startDate = startDate, + endDate = endDate, vacationStartDate = vacationStartDate, vacationEndDate = vacationEndDate, - backlogEnabled = backlogEnabled, - cancelDuenessIfDoneAhead = cancelDuenessIfDoneAhead, + periodSeparationEnabled = periodicSeparationEnabledInPeriodicSchedule!!, ) -internal fun ScheduleEntity.toPeriodicCustomSchedule() = Schedule.PeriodicCustomSchedule ( +internal fun ScheduleEntity.toPeriodicCustomSchedule() = Schedule.AlternateDaysSchedule ( numOfDueDays = numOfDueDaysInByNumOfDueDaysSchedule!!, numOfDaysInPeriod = numOfDaysInPeriodicCustomSchedule!!, - routineStartDate = routineStartDate, - routineEndDate = routineEndDate, + startDate = startDate, + endDate = endDate, vacationStartDate = vacationStartDate, vacationEndDate = vacationEndDate, backlogEnabled = backlogEnabled, - cancelDuenessIfDoneAhead = cancelDuenessIfDoneAhead, + completingAheadEnabled = cancelDuenessIfDoneAhead, + periodSeparationEnabled = periodicSeparationEnabledInPeriodicSchedule!!, ) internal fun ScheduleEntity.toCustomDateSchedule( dueDatesIndices: List, ) = Schedule.CustomDateSchedule( dueDates = dueDatesIndices.map { it.toLocalDate() }, - routineStartDate = routineStartDate, - routineEndDate = routineEndDate, + startDate = startDate, + endDate = endDate, vacationStartDate = vacationStartDate, vacationEndDate = vacationEndDate, backlogEnabled = backlogEnabled, - cancelDuenessIfDoneAhead = cancelDuenessIfDoneAhead, + completingAheadEnabled = cancelDuenessIfDoneAhead, ) internal fun ScheduleEntity.toAnnualScheduleByDueDates( dueDates: List, ) = Schedule.AnnualScheduleByDueDates( dueDates = dueDates.map { it.toAnnualDate() }, - startFromRoutineStart = startFromRoutineStartInMonthlyAndAnnualSchedule!!, + startFromHabitStart = startFromHabitStartInMonthlyAndAnnualSchedule!!, periodSeparationEnabled = periodicSeparationEnabledInPeriodicSchedule!!, - routineStartDate = routineStartDate, - routineEndDate = routineEndDate, + startDate = startDate, + endDate = endDate, vacationStartDate = vacationStartDate, vacationEndDate = vacationEndDate, backlogEnabled = backlogEnabled, - cancelDuenessIfDoneAhead = cancelDuenessIfDoneAhead, + completingAheadEnabled = cancelDuenessIfDoneAhead, ) internal fun ScheduleEntity.toAnnualScheduleByNumOfDueDays() = Schedule.AnnualScheduleByNumOfDueDays( numOfDueDays = numOfDueDaysInByNumOfDueDaysSchedule!!, numOfDueDaysInFirstPeriod = numOfDueDaysInFirstPeriodInByNumOfDueDaysSchedule, - startFromRoutineStart = startFromRoutineStartInMonthlyAndAnnualSchedule!!, - routineStartDate = routineStartDate, - routineEndDate = routineEndDate, + startFromHabitStart = startFromHabitStartInMonthlyAndAnnualSchedule!!, + startDate = startDate, + endDate = endDate, vacationStartDate = vacationStartDate, vacationEndDate = vacationEndDate, - backlogEnabled = backlogEnabled, - cancelDuenessIfDoneAhead = cancelDuenessIfDoneAhead, + periodSeparationEnabled = periodicSeparationEnabledInPeriodicSchedule!!, ) \ No newline at end of file diff --git a/core/database/src/main/java/com/rendox/routinetracker/core/database/routine/RoutineLocalDataSource.kt b/core/database/src/main/java/com/rendox/routinetracker/core/database/routine/RoutineLocalDataSource.kt deleted file mode 100644 index 9fb86e6b..00000000 --- a/core/database/src/main/java/com/rendox/routinetracker/core/database/routine/RoutineLocalDataSource.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.rendox.routinetracker.core.database.routine - -import com.rendox.routinetracker.core.model.Routine -import kotlinx.datetime.LocalTime - -interface RoutineLocalDataSource { - - suspend fun getRoutineById(routineId: Long): Routine - - suspend fun insertRoutine(routine: Routine) - - suspend fun getAllRoutines(): List - - suspend fun updateDueDateSpecificCompletionTime( - newTime: LocalTime, routineId: Long, dueDateNumber: Int - ) - - suspend fun getDueDateSpecificCompletionTime( - routineId: Long, dueDateNumber: Int - ): LocalTime? -} \ No newline at end of file diff --git a/core/database/src/main/java/com/rendox/routinetracker/core/database/streak/StreakLocalDataSource.kt b/core/database/src/main/java/com/rendox/routinetracker/core/database/streak/StreakLocalDataSource.kt deleted file mode 100644 index a569eda7..00000000 --- a/core/database/src/main/java/com/rendox/routinetracker/core/database/streak/StreakLocalDataSource.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.rendox.routinetracker.core.database.streak - -import com.rendox.routinetracker.core.model.Streak -import kotlinx.datetime.LocalDate - -interface StreakLocalDataSource { - - suspend fun getAllStreaks( - routineId: Long, - afterDateInclusive: LocalDate? = null, - beforeDateInclusive: LocalDate? = null, - ): List - - suspend fun getStreakByDate(routineId: Long, dateWithinStreak: LocalDate): Streak? - suspend fun getLastStreak(routineId: Long): Streak? - suspend fun insertStreak(streak: Streak, routineId: Long) - suspend fun deleteStreakById(id: Long) - suspend fun updateStreakById(id: Long, start: LocalDate, end: LocalDate?) -} \ No newline at end of file diff --git a/core/database/src/main/java/com/rendox/routinetracker/core/database/streak/StreakLocalDataSourceImpl.kt b/core/database/src/main/java/com/rendox/routinetracker/core/database/streak/StreakLocalDataSourceImpl.kt deleted file mode 100644 index 0ee97a17..00000000 --- a/core/database/src/main/java/com/rendox/routinetracker/core/database/streak/StreakLocalDataSourceImpl.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.rendox.routinetracker.core.database.streak - -import com.rendox.routinetracker.core.database.RoutineTrackerDatabase -import com.rendox.routinetracker.core.model.Streak -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.withContext -import kotlinx.datetime.LocalDate - -class StreakLocalDataSourceImpl( - private val db: RoutineTrackerDatabase, - private val dispatcher: CoroutineDispatcher, -) : StreakLocalDataSource { - - override suspend fun getAllStreaks( - routineId: Long, - afterDateInclusive: LocalDate?, - beforeDateInclusive: LocalDate?, - ): List { - return withContext(dispatcher) { - db.streakEntityQueries.getAllStreaks( - routineId = routineId, - afterDateInclusive = afterDateInclusive, - beforeDateInclusive = beforeDateInclusive, - ).executeAsList().map { it.toExternalModel() } - } - } - - override suspend fun getStreakByDate(routineId: Long, dateWithinStreak: LocalDate): Streak? { - return withContext(dispatcher) { - db.streakEntityQueries.getStreakByDate( - routineId, dateWithinStreak - ).executeAsOneOrNull()?.toExternalModel() - } - } - - override suspend fun getLastStreak(routineId: Long): Streak? { - return withContext(dispatcher) { - db.streakEntityQueries.getLastStreak(routineId) - .executeAsOneOrNull() - ?.toExternalModel() - } - } - - override suspend fun insertStreak(streak: Streak, routineId: Long) { - withContext(dispatcher) { - db.streakEntityQueries.insertStreak( - id = streak.id, - routineId = routineId, - startDate = streak.startDate, - endDate = streak.endDate, - ) - } - } - - override suspend fun deleteStreakById(id: Long) { - withContext(dispatcher) { - db.streakEntityQueries.deleteStreakById(id) - } - } - - override suspend fun updateStreakById(id: Long, start: LocalDate, end: LocalDate?) { - withContext(dispatcher) { - db.streakEntityQueries.updateStreakById( - id = id, - startDate = start, - endDate = end, - ) - } - } - - private fun StreakEntity.toExternalModel() = Streak( - id = id, - startDate = startDate, - endDate = endDate, - ) -} \ No newline at end of file diff --git a/core/database/src/main/java/com/rendox/routinetracker/core/database/vacation/VacationLocalDataSource.kt b/core/database/src/main/java/com/rendox/routinetracker/core/database/vacation/VacationLocalDataSource.kt new file mode 100644 index 00000000..84381d28 --- /dev/null +++ b/core/database/src/main/java/com/rendox/routinetracker/core/database/vacation/VacationLocalDataSource.kt @@ -0,0 +1,26 @@ +package com.rendox.routinetracker.core.database.vacation + +import com.rendox.routinetracker.core.model.Vacation +import kotlinx.datetime.LocalDate + +interface VacationLocalDataSource { + suspend fun getVacationByDate( + habitId: Long, date: LocalDate + ): Vacation? + + suspend fun getPreviousVacation( + habitId: Long, + currentDate: LocalDate, + ): Vacation? + + suspend fun getLastVacation(habitId: Long): Vacation? + + suspend fun getVacationsInPeriod( + habitId: Long, + minDate: LocalDate? = null, + maxDate: LocalDate? = null, + ): List + + suspend fun insertVacation(habitId: Long, vacation: Vacation) + suspend fun deleteVacationById(id: Long) +} \ No newline at end of file diff --git a/core/database/src/main/java/com/rendox/routinetracker/core/database/vacation/VacationLocalDataSourceImpl.kt b/core/database/src/main/java/com/rendox/routinetracker/core/database/vacation/VacationLocalDataSourceImpl.kt new file mode 100644 index 00000000..d5032605 --- /dev/null +++ b/core/database/src/main/java/com/rendox/routinetracker/core/database/vacation/VacationLocalDataSourceImpl.kt @@ -0,0 +1,81 @@ +package com.rendox.routinetracker.core.database.vacation + +import com.rendox.routinetracker.core.database.RoutineTrackerDatabase +import com.rendox.routinetracker.core.database.VacationEntity +import com.rendox.routinetracker.core.model.Vacation +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.datetime.LocalDate + +class VacationLocalDataSourceImpl( + private val db: RoutineTrackerDatabase, + private val dispatcher: CoroutineDispatcher = Dispatchers.IO, +): VacationLocalDataSource { + override suspend fun getVacationByDate(habitId: Long, date: LocalDate): Vacation? { + return withContext(dispatcher) { + db.vacationEntityQueries.getVacationByDate(habitId, date) + .executeAsOneOrNull()?.toExternalModel() + } + } + + override suspend fun getPreviousVacation(habitId: Long, currentDate: LocalDate): Vacation? { + return withContext(dispatcher) { + val previousVacation = db.vacationEntityQueries.getPreviousVacation( + habitId = habitId, + currentDate = currentDate, + ).executeAsOneOrNull() + previousVacation?.let { + Vacation( + id = it.id, + startDate = it.startDate, + endDate = it.endDate, + ) + } + } + } + + override suspend fun getLastVacation(habitId: Long): Vacation? { + return withContext(dispatcher) { + db.vacationEntityQueries.getLastVacation(habitId) + .executeAsOneOrNull()?.toExternalModel() + } + } + + override suspend fun getVacationsInPeriod( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate?, + ): List { + return withContext(dispatcher) { + db.vacationEntityQueries.getVacationsInPeriod( + habitId = habitId, + minDate = minDate, + maxDate = maxDate, + ).executeAsList().map { it.toExternalModel() } + } + } + + override suspend fun insertVacation(habitId: Long, vacation: Vacation) { + return withContext(dispatcher) { + db.vacationEntityQueries.insertVacation( + habitId = habitId, + id = vacation.id, + startDate = vacation.startDate, + endDate = vacation.endDate, + ) + } + } + + override suspend fun deleteVacationById(id: Long) { + return withContext(dispatcher) { + db.vacationEntityQueries.deleteVacationById(id) + } + } + + private fun VacationEntity.toExternalModel() = Vacation( + id = id, + startDate = startDate, + endDate = endDate, + ) +} \ No newline at end of file diff --git a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/completionHistoryEntity.sq b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/completionHistoryEntity.sq new file mode 100644 index 00000000..4d18b12b --- /dev/null +++ b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/completionHistoryEntity.sq @@ -0,0 +1,55 @@ +import kotlin.Float; +import kotlinx.datetime.LocalDate; + +CREATE TABLE completionHistoryEntity ( + habitId INTEGER NOT NULL, + date INTEGER AS LocalDate NOT NULL, + numOfTimesCompleted REAL AS Float NOT NULL, + PRIMARY KEY (habitId, date) +); + +insertCompletion: +INSERT OR REPLACE +INTO completionHistoryEntity +VALUES (?, ?, ?); + +getNumOfTimesCompletedInPeriod: +SELECT TOTAL(numOfTimesCompleted) +FROM completionHistoryEntity +WHERE habitId = :habitId + AND (:minDate IS NULL OR :minDate <= date) + AND (:maxDate IS NULL OR date <= :maxDate); + +getRecordByDate: +SELECT * +FROM completionHistoryEntity +WHERE habitId = ? AND date = ?; + +getLastRecord: +SELECT * +FROM completionHistoryEntity +WHERE habitId = :habitId + AND (:minDate IS NULL OR :minDate <= date) + AND (:maxDate IS NULL OR date <= :maxDate) +ORDER BY date DESC +LIMIT 1; + +getFirstRecord: +SELECT * +FROM completionHistoryEntity +WHERE habitId = :habitId + AND (:minDate IS NULL OR :minDate <= date) + AND (:maxDate IS NULL OR date <= :maxDate) +ORDER BY date ASC +LIMIT 1; + +getRecordsInPeriod: +SELECT * +FROM completionHistoryEntity +WHERE habitId = :habitId + AND (:minDate IS NULL OR :minDate <= date) + AND (:maxDate IS NULL OR date <= :maxDate); + +deleteCompletionByDate: +DELETE FROM completionHistoryEntity +WHERE habitId = ? AND date = ?; \ No newline at end of file diff --git a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/completion_history/completedLaterHistoryEntity.sq b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/completion_history/completedLaterHistoryEntity.sq deleted file mode 100644 index b3c9309b..00000000 --- a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/completion_history/completedLaterHistoryEntity.sq +++ /dev/null @@ -1,21 +0,0 @@ -import kotlinx.datetime.LocalDate; - -CREATE TABLE completedLaterHistoryEntity ( - id INTEGER NOT NULL PRIMARY KEY, - routineId INTEGER NOT NULL, - date INTEGER AS LocalDate NOT NULL, - FOREIGN KEY(routineId) REFERENCES scheduleEntity(id) -); - -getEntityByDate: -SELECT * -FROM completedLaterHistoryEntity -WHERE routineId = ? AND date = ?; - -insertCompletedLaterDate: -INSERT INTO completedLaterHistoryEntity -VALUES (?, ?, ?); - -deleteCompletedLaterDate: -DELETE FROM completedLaterHistoryEntity -WHERE routineId = ? AND date = ?; \ No newline at end of file diff --git a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/completion_history/completionHistoryEntity.sq b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/completion_history/completionHistoryEntity.sq deleted file mode 100644 index 97c0584e..00000000 --- a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/completion_history/completionHistoryEntity.sq +++ /dev/null @@ -1,97 +0,0 @@ -import com.rendox.routinetracker.core.model.HistoricalStatus; -import kotlin.Float; -import kotlinx.datetime.LocalDate; - -CREATE TABLE completionHistoryEntity ( - id INTEGER NOT NULL PRIMARY KEY, - routineId INTEGER NOT NULL, - date INTEGER AS LocalDate NOT NULL, - status TEXT AS HistoricalStatus NOT NULL, - scheduleDeviation REAL AS Float NOT NULL, - timesCompleted REAL AS Float NOT NULL, - FOREIGN KEY(routineId) REFERENCES routineEntity(id) -); - -getHistoryEntriesByIndices: -SELECT * -FROM completionHistoryEntity -WHERE routineId = :routineId - AND date BETWEEN :start AND :end -ORDER BY - date ASC; - -getHistoryEntryByDate: -SELECT * -FROM completionHistoryEntity -WHERE routineId = ? AND date = ?; - -insertHistoryEntry: -INSERT INTO completionHistoryEntity -VALUES (?, ?, ?, ?, ?, ?); - -updateHistoryEntryStatusByDate: -UPDATE completionHistoryEntity -SET status = ?, - scheduleDeviation = ?, - timesCompleted = ? -WHERE routineId = ? AND date = ?; - -updateLastHistoryEntryStatusByStatus: -UPDATE completionHistoryEntity -SET status = :newStatus, - scheduleDeviation = :currentScheduleDeviation -WHERE id = ( - SELECT id - FROM completionHistoryEntity - WHERE routineId = :routineId AND status IN :statusPredicate - ORDER BY date DESC - LIMIT 1 -); - -getLastHistoryEntryByStatus: -SELECT * -FROM completionHistoryEntity -WHERE routineId = :routineId - AND status IN :matchingStatuses - AND (:minDate IS NULL OR :minDate <= date) - AND (:maxDate IS NULL OR date <= :maxDate) -ORDER BY date DESC -LIMIT 1; - -getFirstHistoryEntryByStatus: -SELECT * -FROM completionHistoryEntity -WHERE routineId = :routineId - AND status IN :matchingStatuses - AND (:minDate IS NULL OR :minDate <= date) - AND (:maxDate IS NULL OR date <= :maxDate) -ORDER BY date ASC -LIMIT 1; - -getFirstHistoryEntry: -SELECT * -FROM completionHistoryEntity -WHERE routineId = ? -ORDER BY date ASC -LIMIT 1; - -getLastHistoryEntry: -SELECT * -FROM completionHistoryEntity -WHERE routineId = ? -ORDER BY date DESC -LIMIT 1; - -deleteHistoryEntry: -DELETE FROM completionHistoryEntity -WHERE routineId = ? AND date = ?; - -getTotalTimesCompletedAtTheMomentOfDate: -SELECT TOTAL(timesCompleted) -FROM completionHistoryEntity -WHERE routineId = :routineId AND date >= :startDate AND date <= :endDate; - -getScheduleDeviationAtTheMomentOfDate: -SELECT TOTAL(scheduleDeviation) -FROM completionHistoryEntity -WHERE routineId = :routineId AND date >= :startDate AND date <= :endDate; diff --git a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/routine/routineEntity.sq b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/habit/habitEntity.sq similarity index 59% rename from core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/routine/routineEntity.sq rename to core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/habit/habitEntity.sq index 8bafe605..9d5b8991 100644 --- a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/routine/routineEntity.sq +++ b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/habit/habitEntity.sq @@ -1,10 +1,10 @@ -import com.rendox.routinetracker.core.database.routine.model.RoutineType; +import com.rendox.routinetracker.core.database.habit.model.HabitType; import kotlin.Float; import kotlin.Int; -CREATE TABLE routineEntity ( +CREATE TABLE habitEntity ( id INTEGER NOT NULL PRIMARY KEY, - type TEXT AS RoutineType NOT NULL, + type TEXT AS HabitType NOT NULL, name TEXT NOT NULL, description TEXT, sessionDurationMinutes INTEGER AS Int, @@ -13,16 +13,16 @@ CREATE TABLE routineEntity ( defaultCompletionTimeMinute INTEGER AS Int ); -getRoutineById: +getHabitById: SELECT * -FROM routineEntity +FROM habitEntity WHERE id = ?; -getAllRoutines: +getAllHabits: SELECT * -FROM routineEntity +FROM habitEntity ORDER BY id; -insertRoutine: -INSERT INTO routineEntity +insertHabit: +INSERT INTO habitEntity VALUES (?, ?, ?, ?, ?, ?, ?, ?); \ No newline at end of file diff --git a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/schedule/dueDateEntity.sq b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/schedule/dueDateEntity.sq index cae496cd..fd853317 100644 --- a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/schedule/dueDateEntity.sq +++ b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/schedule/dueDateEntity.sq @@ -1,18 +1,19 @@ import kotlin.Int; CREATE TABLE dueDateEntity ( - id INTEGER NOT NULL PRIMARY KEY, scheduleId INTEGER NOT NULL, dueDateNumber INTEGER AS Int NOT NULL, completionTimeHour INTEGER AS Int, completionTimeMinute INTEGER AS Int, - FOREIGN KEY(scheduleId) REFERENCES scheduleEntity(id) + PRIMARY KEY (scheduleId, dueDateNumber), + FOREIGN KEY (scheduleId) REFERENCES scheduleEntity(id) ); getDueDates: SELECT dueDateNumber FROM dueDateEntity -WHERE scheduleId = ?; +WHERE scheduleId = ? +ORDER BY dueDateNumber ASC; getCompletionTime: SELECT completionTimeHour, completionTimeMinute @@ -26,4 +27,4 @@ WHERE scheduleId = ? AND dueDateNumber = ?; insertDueDate: INSERT INTO dueDateEntity -VALUES (?, ?, ?, ?, ?); \ No newline at end of file +VALUES (?, ?, ?, ?); \ No newline at end of file diff --git a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/schedule/scheduleEntity.sq b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/schedule/scheduleEntity.sq index bc0cf468..c763433b 100644 --- a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/schedule/scheduleEntity.sq +++ b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/schedule/scheduleEntity.sq @@ -1,4 +1,4 @@ -import com.rendox.routinetracker.core.database.routine.model.ScheduleType; +import com.rendox.routinetracker.core.database.habit.model.ScheduleType; import kotlin.Boolean; import kotlin.Int; import kotlinx.datetime.DayOfWeek; @@ -7,20 +7,20 @@ import kotlinx.datetime.LocalDate; CREATE TABLE scheduleEntity ( id INTEGER NOT NULL PRIMARY KEY, type TEXT AS ScheduleType NOT NULL, - routineStartDate INTEGER AS LocalDate NOT NULL, - routineEndDate INTEGER AS LocalDate, + startDate INTEGER AS LocalDate NOT NULL, + endDate INTEGER AS LocalDate, backlogEnabled INTEGER AS Boolean NOT NULL, cancelDuenessIfDoneAhead INTEGER AS Boolean NOT NULL, vacationStartDate INTEGER AS LocalDate, vacationEndDate INTEGER AS LocalDate, startDayOfWeekInWeeklySchedule INTEGER AS DayOfWeek, - startFromRoutineStartInMonthlyAndAnnualSchedule INTEGER AS Boolean, + startFromHabitStartInMonthlyAndAnnualSchedule INTEGER AS Boolean, includeLastDayOfMonthInMonthlySchedule INTEGER AS Boolean, periodicSeparationEnabledInPeriodicSchedule INTEGER AS Boolean, numOfDueDaysInByNumOfDueDaysSchedule INTEGER AS Int, numOfDueDaysInFirstPeriodInByNumOfDueDaysSchedule INTEGER AS Int, numOfDaysInPeriodicCustomSchedule INTEGER AS Int, - FOREIGN KEY(id) REFERENCES routineEntity(id) + FOREIGN KEY (id) REFERENCES habitEntity(id) ); getScheduleById: diff --git a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/completion_time/specificDateCustomCompletionTime.sq b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/specificDateCustomCompletionTime.sq similarity index 86% rename from core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/completion_time/specificDateCustomCompletionTime.sq rename to core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/specificDateCustomCompletionTime.sq index 5d142d8c..55b0d82b 100644 --- a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/completion_time/specificDateCustomCompletionTime.sq +++ b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/specificDateCustomCompletionTime.sq @@ -2,12 +2,12 @@ import kotlin.Int; import kotlinx.datetime.LocalDate; CREATE TABLE specificDateCustomCompletionTime ( - id INTEGER NOT NULL PRIMARY KEY, routineId INTEGER NOT NULL, date INTEGER AS LocalDate NOT NULL, completionTimeHour INTEGER AS Int NOT NULL, completionTimeMinute INTEGER AS Int NOT NULL, - FOREIGN KEY(routineId) REFERENCES routineEntity(id) + PRIMARY KEY (routineId, date), + FOREIGN KEY (routineId) REFERENCES habitEntity(id) ); getCompletionTime: @@ -22,7 +22,7 @@ WHERE routineId = ? AND date = ?; insertCompletiontime: INSERT INTO specificDateCustomCompletionTime -VALUES (?, ?, ?, ?, ?); +VALUES (?, ?, ?, ?); deleteCompletionTime: DELETE FROM specificDateCustomCompletionTime diff --git a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/streak/streakEntity.sq b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/streak/streakEntity.sq deleted file mode 100644 index 4e027d81..00000000 --- a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/streak/streakEntity.sq +++ /dev/null @@ -1,45 +0,0 @@ -import kotlinx.datetime.LocalDate; - -CREATE TABLE streakEntity ( - id INTEGER NOT NULL PRIMARY KEY, - routineId INTEGER NOT NULL, - startDate INTEGER AS LocalDate NOT NULL, - endDate INTEGER AS LocalDate, - FOREIGN KEY(routineId) REFERENCES routineEntity(id) -); - -getAllStreaks: -SELECT * -FROM streakEntity -WHERE routineId = :routineId - AND (:afterDateInclusive IS NULL OR endDate IS NULL OR endDate >= :afterDateInclusive) - AND (:beforeDateInclusive IS NULL OR startDate <= :beforeDateInclusive) -ORDER BY startDate ASC; - -getStreakByDate: -SELECT * -FROM streakEntity -WHERE routineId = :routineId - AND startDate <= :dateWithinStreak - AND (endDate IS NULL OR :dateWithinStreak <= endDate); - -getLastStreak: -SELECT * -FROM streakEntity -WHERE routineId = :routineId -ORDER BY startDate DESC -LIMIT 1; - -insertStreak: -INSERT INTO streakEntity -VALUES (?, ?, ?, ?); - -deleteStreakById: -DELETE FROM streakEntity -WHERE id = ?; - -updateStreakById: -UPDATE streakEntity -SET startDate = ?, - endDate = ? -WHERE id = ?; \ No newline at end of file diff --git a/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/vacationEntity.sq b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/vacationEntity.sq new file mode 100644 index 00000000..3809b452 --- /dev/null +++ b/core/database/src/main/sqldelight/com/rendox/routinetracker/core/database/vacationEntity.sq @@ -0,0 +1,48 @@ +import kotlinx.datetime.LocalDate; + +CREATE TABLE vacationEntity ( + id INTEGER PRIMARY KEY, + habitId INTEGER NOT NULL, + startDate INTEGER AS LocalDate NOT NULL, + endDate INTEGER AS LocalDate, + FOREIGN KEY (habitId) REFERENCES habitEntity(id) +); + +getVacationByDate: +SELECT * +FROM vacationEntity +WHERE habitId = :habitId AND startDate <= :date AND (endDate IS NULL OR :date <= endDate); + +getPreviousVacation: +SELECT * +FROM vacationEntity +WHERE habitId = :habitId + AND (endDate IS NOT NULL) + AND (endDate < :currentDate) +ORDER BY startDate DESC +LIMIT 1; + +getLastVacation: +SELECT * +FROM vacationEntity +WHERE habitId = :habitId +ORDER BY startDate DESC +LIMIT 1; + +getVacationsInPeriod: +SELECT * +FROM vacationEntity +WHERE habitId = :habitId + -- vacation's period is within minDate..maxDate or vacation contains either minDate or maxDate + AND (:minDate IS NULL OR :minDate <= startDate + OR (startDate <= :minDate AND (endDate IS NULL OR :minDate <= endDate))) + AND (:maxDate IS NULL OR (endDate IS NULL AND startDate <= :maxDate) OR (endDate IS NOT NULL AND endDate <= :maxDate) + OR (startDate <= :maxDate AND (endDate IS NULL OR :maxDate <= endDate))); + +insertVacation: +INSERT INTO vacationEntity +VALUES (?, ?, ?, ?); + +deleteVacationById: +DELETE FROM vacationEntity +WHERE id = ?; \ No newline at end of file diff --git a/core/database/src/test/java/com/rendox/routinetracker/core/database/routine/RoutineLocalDataSourceImplTest.kt b/core/database/src/test/java/com/rendox/routinetracker/core/database/habit/HabitLocalDataSourceImplTest.kt similarity index 68% rename from core/database/src/test/java/com/rendox/routinetracker/core/database/routine/RoutineLocalDataSourceImplTest.kt rename to core/database/src/test/java/com/rendox/routinetracker/core/database/habit/HabitLocalDataSourceImplTest.kt index 9e2bfd56..0d85b9cd 100644 --- a/core/database/src/test/java/com/rendox/routinetracker/core/database/routine/RoutineLocalDataSourceImplTest.kt +++ b/core/database/src/test/java/com/rendox/routinetracker/core/database/habit/HabitLocalDataSourceImplTest.kt @@ -1,15 +1,17 @@ -package com.rendox.routinetracker.core.database.routine +package com.rendox.routinetracker.core.database.habit import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.UnconfinedTestDispatcher import com.rendox.routinetracker.core.database.RoutineTrackerDatabase import com.rendox.routinetracker.core.database.di.localDataSourceModule import com.rendox.routinetracker.core.logic.time.AnnualDate import com.rendox.routinetracker.core.logic.time.WeekDayMonthRelated import com.rendox.routinetracker.core.logic.time.WeekDayNumberMonthRelated -import com.rendox.routinetracker.core.model.Routine +import com.rendox.routinetracker.core.model.Habit import com.rendox.routinetracker.core.model.Schedule +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import kotlinx.datetime.DayOfWeek import kotlinx.datetime.LocalDate @@ -24,10 +26,10 @@ import org.koin.dsl.module import org.koin.test.KoinTest import org.koin.test.get -class RoutineLocalDataSourceImplTest : KoinTest { +class HabitLocalDataSourceImplTest : KoinTest { private lateinit var sqlDriver: SqlDriver - private lateinit var routineLocalDataSource: RoutineLocalDataSource + private lateinit var habitLocalDataSource: HabitLocalDataSource private val testModule = module { single { @@ -35,6 +37,7 @@ class RoutineLocalDataSourceImplTest : KoinTest { } } + @OptIn(ExperimentalCoroutinesApi::class) @Before fun setUp() { startKoin { @@ -47,8 +50,8 @@ class RoutineLocalDataSourceImplTest : KoinTest { sqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) RoutineTrackerDatabase.Schema.create(sqlDriver) - routineLocalDataSource = RoutineLocalDataSourceImpl( - db = get(), dispatcher = get() + habitLocalDataSource = HabitLocalDataSourceImpl( + db = get(), dispatcher = UnconfinedTestDispatcher() ) } @@ -59,23 +62,23 @@ class RoutineLocalDataSourceImplTest : KoinTest { @Test fun getInsertYesNoRoutine() = runTest { - val routine = Routine.YesNoRoutine( + val habit = Habit.YesNoHabit( id = 1, name = "Programming", description = "Make my app", sessionDurationMinutes = 120, progress = 0.8f, schedule = Schedule.EveryDaySchedule( - routineStartDate = LocalDate(2023, Month.SEPTEMBER, 1), + startDate = LocalDate(2023, Month.SEPTEMBER, 1), vacationStartDate = LocalDate(2023, Month.SEPTEMBER, 10), vacationEndDate = null, ), defaultCompletionTime = LocalTime(hour = 18, minute = 30), ) - routineLocalDataSource.insertRoutine(routine) - val resultingRoutine = routineLocalDataSource.getRoutineById(routine.id!!) - assertThat(resultingRoutine).isEqualTo(routine) + habitLocalDataSource.insertHabit(habit) + val resultingRoutine = habitLocalDataSource.getHabitById(habit.id!!) + assertThat(resultingRoutine).isEqualTo(habit) } @Test @@ -84,34 +87,34 @@ class RoutineLocalDataSourceImplTest : KoinTest { DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY, - DayOfWeek.SUNDAY, DayOfWeek.SATURDAY, + DayOfWeek.SUNDAY, ) val schedule = Schedule.WeeklyScheduleByDueDaysOfWeek( dueDaysOfWeek = dueDaysOfWeek, startDayOfWeek = DayOfWeek.WEDNESDAY, - routineStartDate = LocalDate(2023, Month.SEPTEMBER, 1), + startDate = LocalDate(2023, Month.SEPTEMBER, 1), vacationStartDate = LocalDate(2023, Month.SEPTEMBER, 10), vacationEndDate = null, backlogEnabled = false, - cancelDuenessIfDoneAhead = false, + completingAheadEnabled = false, ) - val routine = Routine.YesNoRoutine( + val habit = Habit.YesNoHabit( id = 1, name = "Programming", schedule = schedule, ) - routineLocalDataSource.insertRoutine(routine) - val resultingRoutine = routineLocalDataSource.getRoutineById(routine.id!!) - assertThat(resultingRoutine).isEqualTo(routine) + habitLocalDataSource.insertHabit(habit) + val resultingRoutine = habitLocalDataSource.getHabitById(habit.id!!) + assertThat(resultingRoutine).isEqualTo(habit) } @Test fun getInsertRoutineWithPeriodicCustomSchedule() = runTest { - val dueDatesIndices = listOf(1, 3, 4, 5, 18, 31, 30, 21, 8) + val dueDatesIndices = listOf(1, 3, 4, 5, 8, 18, 21, 30, 31) val weekDaysMonthRelated = listOf( WeekDayMonthRelated(DayOfWeek.MONDAY, WeekDayNumberMonthRelated.First), WeekDayMonthRelated(DayOfWeek.WEDNESDAY, WeekDayNumberMonthRelated.First), @@ -123,23 +126,23 @@ class RoutineLocalDataSourceImplTest : KoinTest { dueDatesIndices = dueDatesIndices, includeLastDayOfMonth = true, weekDaysMonthRelated = weekDaysMonthRelated, - startFromRoutineStart = true, - routineStartDate = LocalDate(2023, Month.SEPTEMBER, 1), + startFromHabitStart = true, + startDate = LocalDate(2023, Month.SEPTEMBER, 1), vacationStartDate = LocalDate(2023, Month.SEPTEMBER, 10), vacationEndDate = null, backlogEnabled = false, - cancelDuenessIfDoneAhead = false, + completingAheadEnabled = false, ) - val routine = Routine.YesNoRoutine( + val habit = Habit.YesNoHabit( id = 1, name = "Studying", schedule = schedule, ) - routineLocalDataSource.insertRoutine(routine) - val resultingRoutine = routineLocalDataSource.getRoutineById(routine.id!!) - assertThat(resultingRoutine).isEqualTo(routine) + habitLocalDataSource.insertHabit(habit) + val resultingRoutine = habitLocalDataSource.getHabitById(habit.id!!) + assertThat(resultingRoutine).isEqualTo(habit) } @Test @@ -157,81 +160,81 @@ class RoutineLocalDataSourceImplTest : KoinTest { weekDayNumberMonthRelated = WeekDayNumberMonthRelated.Third, ) ), - startFromRoutineStart = true, - routineStartDate = LocalDate(2023, Month.SEPTEMBER, 1), + startFromHabitStart = true, + startDate = LocalDate(2023, Month.SEPTEMBER, 1), vacationStartDate = LocalDate(2023, Month.SEPTEMBER, 10), vacationEndDate = null, backlogEnabled = false, - cancelDuenessIfDoneAhead = false, + completingAheadEnabled = false, ) - val routine = Routine.YesNoRoutine( + val habit = Habit.YesNoHabit( id = 1, name = "Studying", schedule = schedule, ) - routineLocalDataSource.insertRoutine(routine) - val resultingRoutine = routineLocalDataSource.getRoutineById(routine.id!!) - assertThat(resultingRoutine).isEqualTo(routine) + habitLocalDataSource.insertHabit(habit) + val resultingRoutine = habitLocalDataSource.getHabitById(habit.id!!) + assertThat(resultingRoutine).isEqualTo(habit) } @Test fun getInsertRoutineWithCustomDateSchedule() = runTest { val dueDates = listOf( + LocalDate(2023, Month.JULY, 30), LocalDate(2023, Month.OCTOBER, 4), LocalDate(2023, Month.OCTOBER, 15), - LocalDate(2023, Month.JULY, 30), LocalDate(2024, Month.JANUARY, 1), ) val schedule = Schedule.CustomDateSchedule( dueDates = dueDates, - routineStartDate = LocalDate(2023, Month.SEPTEMBER, 1), + startDate = LocalDate(2023, Month.SEPTEMBER, 1), vacationStartDate = LocalDate(2023, Month.SEPTEMBER, 10), vacationEndDate = null, backlogEnabled = false, - cancelDuenessIfDoneAhead = false, + completingAheadEnabled = false, ) - val routine = Routine.YesNoRoutine( + val habit = Habit.YesNoHabit( id = 1, name = "Studying", schedule = schedule, ) - routineLocalDataSource.insertRoutine(routine) - val resultingRoutine = routineLocalDataSource.getRoutineById(routine.id!!) - assertThat(resultingRoutine).isEqualTo(routine) + habitLocalDataSource.insertHabit(habit) + val resultingRoutine = habitLocalDataSource.getHabitById(habit.id!!) + assertThat(resultingRoutine).isEqualTo(habit) } @Test fun getInsertRoutineWithAnnualSchedule() = runTest { val dueDates = listOf( AnnualDate(Month.JANUARY, 1), - AnnualDate(Month.FEBRUARY, 29), AnnualDate(Month.MAY, 25), AnnualDate(Month.SEPTEMBER, 30), + AnnualDate(Month.FEBRUARY, 29), ) val schedule = Schedule.AnnualScheduleByDueDates( dueDates = dueDates, - routineStartDate = LocalDate(2023, Month.SEPTEMBER, 1), + startDate = LocalDate(2023, Month.SEPTEMBER, 1), vacationStartDate = LocalDate(2023, Month.SEPTEMBER, 10), vacationEndDate = null, backlogEnabled = false, - cancelDuenessIfDoneAhead = false, - startFromRoutineStart = false, + completingAheadEnabled = false, + startFromHabitStart = false, ) - val routine = Routine.YesNoRoutine( + val habit = Habit.YesNoHabit( id = 1, name = "Studying", schedule = schedule, ) - routineLocalDataSource.insertRoutine(routine) - val resultingRoutine = routineLocalDataSource.getRoutineById(routine.id!!) - assertThat(resultingRoutine).isEqualTo(routine) + habitLocalDataSource.insertHabit(habit) + val resultingRoutine = habitLocalDataSource.getHabitById(habit.id!!) + assertThat(resultingRoutine).isEqualTo(habit) } } \ No newline at end of file diff --git a/core/database/src/test/java/com/rendox/routinetracker/core/database/habit/VacationLocalDataSourceImplTest.kt b/core/database/src/test/java/com/rendox/routinetracker/core/database/habit/VacationLocalDataSourceImplTest.kt new file mode 100644 index 00000000..16a6f0d2 --- /dev/null +++ b/core/database/src/test/java/com/rendox/routinetracker/core/database/habit/VacationLocalDataSourceImplTest.kt @@ -0,0 +1,149 @@ +package com.rendox.routinetracker.core.database.habit + +import com.google.common.truth.Truth.assertThat +import app.cash.sqldelight.db.SqlDriver +import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver +import com.rendox.routinetracker.core.database.RoutineTrackerDatabase +import com.rendox.routinetracker.core.database.di.localDataSourceModule +import com.rendox.routinetracker.core.database.vacation.VacationLocalDataSource +import com.rendox.routinetracker.core.database.vacation.VacationLocalDataSourceImpl +import com.rendox.routinetracker.core.model.Vacation +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.LocalDate +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module +import org.koin.test.KoinTest +import org.koin.test.get + +class VacationLocalDataSourceImplTest : KoinTest { + private lateinit var sqlDriver: SqlDriver + private lateinit var vacationLocalDataSource: VacationLocalDataSource + + private val testModule = module { + single { + sqlDriver + } + } + + private val habitId = 1L + private val vacationsList = listOf( + Vacation( + id = 1, + startDate = LocalDate(2023, 1, 1), + endDate = LocalDate(2023, 1, 3), + ), + Vacation( + id = 2, + startDate = LocalDate(2023, 1, 5), + endDate = LocalDate(2023, 1, 11), + ), + Vacation( + id = 3, + startDate = LocalDate(2023, 1, 15), + endDate = LocalDate(2023, 1, 20), + ), + Vacation( + id = 4, + startDate = LocalDate(2023, 5, 25), + endDate = LocalDate(2023, 5, 30), + ), + Vacation( + id = 5, + startDate = LocalDate(2023, 6, 1), + endDate = LocalDate(2023, 6, 8), + ), + Vacation( + id = 6, + startDate = LocalDate(2023, 9, 30), + endDate = LocalDate(2023, 9, 30), + ), + Vacation( + id = 7, + startDate = LocalDate(2023, 10, 1), + endDate = LocalDate(2023, 11, 1), + ), + Vacation( + id = 8, + startDate = LocalDate(2023, 11, 20), + endDate = null, + ), + ) + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() = runTest { + startKoin { + modules( + localDataSourceModule, + testModule, + ) + } + + sqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) + RoutineTrackerDatabase.Schema.create(sqlDriver) + + vacationLocalDataSource = VacationLocalDataSourceImpl( + db = get(), dispatcher = UnconfinedTestDispatcher() + ) + + for (vacation in vacationsList) { + vacationLocalDataSource.insertVacation( + habitId = habitId, + vacation = vacation, + ) + } + } + + @After + fun tearDown() { + stopKoin() + } + + @Test + fun `assert get vacations in period returns vacations within date range`() = runTest { + val resultingVacations = vacationLocalDataSource.getVacationsInPeriod( + habitId = habitId, + minDate = LocalDate(2023, 1, 13), + maxDate = LocalDate(2023, 6, 10), + ) + val expectedVacations = vacationsList.filter { it.id in 3L..5L } + assertThat(resultingVacations).containsExactlyElementsIn(expectedVacations) + } + + @Test + fun `assert get vacations in period returns vacations that contain max and min dates`() = runTest { + val resultingVacations = vacationLocalDataSource.getVacationsInPeriod( + habitId = habitId, + minDate = LocalDate(2023, 1, 10), + maxDate = LocalDate(2023, 11, 20), + ) + val expectedVacations = vacationsList.filter { it.id!! >= 2L } + assertThat(resultingVacations).containsExactlyElementsIn(expectedVacations) + } + + @Test + fun `assert get vacation by date returns vacation that contains date`() = runTest { + val resultingVacation = vacationLocalDataSource.getVacationByDate( + habitId = habitId, + date = LocalDate(2023, 1, 10), + ) + val expectedVacation = vacationsList.find { it.id == 2L } + assertThat(resultingVacation).isEqualTo(expectedVacation) + } + + @Test + fun `assert previous vacation returns vacation that ends before current date`() = runTest { + val resultingVacation = vacationLocalDataSource.getPreviousVacation( + habitId = habitId, + currentDate = LocalDate(2023, 9, 30), + ) + val expectedVacation = vacationsList.find { it.id == 5L } + assertThat(resultingVacation).isEqualTo(expectedVacation) + } +} \ No newline at end of file diff --git a/core/database/src/test/java/com/rendox/routinetracker/core/database/routine/CompletionHistoryLocalDataSourceImplTest.kt b/core/database/src/test/java/com/rendox/routinetracker/core/database/routine/CompletionHistoryLocalDataSourceImplTest.kt deleted file mode 100644 index 98e53985..00000000 --- a/core/database/src/test/java/com/rendox/routinetracker/core/database/routine/CompletionHistoryLocalDataSourceImplTest.kt +++ /dev/null @@ -1,356 +0,0 @@ -package com.rendox.routinetracker.core.database.routine - -import app.cash.sqldelight.db.SqlDriver -import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver -import com.google.common.truth.Truth.assertThat -import com.rendox.routinetracker.core.database.RoutineTrackerDatabase -import com.rendox.routinetracker.core.database.completion_history.CompletionHistoryLocalDataSourceImpl -import com.rendox.routinetracker.core.database.di.localDataSourceModule -import com.rendox.routinetracker.core.logic.time.epochDate -import com.rendox.routinetracker.core.logic.time.generateRandomDateRange -import com.rendox.routinetracker.core.logic.time.plusDays -import com.rendox.routinetracker.core.logic.time.rangeTo -import com.rendox.routinetracker.core.model.CompletionHistoryEntry -import com.rendox.routinetracker.core.model.HistoricalStatus -import kotlinx.coroutines.test.runTest -import kotlinx.datetime.LocalDate -import kotlinx.datetime.Month -import kotlinx.datetime.daysUntil -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import org.koin.dsl.module -import org.koin.test.KoinTest -import org.koin.test.get -import kotlin.random.Random - -class CompletionHistoryLocalDataSourceImplTest : KoinTest { - private lateinit var sqlDriver: SqlDriver - private lateinit var completionHistoryLocalDataSource: CompletionHistoryLocalDataSourceImpl - private lateinit var completionHistory: List - private val routineId = 1L - private val startDate = LocalDate(2023, Month.OCTOBER, 1) - private lateinit var endDate: LocalDate - - private val testModule = module { - single { - sqlDriver - } - } - - @Before - fun setUp() = runTest { - startKoin { - modules( - localDataSourceModule, - testModule, - ) - } - - sqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) - RoutineTrackerDatabase.Schema.create(sqlDriver) - - completionHistoryLocalDataSource = CompletionHistoryLocalDataSourceImpl( - db = get(), dispatcher = get() - ) - - val history = mutableListOf() - var dateCounter = startDate - - for (status in HistoricalStatus.values().toList()) { - val scheduleDeviation = when (status) { - HistoricalStatus.NotCompleted -> -1F - HistoricalStatus.Completed -> 0F - HistoricalStatus.OverCompleted -> 1F - HistoricalStatus.OverCompletedOnVacation -> 1F - HistoricalStatus.SortedOutBacklogOnVacation -> 1F - HistoricalStatus.Skipped -> 0F - HistoricalStatus.NotCompletedOnVacation -> 0F - HistoricalStatus.CompletedLater -> 0F - HistoricalStatus.SortedOutBacklog -> 1F - HistoricalStatus.AlreadyCompleted -> 0F - } - val timesCompleted = when (status) { - HistoricalStatus.Completed, - HistoricalStatus.OverCompleted, - HistoricalStatus.OverCompletedOnVacation, - HistoricalStatus.SortedOutBacklog, - HistoricalStatus.SortedOutBacklogOnVacation -> 1F - else -> 0F - } - repeat(50) { - dateCounter = dateCounter.plusDays(1) - history.add( - CompletionHistoryEntry( - date = dateCounter, - status = status, - scheduleDeviation = scheduleDeviation, - timesCompleted = timesCompleted, - ) - ) - } - } - - history.shuffle() - history.forEachIndexed { index, entry -> - history[index] = entry.copy(date = startDate.plusDays(index)) - } - - completionHistory = history - endDate = dateCounter - - for (historyEntry in history) { - completionHistoryLocalDataSource.insertHistoryEntry( - routineId = routineId, - entry = historyEntry, - ) - } - } - - @After - fun tearDown() { - stopKoin() - } - - @Test - fun assertAllHistoryIsEqualToExpected() = runTest { - val wholeHistory = completionHistoryLocalDataSource.getHistoryEntries( - routineId = routineId, - dates = startDate..startDate.plusDays(completionHistory.lastIndex), - ) - assertThat(wholeHistory).isEqualTo(completionHistory) - } - - @Test - fun assertReturnsCorrectEntriesForDateRange() = runTest { - val randomDateRange = generateRandomDateRange( - minDate = startDate, - maxDate = endDate, - ) - val randomDateRangeIndices = - startDate.daysUntil(randomDateRange.start)..startDate.daysUntil(randomDateRange.endInclusive) - val randomPeriodInHistory = completionHistoryLocalDataSource.getHistoryEntries( - routineId = routineId, - dates = randomDateRange, - ) - val expectedPeriodInHistory = completionHistory.slice(randomDateRangeIndices) - assertThat(randomPeriodInHistory).isEqualTo(expectedPeriodInHistory) - } - - @Test - fun assertReturnsCorrectEntryForSingleDate() = runTest { - val randomDate = startDate.plusDays(Random.nextInt(completionHistory.size)) - val singleDateRange = randomDate..randomDate - assertThat( - completionHistoryLocalDataSource.getHistoryEntries( - routineId = routineId, - dates = singleDateRange, - ) - ).isEqualTo(listOf(completionHistory[startDate.daysUntil(randomDate)])) - } - - @Test - fun assertReturnsCorrectFirstHistoryEntry() = runTest { - assertThat( - completionHistoryLocalDataSource.getFirstHistoryEntry(routineId) - ).isEqualTo(completionHistory.firstOrNull()) - } - - @Test - fun assertReturnsCorrectLastHistoryEntry() = runTest { - assertThat( - completionHistoryLocalDataSource.getLastHistoryEntry(routineId) - ).isEqualTo(completionHistory.lastOrNull()) - } - - @Test - fun assertReturnsCorrectLastHistoryEntryDateByStatus() = runTest { - val desiredStatuses = listOf(HistoricalStatus.NotCompleted, HistoricalStatus.Skipped) - assertThat( - completionHistoryLocalDataSource.getLastHistoryEntryByStatus( - routineId = routineId, - matchingStatuses = desiredStatuses, - ) - ).isEqualTo(completionHistory.findLast { it.status in desiredStatuses }) - } - - @Test - fun assertInsertsCompletedLaterStatusesIntoSeparateTableOnInsert() = runTest { - val completedLaterEntries = completionHistory.filter { - it.status == HistoricalStatus.CompletedLater - } - - for (entry in completedLaterEntries) { - assertThat( - completionHistoryLocalDataSource.checkIfStatusWasCompletedLater( - routineId = routineId, - date = entry.date, - ) - ).isTrue() - } - } - - @Test - fun assertInsertsCompletedLaterStatusesIntoSeparateTableOnUpdate() = runTest { - val notCompletedLaterEntry = completionHistory.find { - it.status != HistoricalStatus.CompletedLater - }!! - - assertThat( - completionHistoryLocalDataSource.checkIfStatusWasCompletedLater( - routineId = routineId, - date = notCompletedLaterEntry.date, - ) - ).isFalse() - - completionHistoryLocalDataSource.updateHistoryEntryByDate( - routineId = routineId, - date = notCompletedLaterEntry.date, - newStatus = HistoricalStatus.CompletedLater, - newScheduleDeviation = notCompletedLaterEntry.scheduleDeviation, - newTimesCompleted = notCompletedLaterEntry.timesCompleted, - ) - - - assertThat( - completionHistoryLocalDataSource.checkIfStatusWasCompletedLater( - routineId = routineId, - date = notCompletedLaterEntry.date, - ) - ).isTrue() - } - - @Test - fun assertDoesNotRecordOtherEntriesExceptCompletedLaterIntoSeparateTable() = runTest { - val notCompletedLaterEntries = completionHistory.filter { - it.status != HistoricalStatus.CompletedLater - } - - for (entry in notCompletedLaterEntries) { - assertThat( - completionHistoryLocalDataSource.checkIfStatusWasCompletedLater( - routineId = routineId, - date = entry.date, - ) - ).isFalse() - } - } - - @Test - fun assertDeletesHistoryEntries() = runTest { - val randomEntryIndex = Random.nextInt(completionHistory.size) - val randomEntry = completionHistory[randomEntryIndex] - completionHistoryLocalDataSource.deleteHistoryEntry( - routineId = routineId, - date = randomEntry.date, - ) - val newHistory = completionHistory.toMutableList().apply { removeAt(randomEntryIndex) } - assertThat( - completionHistoryLocalDataSource.getHistoryEntries( - routineId = routineId, - dates = startDate..startDate.plusDays(completionHistory.lastIndex), - ) - ).isEqualTo(newHistory) - } - - @Test - fun assertDoesNotDeleteCompletedLaterBackupEntriesOnStatusUpdate() = runTest { - val completedLaterStatus = completionHistory.find { - it.status == HistoricalStatus.CompletedLater - }!! - completionHistoryLocalDataSource.updateHistoryEntryByDate( - routineId = routineId, - date = completedLaterStatus.date, - newStatus = HistoricalStatus.NotCompleted, - newScheduleDeviation = -1F, - newTimesCompleted = 0F, - ) - assertThat( - completionHistoryLocalDataSource.checkIfStatusWasCompletedLater( - routineId = routineId, - date = completedLaterStatus.date, - ) - ).isTrue() - } - - @Test - fun assertDoesNotDeleteCompletedLaterBackupEntriesOnStatusDelete() = runTest { - val completedLaterStatus = completionHistory.find { - it.status == HistoricalStatus.CompletedLater - }!! - completionHistoryLocalDataSource.deleteHistoryEntry( - routineId = routineId, - date = completedLaterStatus.date, - ) - assertThat( - completionHistoryLocalDataSource.checkIfStatusWasCompletedLater( - routineId = routineId, - date = completedLaterStatus.date, - ) - ).isTrue() - } - - @Test - fun assertTotalTimesCompletedInPeriodReturnsCorrectValue() = runTest { - val datePeriod = generateRandomDateRange( - minDate = completionHistory.first().date, - maxDate = completionHistory.last().date, - ) - val expectedValue = completionHistory - .filter { it.date >= datePeriod.start && it.date <= datePeriod.endInclusive } - .map { it.timesCompleted } - .sum().toDouble() - assertThat( - completionHistoryLocalDataSource.getTotalTimesCompletedInPeriod( - routineId = routineId, - startDate = datePeriod.start, - endDate = datePeriod.endInclusive, - ) - ).isEqualTo(expectedValue) - } - - @Test - fun assertTotalTimesCompletedInPeriodReturnsZeroIfPeriodIsNotPresent() = runTest { - val datePeriod = epochDate..epochDate - assertThat( - completionHistoryLocalDataSource.getScheduleDeviationInPeriod( - routineId = routineId, - startDate = datePeriod.start, - endDate = datePeriod.endInclusive, - ) - ).isZero() - } - - @Test - fun assertScheduleDeviationInPeriodReturnsCorrectValue() = runTest { - val datePeriod = generateRandomDateRange( - minDate = completionHistory.first().date, - maxDate = completionHistory.last().date, - ) - val expectedValue = completionHistory - .filter { it.date >= datePeriod.start && it.date <= datePeriod.endInclusive } - .map { it.scheduleDeviation } - .sum().toDouble() - assertThat( - completionHistoryLocalDataSource.getScheduleDeviationInPeriod( - routineId = routineId, - startDate = datePeriod.start, - endDate = datePeriod.endInclusive, - ) - ).isEqualTo(expectedValue) - } - - @Test - fun assertScheduleDeviationInPeriodReturnsZeroIfPeriodIsNotPresent() = runTest { - val datePeriod = epochDate..epochDate - assertThat( - completionHistoryLocalDataSource.getScheduleDeviationInPeriod( - routineId = routineId, - startDate = datePeriod.start, - endDate = datePeriod.endInclusive, - ) - ).isZero() - } -} \ No newline at end of file diff --git a/core/database/src/test/java/com/rendox/routinetracker/core/database/routine/StreakLocalDataSourceImplTest.kt b/core/database/src/test/java/com/rendox/routinetracker/core/database/routine/StreakLocalDataSourceImplTest.kt deleted file mode 100644 index 74240c79..00000000 --- a/core/database/src/test/java/com/rendox/routinetracker/core/database/routine/StreakLocalDataSourceImplTest.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.rendox.routinetracker.core.database.routine - -import com.google.common.truth.Truth.assertThat -import app.cash.sqldelight.db.SqlDriver -import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver -import com.rendox.routinetracker.core.database.RoutineTrackerDatabase -import com.rendox.routinetracker.core.database.di.localDataSourceModule -import com.rendox.routinetracker.core.database.streak.StreakLocalDataSource -import com.rendox.routinetracker.core.database.streak.StreakLocalDataSourceImpl -import com.rendox.routinetracker.core.model.Streak -import kotlinx.coroutines.test.runTest -import kotlinx.datetime.LocalDate -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import org.koin.dsl.module -import org.koin.test.KoinTest -import org.koin.test.get - -class StreakLocalDataSourceImplTest : KoinTest { - - private lateinit var sqlDriver: SqlDriver - private lateinit var streakLocalDataSource: StreakLocalDataSource - private val routineId = 1L - - private val testModule = module { - single { - sqlDriver - } - } - - private val initialStreaks = listOf( - Streak( - id = 1, - startDate = LocalDate(2023, 1, 5), - endDate = LocalDate(2023, 1, 7), - ), - Streak( - id = 2, - startDate = LocalDate(2023, 2, 8), - endDate = LocalDate(2023, 2, 15), - ), - Streak( - id = 3, - startDate = LocalDate(2023, 3, 1), - endDate = LocalDate(2023, 3, 5), - ), - Streak( - id = 4, - startDate = LocalDate(2023, 4, 1), - endDate = LocalDate(2023, 4, 10), - ), - Streak( - id = 5, - startDate = LocalDate(2023, 5, 25), - endDate = null, - ), - ) - - @Before - fun setUp() = runTest { - startKoin { - modules( - localDataSourceModule, - testModule, - ) - } - - sqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY) - RoutineTrackerDatabase.Schema.create(sqlDriver) - - streakLocalDataSource = StreakLocalDataSourceImpl( - db = get(), dispatcher = get() - ) - for (streak in initialStreaks) { - streakLocalDataSource.insertStreak(streak, routineId) - } - } - - @After - fun tearDown() { - stopKoin() - } - - @Test - fun getAllStreaksWithoutParametersAssertReturnsAllStreaksTest() = runTest { - assertThat( - streakLocalDataSource.getAllStreaks(routineId = routineId) - ).containsExactlyElementsIn(initialStreaks).inOrder() - } - - @Test - fun getAllStreaksAfterSomeDateTest() = runTest { - assertThat( - streakLocalDataSource.getAllStreaks( - routineId = routineId, - afterDateInclusive = LocalDate(2023, 3, 4), - ) - ).containsExactlyElementsIn(initialStreaks.drop(2)).inOrder() - } - - @Test - fun getAllStreaksBeforeSomeDateTest() = runTest { - assertThat( - streakLocalDataSource.getAllStreaks( - routineId = routineId, - beforeDateInclusive = LocalDate(2023, 4, 6), - ) - ).containsExactlyElementsIn(initialStreaks.take(4)).inOrder() - } - - @Test - fun getAllStreaksRangeBiggerThanStreaksTest() = runTest { - assertThat( - streakLocalDataSource.getAllStreaks( - routineId = routineId, - afterDateInclusive = LocalDate(2022, 1, 1), - beforeDateInclusive = LocalDate(2024, 1, 1), - ) - ).containsExactlyElementsIn(initialStreaks).inOrder() - } - - @Test - fun getFinishedStreakByDateTest() = runTest { - assertThat( - streakLocalDataSource.getStreakByDate( - routineId = routineId, - dateWithinStreak = LocalDate(2023, 4, 7), - ) - ).isEqualTo(initialStreaks[3]) - } - - @Test - fun getNotFinishedStreakByDateTest() = runTest { - assertThat( - streakLocalDataSource.getStreakByDate( - routineId = routineId, - dateWithinStreak = LocalDate(2023, 6, 1), - ) - ).isEqualTo(initialStreaks.last()) - } -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/HabitComputeStatusUseCase.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/HabitComputeStatusUseCase.kt new file mode 100644 index 00000000..fad2565f --- /dev/null +++ b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/HabitComputeStatusUseCase.kt @@ -0,0 +1,431 @@ +package com.rendox.routinetracker.core.domain.completion_history + +import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository +import com.rendox.routinetracker.core.data.habit.HabitRepository +import com.rendox.routinetracker.core.data.vacation.VacationRepository +import com.rendox.routinetracker.core.logic.time.LocalDateRange +import com.rendox.routinetracker.core.logic.time.plusDays +import com.rendox.routinetracker.core.logic.time.rangeTo +import com.rendox.routinetracker.core.model.Habit +import com.rendox.routinetracker.core.model.HabitStatus +import com.rendox.routinetracker.core.model.Schedule +import com.rendox.routinetracker.core.model.Vacation +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.LocalDate +import kotlinx.datetime.minus +import kotlin.coroutines.CoroutineContext + + +/** + * The HabitComputeStatusUseCase class is responsible for computing the [HabitStatus] based on + * various factors such as the habit's schedule, what dates are completed, and vacation periods. + * + * The result also depends on whether the validation date is in the past or in the future. For + * example, when the habit has some backlog, but the validation date is in the past, the invoke + * function will return not [HabitStatus.Backlog], but [HabitStatus.Skipped] instead. That's because + * the user deliberately chose to skip the habit on that day. Nonetheless, if the validation date is + * in the future, the invoke function will return [HabitStatus.Backlog] so that the user can adjust + * their schedule to sort out this backlog later. + * + * Note that today is considered to be in the future. + * + * When the habit is on vacation, it's considered to be not due even if it's planned on schedule. + * During the vacation, the user can still completed the habit, which will either sort out the + * backlog (if any is present), or will complete the habit ahead. + * + * Each date depends on whether other dates are completed or not. For example, when one date has + * status [HabitStatus.OverCompleted], the user will be able to skip the next planned date, which + * will have already completed status. In the case of the previous example, with + * backlog, the next over completed date will have status [HabitStatus.SortedOutBacklog]. + * + * Backlog and completing ahead can be disabled by toggling the [Habit]'s [Schedule]'s properties. + * + * Period separation can be enabled or disabled as well. If enabled, the schedule deviation that + * indicates backlog and how many times the habit was completed ahead will be reset at the start + * of each period. + * + * @see [HabitStatus] for more details on what each status means, and when it is returned. + */ +class HabitComputeStatusUseCase( + private val habitRepository: HabitRepository, + private val vacationRepository: VacationRepository, + private val completionHistoryRepository: CompletionHistoryRepository, + private val dispatcher: CoroutineContext = Dispatchers.Default, +) { + + suspend operator fun invoke( + habitId: Long, + validationDate: LocalDate, + today: LocalDate, + ): HabitStatus = withContext(dispatcher) { + val habit = habitRepository.getHabitById(habitId) + + if (validationDate < habit.schedule.startDate) return@withContext HabitStatus.NotStarted + habit.schedule.endDate?.let { if (validationDate > it) return@withContext HabitStatus.Finished } + + val completedToday = completionHistoryRepository.getRecordByDate(habit.id!!, today) != null + val scheduleDeviation = computeScheduleDeviation( + habit = habit, + currentDate = validationDate, + today = today, + completedToday = completedToday, + ) + + val numOfTimesCompletedOnValidationDate = completionHistoryRepository.getRecordByDate( + habitId = habit.id!!, + date = validationDate, + )?.numOfTimesCompleted ?: 0f + + val habitIsOnVacationAtTheMomentOfValidationDate = vacationRepository.getVacationByDate( + habitId = habit.id!!, + date = validationDate, + ) != null + + val numOfDueTimesOnValidationDate = habit.getNumOfDueTimesOnDate( + date = validationDate, habitIsOnVacation = habitIsOnVacationAtTheMomentOfValidationDate + ) + + val validationDateIsDue = numOfDueTimesOnValidationDate > 0f + if (validationDateIsDue) { + val completedStatus = deriveCompletedStatusWhenPlanned( + habit = habit, + scheduleDeviation = scheduleDeviation, + numOfTimesCompletedOnValidationDate = numOfTimesCompletedOnValidationDate, + numOfDueTimesOnValidationDate = numOfDueTimesOnValidationDate, + ) + if (completedStatus != null) return@withContext completedStatus + + println("validationDate $validationDate is due: $validationDate") + val alreadyCompleted = checkIfIsAlreadyCompleted( + habit, scheduleDeviation, numOfDueTimesOnValidationDate, validationDate, today + ) + if (alreadyCompleted && validationDate < today) return@withContext HabitStatus.PastDateAlreadyCompleted + if (alreadyCompleted && validationDate >= today) return@withContext HabitStatus.FutureDateAlreadyCompleted + + if (validationDate < today) { + val wasCompletedLater = checkIfWasCompletedLater( + currentDate = validationDate, + numOfDueTimesOnCurrentDate = numOfDueTimesOnValidationDate, + habit = habit, + ) + if (wasCompletedLater) return@withContext HabitStatus.CompletedLater + } + + return@withContext if (validationDate < today) HabitStatus.Failed else HabitStatus.Planned + } else { + val backlogStatus = deriveBacklogStatus( + habit, + scheduleDeviation, + numOfTimesCompletedOnValidationDate, + validationDate, + today, + completedToday = completedToday, + ) + if (backlogStatus != null) return@withContext backlogStatus + + if (numOfTimesCompletedOnValidationDate > 0f) { + return@withContext HabitStatus.OverCompleted + } + if (habitIsOnVacationAtTheMomentOfValidationDate) return@withContext HabitStatus.OnVacation + return@withContext if (validationDate < today) HabitStatus.Skipped else HabitStatus.NotDue + } + } + + private fun deriveCompletedStatusWhenPlanned( + habit: Habit, + scheduleDeviation: Double, + numOfTimesCompletedOnValidationDate: Float, + numOfDueTimesOnValidationDate: Float, + ): HabitStatus? = when { + numOfTimesCompletedOnValidationDate == numOfDueTimesOnValidationDate -> + HabitStatus.Completed + + numOfTimesCompletedOnValidationDate > numOfDueTimesOnValidationDate -> { + if (scheduleDeviation < 0.0 && habit.schedule.backlogEnabled) { + HabitStatus.SortedOutBacklog + } else { + HabitStatus.OverCompleted + } + } + + numOfTimesCompletedOnValidationDate > 0f -> HabitStatus.PartiallyCompleted + else -> null + } + + private suspend fun deriveBacklogStatus( + habit: Habit, + scheduleDeviation: Double, + numOfTimesCompletedOnValidationDate: Float, + validationDate: LocalDate, + today: LocalDate, + completedToday: Boolean, + ): HabitStatus? { + if (scheduleDeviation < 0.0 && habit.schedule.backlogEnabled) { + val numOfNotDueTimes = + if (validationDate >= today) { + val startDate = if (completedToday) today.plusDays(1) else today + getNumOfNotDueTimesInPeriod( + habit = habit, + period = startDate..validationDate, + ) + } else { + 0.0 + } + if (scheduleDeviation <= -numOfNotDueTimes) { + if (numOfTimesCompletedOnValidationDate > 0f) { + return HabitStatus.SortedOutBacklog + } + if (validationDate >= today) { + return HabitStatus.Backlog + } + } + } + return null + } + + private suspend fun getNumOfNotDueTimesInPeriod( + habit: Habit, + period: LocalDateRange, + ): Double { + var numOfNotDueTimesInPeriod = 0.0 + + val defaultNumOfDueTimesOnDate = when (habit) { + is Habit.YesNoHabit -> 1F + } + + val vacations: List = vacationRepository.getVacationsInPeriod( + habitId = habit.id!!, + minDate = period.start, + maxDate = period.endInclusive, + ) + + for (date in period) { + val habitIsOnVacation = vacations.any { it.containsDate(date) } + val habitIsDue = if (habitIsOnVacation) { + false + } else { + habit.schedule.isDue(validationDate = date) + } + if (!habitIsDue) numOfNotDueTimesInPeriod += defaultNumOfDueTimesOnDate + } + + return numOfNotDueTimesInPeriod + } + + private suspend fun checkIfIsAlreadyCompleted( + habit: Habit, + scheduleDeviation: Double, + numOfDueTimesOnValidationDate: Float, + validationDate: LocalDate, + today: LocalDate, + ): Boolean { + if (!habit.schedule.completingAheadEnabled) return false + + if (habit.schedule.backlogEnabled) { + val numOfDueTimes = + if (validationDate >= today) { + getNumOfDueTimesInPeriod( + habit = habit, + period = today..validationDate, + ) + } else { + numOfDueTimesOnValidationDate.toDouble() + } + if (scheduleDeviation >= numOfDueTimes) { + return true + } + } else { + // when backlog is disabled, there may be a situation when the schedule deviation is + // negative and the user can neither sort out the backlog nor complete ahead; the + // following code is required to fix this bug + val schedule = habit.schedule + val currentDatePeriod: LocalDateRange? = + if (schedule is Schedule.PeriodicSchedule && schedule.periodSeparationEnabled) { + schedule.getPeriodRange(currentDate = validationDate) + } else { + null + } + + val firstDateInPeriod = currentDatePeriod?.start + val firstDateToLookFor = + completionHistoryRepository.getFirstCompletedRecord( + habitId = habit.id!!, + minDate = firstDateInPeriod, + maxDate = validationDate.minus(DatePeriod(days = 1)), + )?.date ?: return false + val lastDateToLookFor = validationDate.minus(DatePeriod(days = 1)) + + val completionRecords: List = + completionHistoryRepository.getRecordsInPeriod( + habitId = habit.id!!, + minDate = firstDateToLookFor, + maxDate = lastDateToLookFor, + ) + val vacations: List = vacationRepository.getVacationsInPeriod( + habitId = habit.id!!, + minDate = firstDateToLookFor, + maxDate = lastDateToLookFor, + ) + + var numOfDueTimes = 0.0 + var numOfTimesCompleted = 0.0 + + var date = lastDateToLookFor + while (date >= firstDateToLookFor) { + numOfDueTimes += habit.getNumOfDueTimesOnDate( + date = date, + habitIsOnVacation = vacations.any { it.containsDate(date) }, + ) + numOfTimesCompleted += + completionRecords.find { it.date == date }?.numOfTimesCompleted ?: 0f + + if (numOfTimesCompleted - numOfDueTimes >= numOfDueTimesOnValidationDate) { + return true + } + + date = date.minus(DatePeriod(days = 1)) + } + } + return false + } + + /** + * @return positive value if the habit is ahead of schedule (completed even more than planned), + * negative if behind (there is some backlog), 0 if on schedule + */ + private suspend fun computeScheduleDeviation( + habit: Habit, + today: LocalDate, + currentDate: LocalDate, + completedToday: Boolean, + ): Double { + val actualDate = if (currentDate <= today) { + currentDate.minus(DatePeriod(days = 1)) + } else { + if (completedToday) { + today + } else { + today.minus(DatePeriod(days = 1)) + } + } + + val schedule = habit.schedule + val period = + if (schedule is Schedule.PeriodicSchedule && schedule.periodSeparationEnabled) { + val lastPeriod = schedule.getPeriodRange(currentDate = actualDate) + if (lastPeriod == null || currentDate !in lastPeriod) return 0.0 + lastPeriod.start..actualDate + } else { + schedule.startDate..actualDate + } + + val numOfTimesCompleted = completionHistoryRepository.getNumOfTimesCompletedInPeriod( + habitId = habit.id!!, + minDate = period.start, + maxDate = period.endInclusive, + ) + val numOfDueTimes = getNumOfDueTimesInPeriod(habit, period) + return numOfTimesCompleted - numOfDueTimes + } + + /** + * @return true if the habit wasn't completed on the date it was planned and introduced a + * backlog that was sorted out later + */ + private suspend fun checkIfWasCompletedLater( + habit: Habit, + currentDate: LocalDate, + numOfDueTimesOnCurrentDate: Float, + ): Boolean { + if (!habit.schedule.backlogEnabled) return false + + val schedule = habit.schedule + val currentDatePeriod: LocalDateRange? = + if (schedule is Schedule.PeriodicSchedule && schedule.periodSeparationEnabled) { + schedule.getPeriodRange(currentDate = currentDate) + } else { + null + } + + val lastCompletedDate = + completionHistoryRepository.getLastCompletedRecord(habit.id!!)?.date ?: return false + val lastDateInPeriod = currentDatePeriod?.endInclusive + + val firstDateToLookFor = currentDate.plusDays(1) + val lastDateToLookFor = + if (lastDateInPeriod != null && lastDateInPeriod < lastCompletedDate) { + lastDateInPeriod + } else { + lastCompletedDate + } + + val completionRecords: List = + completionHistoryRepository.getRecordsInPeriod( + habitId = habit.id!!, + minDate = firstDateToLookFor, + maxDate = lastDateToLookFor, + ) + val vacations: List = vacationRepository.getVacationsInPeriod( + habitId = habit.id!!, + minDate = firstDateToLookFor, + maxDate = lastDateToLookFor, + ) + + var numOfDueTimes = 0.0 + var numOfTimesCompleted = 0.0 + + for (date in firstDateToLookFor..lastDateToLookFor) { + numOfDueTimes += habit.getNumOfDueTimesOnDate( + date = date, + habitIsOnVacation = vacations.any { it.containsDate(date) }, + ) + numOfTimesCompleted += + completionRecords.find { it.date == date }?.numOfTimesCompleted ?: 0f + + val scheduleDeviation = numOfTimesCompleted - numOfDueTimes + if (scheduleDeviation >= numOfDueTimesOnCurrentDate) return true + } + return false + } + + private suspend fun getNumOfDueTimesInPeriod( + habit: Habit, + period: LocalDateRange, + ): Double { + var numOfDueTimesInPeriod = 0.0 + + val vacationsInPeriod = vacationRepository.getVacationsInPeriod( + habitId = habit.id!!, + minDate = period.start, + maxDate = period.endInclusive, + ) + + for (date in period) { + numOfDueTimesInPeriod += habit.getNumOfDueTimesOnDate( + date = date, + habitIsOnVacation = vacationsInPeriod.any { it.containsDate(date) }, + ) + } + + return numOfDueTimesInPeriod + } + + private fun Habit.getNumOfDueTimesOnDate( + date: LocalDate, + habitIsOnVacation: Boolean, + ): Float { + val numOfDueTimes = when (this) { + is Habit.YesNoHabit -> { + val dueOnSchedule = if (habitIsOnVacation) { + false + } else { + schedule.isDue(validationDate = date) + } + if (dueOnSchedule) 1f else 0f + } + } + return numOfDueTimes + } +} diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/InsertHabitCompletionUseCase.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/InsertHabitCompletionUseCase.kt new file mode 100644 index 00000000..e419aca3 --- /dev/null +++ b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/InsertHabitCompletionUseCase.kt @@ -0,0 +1,44 @@ +package com.rendox.routinetracker.core.domain.completion_history + +import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository +import com.rendox.routinetracker.core.data.habit.HabitRepository +import com.rendox.routinetracker.core.model.Habit +import kotlinx.datetime.LocalDate + +class InsertHabitCompletionUseCase( + private val completionHistoryRepository: CompletionHistoryRepository, + private val habitRepository: HabitRepository, +) { + /** + * Inserts a completion record for a habit. + * If the number of times completed is 0, the completion record is deleted. + * + * @throws IllegalDateException if the [completionRecord]'s date is earlier than the habit's + * start date or later than the habit's end date (if it exists), or if the date is later + * than today. + */ + suspend operator fun invoke( + habitId: Long, + completionRecord: Habit.CompletionRecord, + today: LocalDate, + ) { + val habit = habitRepository.getHabitById(habitId) + if (completionRecord.date > today) throw IllegalDateException() + if (completionRecord.date < habit.schedule.startDate) throw IllegalDateException() + habit.schedule.endDate?.let { if (completionRecord.date > it) throw IllegalDateException() } + + if (completionRecord.numOfTimesCompleted > 0F) { + completionHistoryRepository.insertCompletion( + habitId = habitId, + completionRecord = completionRecord, + ) + } else { + completionHistoryRepository.deleteCompletionByDate( + habitId = habitId, + date = completionRecord.date, + ) + } + } + + class IllegalDateException : Exception() +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/RoutineComputePlanningStatus.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/RoutineComputePlanningStatus.kt deleted file mode 100644 index 6888e42c..00000000 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/RoutineComputePlanningStatus.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.rendox.routinetracker.core.domain.completion_history - -import com.rendox.routinetracker.core.logic.time.plusDays -import com.rendox.routinetracker.core.logic.time.rangeTo -import com.rendox.routinetracker.core.model.PlanningStatus -import com.rendox.routinetracker.core.model.Routine -import kotlinx.datetime.LocalDate - -fun Routine.computePlanningStatus( - validationDate: LocalDate, - currentScheduleDeviation: Double, - actualDate: LocalDate?, - numOfTimesCompletedInCurrentPeriod: Double, - scheduleDeviationInCurrentPeriod: Double?, - lastVacationEndDate: LocalDate?, -): PlanningStatus? { - if (validationDate < schedule.routineStartDate) return null - schedule.routineEndDate?.let { if (validationDate > it) return null } - if (isCurrentlyOnVacation(validationDate)) return PlanningStatus.OnVacation - - return when (this) { - is Routine.YesNoRoutine -> yesNoRoutineComputePlanningStatus( - validationDate, - currentScheduleDeviation, - actualDate, - numOfTimesCompletedInCurrentPeriod, - scheduleDeviationInCurrentPeriod, - lastVacationEndDate, - ) - } -} - -private fun Routine.isCurrentlyOnVacation(validationDate: LocalDate): Boolean { - schedule.vacationStartDate?.let { - if (validationDate < it) return false - } ?: return false - schedule.vacationEndDate?.let { - return validationDate <= it - } - return true -} - -private fun Routine.YesNoRoutine.yesNoRoutineComputePlanningStatus( - validationDate: LocalDate, - currentScheduleDeviation: Double, - actualDate: LocalDate?, - numOfTimesCompletedInCurrentPeriod: Double, - scheduleDeviationInCurrentPeriod: Double?, - lastVacationEndDate: LocalDate?, - ): PlanningStatus { - println("RoutineComputePlanningStatus: validation date = $validationDate") -// if ( -// schedule.isDue( -// validationDate, -// actualDate, -// numOfTimesCompletedInCurrentPeriod, -// scheduleDeviationInCurrentPeriod, -// lastVacationEndDate, -// ) -// ) { -// println("RoutineComputePlanningStatus: currentScheduleDeviation = $currentScheduleDeviation") -// println("RoutineComputePlanningStatus: cancelDuenessIfDoneAhead = ${schedule.cancelDuenessIfDoneAhead}") -// if (currentScheduleDeviation > 0 && schedule.cancelDuenessIfDoneAhead) { -// actualDate?.let { -// var dueDaysCounter = 0 -// for (day in it.plusDays(1)..validationDate) { -// if ( -// schedule.isDue( -// validationDate, -// it, -// numOfTimesCompletedInCurrentPeriod, -// scheduleDeviationInCurrentPeriod -// ) -// ) dueDaysCounter++ -// } -// println("due days counter = $dueDaysCounter") -// if (currentScheduleDeviation >= dueDaysCounter) -// return PlanningStatus.AlreadyCompleted -// } -// } - -// return PlanningStatus.Planned -// } -// -// println("RoutineComputePlanningStatus: backlog enabled = ${schedule.backlogEnabled}") -// println() -// if (currentScheduleDeviation < 0 && schedule.backlogEnabled) { -// actualDate?.let { -// var notDueDaysCounter = 0 -// for (day in it.plusDays(1)..validationDate) { -// if ( -// !schedule.isDue( -// day, -// it, -// numOfTimesCompletedInCurrentPeriod, -// scheduleDeviationInCurrentPeriod -// ) -// ) notDueDaysCounter++ -// } -// if (currentScheduleDeviation <= -notDueDaysCounter) { -// return PlanningStatus.Backlog -// } -// } -// } -// -// return PlanningStatus.NotDue - TODO() -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleGetPeriodRange.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleGetPeriodRange.kt index e871ca2b..7797a42b 100644 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleGetPeriodRange.kt +++ b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleGetPeriodRange.kt @@ -22,11 +22,15 @@ import kotlinx.datetime.yearsUntil fun Schedule.PeriodicSchedule.getPeriodRange( currentDate: LocalDate, lastVacationEndDate: LocalDate? = null, -): LocalDateRange { - if (currentDate < routineStartDate) { - throw IllegalArgumentException( - "This function shouldn't be called for dates that are prior to routine start date." - ) +): LocalDateRange? { + if (currentDate < startDate) { + return null + } + + endDate?.let { + if (currentDate > it) { + return null + } } val periodRange = when (this) { @@ -36,13 +40,12 @@ fun Schedule.PeriodicSchedule.getPeriodRange( is Schedule.MonthlyScheduleByNumOfDueDays -> monthlyScheduleGetPeriodDateRange(currentDate) is Schedule.AnnualScheduleByDueDates -> annualScheduleGetPeriodDateRange(currentDate) is Schedule.AnnualScheduleByNumOfDueDays -> annualScheduleGetPeriodDateRange(currentDate) - is Schedule.PeriodicCustomSchedule -> periodicCustomScheduleGetPeriodDateRange( + is Schedule.AlternateDaysSchedule -> periodicCustomScheduleGetPeriodDateRange( currentDate, lastVacationEndDate ) } - routineEndDate?.let { - if (periodRange.start > it) throw IllegalArgumentException() + endDate?.let { if (periodRange.endInclusive > it) { return periodRange.copy(endInclusive = it) } @@ -60,12 +63,12 @@ private fun Schedule.WeeklySchedule.weeklyScheduleGetPeriodDateRange( val startFromRoutineStart = startDayOfWeek == null if (startFromRoutineStart) { val unit = DateTimeUnit.WEEK - val numberOfPeriodsAlreadyPassed = routineStartDate.until(currentDate, unit) - startPeriodDate = routineStartDate.plus(numberOfPeriodsAlreadyPassed, unit) + val numberOfPeriodsAlreadyPassed = startDate.until(currentDate, unit) + startPeriodDate = startDate.plus(numberOfPeriodsAlreadyPassed, unit) endPeriodDate = atEndOfPeriod(startPeriodDate, correspondingPeriod) } else { startPeriodDate = currentDate - while (startPeriodDate.dayOfWeek != startDayOfWeek && startPeriodDate != routineStartDate) { + while (startPeriodDate.dayOfWeek != startDayOfWeek && startPeriodDate != startDate) { startPeriodDate = startPeriodDate.minus(DatePeriod(days = 1)) } val endDateDayOfWeekIndex = startDayOfWeek!!.value - 1 @@ -88,14 +91,14 @@ private fun Schedule.MonthlySchedule.monthlyScheduleGetPeriodDateRange( val startPeriodDate: LocalDate val endPeriodDate: LocalDate - if (startFromRoutineStart) { - val numberOfPeriodsAlreadyPassed = routineStartDate.monthsUntil(currentDate) - startPeriodDate = routineStartDate.plus(DatePeriod(months = numberOfPeriodsAlreadyPassed)) + if (startFromHabitStart) { + val numberOfPeriodsAlreadyPassed = startDate.monthsUntil(currentDate) + startPeriodDate = startDate.plus(DatePeriod(months = numberOfPeriodsAlreadyPassed)) endPeriodDate = atEndOfPeriod(startPeriodDate, correspondingPeriod) } else { - val stillFirstMonth = currentDate <= routineStartDate.atEndOfMonth + val stillFirstMonth = currentDate <= startDate.atEndOfMonth startPeriodDate = if (stillFirstMonth) { - routineStartDate + startDate } else { currentDate.withDayOfMonth(1) } @@ -110,22 +113,22 @@ private fun Schedule.AnnualSchedule.annualScheduleGetPeriodDateRange( var startPeriodDate: LocalDate var endPeriodDate: LocalDate - if (startFromRoutineStart) { - val numberOfPeriodsAlreadyPassed = routineStartDate.yearsUntil(currentDate) - startPeriodDate = routineStartDate.plus(DatePeriod(years = numberOfPeriodsAlreadyPassed)) + if (startFromHabitStart) { + val numberOfPeriodsAlreadyPassed = startDate.yearsUntil(currentDate) + startPeriodDate = startDate.plus(DatePeriod(years = numberOfPeriodsAlreadyPassed)) endPeriodDate = atEndOfPeriod(startPeriodDate, correspondingPeriod) // plus and until functions don't work as expected with february 29 - if (routineStartDate.month == Month.FEBRUARY && routineStartDate.dayOfMonth == 29) { + if (startDate.month == Month.FEBRUARY && startDate.dayOfMonth == 29) { if (numberOfPeriodsAlreadyPassed > 0) { startPeriodDate = startPeriodDate.plusDays(1) } endPeriodDate = endPeriodDate.plusDays(1) } } else { - val stillFirstYear = currentDate <= routineStartDate.atEndOfYear + val stillFirstYear = currentDate <= startDate.atEndOfYear startPeriodDate = if (stillFirstYear) { - routineStartDate + startDate } else { LocalDate(currentDate.year, Month.JANUARY, 1) } @@ -134,7 +137,7 @@ private fun Schedule.AnnualSchedule.annualScheduleGetPeriodDateRange( return startPeriodDate..endPeriodDate } -private fun Schedule.PeriodicCustomSchedule.periodicCustomScheduleGetPeriodDateRange( +private fun Schedule.AlternateDaysSchedule.periodicCustomScheduleGetPeriodDateRange( currentDate: LocalDate, lastVacationEndDate: LocalDate?, ): LocalDateRange { @@ -147,7 +150,7 @@ private fun Schedule.PeriodicCustomSchedule.periodicCustomScheduleGetPeriodDateR val scheduleStartDate = if (lastVacationEndDate != null && lastVacationEndDate <= currentDate) { lastVacationEndDate.plusDays(1) } else { - routineStartDate + startDate } var startPeriodDateIndex = scheduleStartDate.daysUntil(currentDate) while (startPeriodDateIndex % correspondingPeriod.days != 0) { diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleIsDue.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleIsDue.kt index 2aaf2caf..8e8289bc 100644 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleIsDue.kt +++ b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleIsDue.kt @@ -9,34 +9,29 @@ import kotlinx.datetime.daysUntil fun Schedule.isDue( validationDate: LocalDate, lastVacationEndDate: LocalDate? = null, -): Boolean { - if (validationDate < routineStartDate) return false - routineEndDate?.let { if (validationDate > it) return false } - - return when (this) { - is Schedule.EveryDaySchedule -> true +): Boolean = when (this) { + is Schedule.EveryDaySchedule -> true - is Schedule.WeeklyScheduleByDueDaysOfWeek -> weeklyScheduleByDueDaysOfWeekIsDue( - validationDate = validationDate - ) + is Schedule.ByNumOfDueDays -> scheduleByNumOfDueDaysIsDue( + validationDate = validationDate, + lastVacationEndDate = lastVacationEndDate, + ) - is Schedule.ByNumOfDueDays -> scheduleByNumOfDueDaysIsDue( - validationDate = validationDate, - lastVacationEndDate = lastVacationEndDate, - ) + is Schedule.WeeklyScheduleByDueDaysOfWeek -> weeklyScheduleByDueDaysOfWeekIsDue( + validationDate = validationDate + ) - is Schedule.MonthlyScheduleByDueDatesIndices -> monthlyScheduleIsDue( - validationDate = validationDate - ) + is Schedule.MonthlyScheduleByDueDatesIndices -> monthlyScheduleIsDue( + validationDate = validationDate + ) - is Schedule.CustomDateSchedule -> customDateScheduleIsDue( - validationDate = validationDate - ) + is Schedule.CustomDateSchedule -> customDateScheduleIsDue( + validationDate = validationDate + ) - is Schedule.AnnualScheduleByDueDates -> annualScheduleIsDue( - validationDate = validationDate - ) - } + is Schedule.AnnualScheduleByDueDates -> annualScheduleIsDue( + validationDate = validationDate + ) } private fun Schedule.WeeklyScheduleByDueDaysOfWeek.weeklyScheduleByDueDaysOfWeekIsDue( @@ -75,7 +70,10 @@ private fun Schedule.ByNumOfDueDays.scheduleByNumOfDueDaysIsDue( val validationDatePeriod = (this as Schedule.PeriodicSchedule).getPeriodRange( currentDate = validationDate, lastVacationEndDate = lastVacationEndDate, - ) + ) ?: return false val validationDateNumber = validationDatePeriod.start.daysUntil(validationDate) + 1 - return validationDateNumber <= getNumOfDueDatesInPeriod(validationDatePeriod) + val scheduleStartDate = (this as Schedule).startDate + val numOfDueDays = + getNumOfDueDates(getForFirstPeriod = scheduleStartDate in validationDatePeriod) + return validationDateNumber <= numOfDueDays } \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/use_cases/CompletionHistoryCommon.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/use_cases/CompletionHistoryCommon.kt deleted file mode 100644 index d64e04db..00000000 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/use_cases/CompletionHistoryCommon.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.rendox.routinetracker.core.domain.completion_history.use_cases - -import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository -import com.rendox.routinetracker.core.domain.streak.StartStreakOrJoinStreaksUseCase -import com.rendox.routinetracker.core.model.HistoricalStatus -import com.rendox.routinetracker.core.model.Routine -import kotlinx.datetime.DatePeriod -import kotlinx.datetime.LocalDate -import kotlinx.datetime.minus - -suspend fun sortOutBacklog( - routine: Routine, - completionHistoryRepository: CompletionHistoryRepository, - startStreakOrJoinStreaks: StartStreakOrJoinStreaksUseCase, - currentDate: LocalDate -) { - val lastNotCompleted = completionHistoryRepository.getLastHistoryEntryByStatus( - routineId = routine.id!!, - matchingStatuses = listOf(HistoricalStatus.NotCompleted), - maxDate = currentDate.minus(DatePeriod(days = 1)), - )!! - - startStreakOrJoinStreaks( - routine = routine, - date = lastNotCompleted.date, - ) - - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routine.id!!, - date = lastNotCompleted.date, - newStatus = HistoricalStatus.CompletedLater, - newScheduleDeviation = lastNotCompleted.scheduleDeviation, - newTimesCompleted = lastNotCompleted.timesCompleted, - ) -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/use_cases/GetRoutineStatusUseCase.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/use_cases/GetRoutineStatusUseCase.kt deleted file mode 100644 index b9007d7d..00000000 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/use_cases/GetRoutineStatusUseCase.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.rendox.routinetracker.core.domain.completion_history.use_cases - -import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository -import com.rendox.routinetracker.core.data.routine.RoutineRepository -import com.rendox.routinetracker.core.domain.completion_history.computePlanningStatus -import com.rendox.routinetracker.core.domain.completion_history.getPeriodRange -import com.rendox.routinetracker.core.logic.time.LocalDateRange -import com.rendox.routinetracker.core.logic.time.plusDays -import com.rendox.routinetracker.core.logic.time.rangeTo -import com.rendox.routinetracker.core.model.Routine -import com.rendox.routinetracker.core.model.RoutineStatus -import com.rendox.routinetracker.core.model.Schedule -import com.rendox.routinetracker.core.model.StatusEntry -import com.rendox.routinetracker.core.model.onVacationHistoricalStatuses -import com.rendox.routinetracker.core.model.toStatusEntry -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.datetime.DatePeriod -import kotlinx.datetime.LocalDate -import kotlinx.datetime.minus - -class GetRoutineStatusUseCase( - private val routineRepository: RoutineRepository, - private val completionHistoryRepository: CompletionHistoryRepository, - private val insertRoutineStatus: InsertRoutineStatusUseCase, -) { - suspend operator fun invoke( - routineId: Long, date: LocalDate, today: LocalDate - ): RoutineStatus? = invoke(routineId, date..date, today).firstOrNull()?.status - - suspend operator fun invoke( - routineId: Long, - dates: LocalDateRange, - today: LocalDate, - ): List { - println("get routine status") - return withContext(Dispatchers.Default) { - val routine: Routine = routineRepository.getRoutineById(routineId) - prepopulateHistoryWithMissingDates(routine, today) - - val resultingStatusList = mutableListOf() - - val completionHistoryPart = - completionHistoryRepository.getHistoryEntries(routineId, dates) - resultingStatusList.addAll(completionHistoryPart.map { it.toStatusEntry() }) - - if (completionHistoryPart.isEmpty()) { - resultingStatusList.addAll(computeFutureStatuses(dates, routine)) - } else if (completionHistoryPart.last().date < dates.endInclusive) { - val startDate = completionHistoryPart.last().date.plusDays(1) - val endDate = dates.endInclusive - resultingStatusList.addAll(computeFutureStatuses(startDate..endDate, routine)) - } - resultingStatusList - } - } - - private suspend fun prepopulateHistoryWithMissingDates(routine: Routine, today: LocalDate) { - val yesterday = today.minus(DatePeriod(days = 1)) - val lastDateInHistory = completionHistoryRepository.getLastHistoryEntry(routine.id!!)?.date - - val routineEndDate: LocalDate? = routine.schedule.routineEndDate - if (routineEndDate != null && yesterday > routineEndDate) return - - if (lastDateInHistory == null && routine.schedule.routineStartDate <= yesterday) { - for (date in routine.schedule.routineStartDate..yesterday) { - insertRoutineStatus( - routineId = routine.id!!, - currentDate = date, - completedOnCurrentDate = false, - today = today, - ) - } - return - } - - if (lastDateInHistory != null && lastDateInHistory < yesterday) { - for (date in lastDateInHistory.plusDays(1)..yesterday) { - insertRoutineStatus( - routineId = routine.id!!, - currentDate = date, - completedOnCurrentDate = false, - today = today, - ) - } - return - } - } - - private suspend fun computeFutureStatuses( - dateRange: LocalDateRange, - routine: Routine, - ): List { - val lastVacationStatus = completionHistoryRepository.getLastHistoryEntryByStatus( - routineId = routine.id!!, - matchingStatuses = onVacationHistoricalStatuses, - ) - - val statusList = mutableListOf() - val lastHistoryEntry = completionHistoryRepository.getLastHistoryEntry(routine.id!!) - val schedule = routine.schedule - val lastPeriod: LocalDateRange? = lastHistoryEntry?.let { - if (schedule is Schedule.PeriodicSchedule) schedule.getPeriodRange( - currentDate = it.date, - lastVacationEndDate = lastVacationStatus?.date, - ) - else null - } - val numOfTimesCompletedInLastPeriodAtTheMomentOfLastHistoryEntryDate = - lastHistoryEntry?.let { - completionHistoryRepository.getTotalTimesCompletedInPeriod( - routineId = routine.id!!, - startDate = lastPeriod?.start ?: routine.schedule.routineStartDate, - endDate = it.date, - ) - } ?: 0.0 - val periodSeparationEnabled = - schedule is Schedule.PeriodicSchedule && schedule.periodSeparationEnabled - val scheduleDeviationAtTheMomentOfLastHistoryEntryDate = - completionHistoryRepository.getScheduleDeviationInPeriod( - routineId = routine.id!!, - startDate = if (periodSeparationEnabled && lastPeriod != null) { - lastPeriod.start - } else { - schedule.routineStartDate - }, - endDate = dateRange.start, - ) - val scheduleDeviationInCurrentPeriod = lastPeriod?.let { - completionHistoryRepository.getScheduleDeviationInPeriod( - routineId = routine.id!!, - startDate = it.start, - endDate = dateRange.start, - ) - } - - for (date in dateRange) { - routine.computePlanningStatus( - validationDate = date, - currentScheduleDeviation = scheduleDeviationAtTheMomentOfLastHistoryEntryDate, - actualDate = lastHistoryEntry?.date, - numOfTimesCompletedInCurrentPeriod = numOfTimesCompletedInLastPeriodAtTheMomentOfLastHistoryEntryDate, - scheduleDeviationInCurrentPeriod = scheduleDeviationInCurrentPeriod, - lastVacationEndDate = lastVacationStatus?.date, - )?.let { - statusList.add(StatusEntry( - date = date, - status = it, - )) - } - } - return statusList - } -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/use_cases/InsertRoutineStatusUseCase.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/use_cases/InsertRoutineStatusUseCase.kt deleted file mode 100644 index c920c48a..00000000 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/use_cases/InsertRoutineStatusUseCase.kt +++ /dev/null @@ -1,326 +0,0 @@ -package com.rendox.routinetracker.core.domain.completion_history.use_cases - -import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository -import com.rendox.routinetracker.core.data.routine.RoutineRepository -import com.rendox.routinetracker.core.domain.completion_history.computePlanningStatus -import com.rendox.routinetracker.core.domain.completion_history.getPeriodRange -import com.rendox.routinetracker.core.domain.streak.BreakStreakUseCase -import com.rendox.routinetracker.core.domain.streak.StartStreakOrJoinStreaksUseCase -import com.rendox.routinetracker.core.logic.time.LocalDateRange -import com.rendox.routinetracker.core.model.CompletionHistoryEntry -import com.rendox.routinetracker.core.model.HistoricalStatus -import com.rendox.routinetracker.core.model.PlanningStatus -import com.rendox.routinetracker.core.model.Routine -import com.rendox.routinetracker.core.model.Schedule -import com.rendox.routinetracker.core.model.onVacationHistoricalStatuses -import kotlinx.datetime.LocalDate -import kotlinx.datetime.daysUntil - -class InsertRoutineStatusUseCase( - private val completionHistoryRepository: CompletionHistoryRepository, - private val routineRepository: RoutineRepository, - private val startStreakOrJoinStreaks: StartStreakOrJoinStreaksUseCase, - private val breakStreak: BreakStreakUseCase, -) { - - suspend operator fun invoke( - routineId: Long, - currentDate: LocalDate, - completedOnCurrentDate: Boolean, - today: LocalDate, - ) { - if (currentDate > today) return - - when (val routine = routineRepository.getRoutineById(routineId)) { - is Routine.YesNoRoutine -> insertYesNoRoutineStatus( - routine, currentDate, completedOnCurrentDate - ) - } - } - - private suspend fun insertYesNoRoutineStatus( - routine: Routine.YesNoRoutine, - currentDate: LocalDate, - completedOnCurrentDate: Boolean, - ) { - val lastVacationStatus = completionHistoryRepository.getLastHistoryEntryByStatus( - routineId = routine.id!!, - matchingStatuses = onVacationHistoricalStatuses, - ) - - val schedule = routine.schedule - val currentPeriod: LocalDateRange? = - if (schedule is Schedule.PeriodicSchedule) schedule.getPeriodRange( - currentDate = currentDate, lastVacationEndDate = lastVacationStatus?.date - ) - else null - val lastHistoryEntry = completionHistoryRepository.getLastHistoryEntry(routine.id!!) - val numOfTimesCompletedInCurrentPeriod = - completionHistoryRepository.getTotalTimesCompletedInPeriod( - routineId = routine.id!!, - startDate = currentPeriod?.start ?: routine.schedule.routineStartDate, - endDate = currentDate, - ) - val periodSeparationEnabled = - schedule is Schedule.PeriodicSchedule && schedule.periodSeparationEnabled - - val startDate = if (periodSeparationEnabled && currentPeriod != null) { - currentPeriod.start - } else { - schedule.routineStartDate - } - - val currentScheduleDeviation = - completionHistoryRepository.getScheduleDeviationInPeriod( - routineId = routine.id!!, - startDate = startDate, - endDate = currentDate, - ) - - val scheduleDeviationInCurrentPeriod = currentPeriod?.let { - completionHistoryRepository.getScheduleDeviationInPeriod( - routineId = routine.id!!, - startDate = it.start, - endDate = currentDate, - ) - } - - val planningStatus = routine.computePlanningStatus( - validationDate = currentDate, - currentScheduleDeviation = currentScheduleDeviation, - actualDate = lastHistoryEntry?.date, - numOfTimesCompletedInCurrentPeriod = numOfTimesCompletedInCurrentPeriod, - scheduleDeviationInCurrentPeriod = scheduleDeviationInCurrentPeriod, - lastVacationEndDate = lastVacationStatus?.date, - )!! - - val historicalStatusData = when (planningStatus) { - PlanningStatus.Planned -> deriveHistoricalStatusFromPlannedStatus( - routine, - routine.schedule, - completedOnCurrentDate, - currentDate, - routine.schedule.backlogEnabled, - numOfTimesCompletedInCurrentPeriod, - ) - - PlanningStatus.Backlog -> deriveHistoricalStatusFromBacklogStatus( - completedOnCurrentDate, routine, currentDate - ) - - PlanningStatus.AlreadyCompleted -> deriveHistoricalStatusFromAlreadyCompletedStatus( - completedOnCurrentDate - ) - - PlanningStatus.NotDue -> deriveHistoricalStatusFromNotDueStatus( - routine, - currentDate, - completedOnCurrentDate, - routine.schedule.cancelDuenessIfDoneAhead, - ) - - PlanningStatus.OnVacation -> - deriveHistoricalStatusFromOnVacationStatus( - completed = completedOnCurrentDate, - routine = routine, - currentScheduleDeviation = currentScheduleDeviation, - cancelDuenessIfDoneAhead = routine.schedule.cancelDuenessIfDoneAhead, - currentDate = currentDate, - ) - } - - completionHistoryRepository.insertHistoryEntry( - routineId = routine.id!!, - entry = CompletionHistoryEntry( - date = currentDate, - status = historicalStatusData.historicalStatus, - scheduleDeviation = historicalStatusData.scheduleDeviation, - timesCompleted = if (completedOnCurrentDate) 1F else 0F, - ), - ) - } - - private suspend fun deriveHistoricalStatusFromPlannedStatus( - routine: Routine, - schedule: Schedule, - completed: Boolean, - currentDate: LocalDate, - backlogEnabled: Boolean, - timesCompletedInCurrentPeriod: Double, - ): HistoricalStatusData = if (completed) { - startStreakOrJoinStreaks( - routine = routine, - date = currentDate, - ) - HistoricalStatusData( - scheduleDeviation = 0F, - historicalStatus = HistoricalStatus.Completed, - ) - } else { - if (schedule is Schedule.ByNumOfDueDays) { - val lastVacationStatus = completionHistoryRepository.getLastHistoryEntryByStatus( - routineId = routine.id!!, - matchingStatuses = onVacationHistoricalStatuses, - ) - - val validationDatePeriod = (schedule as Schedule.PeriodicSchedule).getPeriodRange( - currentDate = currentDate, - lastVacationEndDate = lastVacationStatus?.date, - ) - - val numOfDueDays = - schedule.getNumOfDueDatesInPeriod(validationDatePeriod) - - val numOfDaysThatRemainToBeCompletedInPeriod = - numOfDueDays - timesCompletedInCurrentPeriod - val daysRemainingInCurrentPeriod = - currentDate.daysUntil(validationDatePeriod.endInclusive) + 1 // including today - val thereAreEnoughDaysInPeriodToCompleteLater = - numOfDaysThatRemainToBeCompletedInPeriod < (daysRemainingInCurrentPeriod.toDouble()) - - if (thereAreEnoughDaysInPeriodToCompleteLater) { - HistoricalStatusData( - scheduleDeviation = 0F, - historicalStatus = HistoricalStatus.Skipped, - ) - } else { - breakStreak( - routineId = routine.id!!, - date = currentDate, - ) - HistoricalStatusData( - scheduleDeviation = if (backlogEnabled) -1F else 0F, - historicalStatus = HistoricalStatus.NotCompleted, - ) - } - } else { - breakStreak( - routineId = routine.id!!, - date = currentDate, - ) - HistoricalStatusData( - scheduleDeviation = if (backlogEnabled) -1F else 0F, - historicalStatus = HistoricalStatus.NotCompleted, - ) - } - } - - private suspend fun deriveHistoricalStatusFromBacklogStatus( - completed: Boolean, routineId: Routine, currentDate: LocalDate - ): HistoricalStatusData = if (completed) { - sortOutBacklog( - routine = routineId, - completionHistoryRepository = completionHistoryRepository, - startStreakOrJoinStreaks = startStreakOrJoinStreaks, - currentDate = currentDate, - ) - HistoricalStatusData( - scheduleDeviation = 1F, - historicalStatus = HistoricalStatus.SortedOutBacklog, - ) - } else { - HistoricalStatusData( - scheduleDeviation = 0F, - historicalStatus = HistoricalStatus.Skipped, - ) - } - - private fun deriveHistoricalStatusFromAlreadyCompletedStatus( - completed: Boolean - ): HistoricalStatusData = if (completed) { - HistoricalStatusData( - scheduleDeviation = 0F, - historicalStatus = HistoricalStatus.Completed - ) - } else { - HistoricalStatusData( - scheduleDeviation = -1F, - historicalStatus = HistoricalStatus.AlreadyCompleted - ) - } - - private suspend fun deriveHistoricalStatusFromNotDueStatus( - routine: Routine, - currentDate: LocalDate, - completed: Boolean, - cancelDuenessIfDoneAhead: Boolean, - ): HistoricalStatusData = if (completed) { - startStreakOrJoinStreaks( - routine = routine, - date = currentDate, - ) - HistoricalStatusData( - scheduleDeviation = if (cancelDuenessIfDoneAhead) 1F else 0F, - historicalStatus = HistoricalStatus.OverCompleted, - ) - } else { - HistoricalStatusData( - scheduleDeviation = 0F, - historicalStatus = HistoricalStatus.Skipped, - ) - } - - private suspend fun deriveHistoricalStatusFromOnVacationStatus( - completed: Boolean, - routine: Routine, - currentScheduleDeviation: Double, - cancelDuenessIfDoneAhead: Boolean, - currentDate: LocalDate, - ): HistoricalStatusData = if (completed) { - if (currentScheduleDeviation < 0) { - sortOutBacklog( - routine = routine, - completionHistoryRepository = completionHistoryRepository, - startStreakOrJoinStreaks = startStreakOrJoinStreaks, - currentDate = currentDate, - ) - HistoricalStatusData( - scheduleDeviation = 1F, - historicalStatus = HistoricalStatus.SortedOutBacklogOnVacation, - ) - } else { - HistoricalStatusData( - scheduleDeviation = if (cancelDuenessIfDoneAhead) 1F else 0F, - historicalStatus = HistoricalStatus.OverCompletedOnVacation, - ) - } - } else { - HistoricalStatusData( - scheduleDeviation = 0F, - historicalStatus = HistoricalStatus.NotCompletedOnVacation, - ) - } - - private data class HistoricalStatusData( - val scheduleDeviation: Float, - val historicalStatus: HistoricalStatus, - ) - -// private suspend fun startStreak(routine: Routine, currentDate: LocalDate) { -// val currentStreakIsStillLasting = -// streakRepository.getLastStreak(routine.id!!)?.let { it.end == null } ?: false -// if (!currentStreakIsStillLasting) { -// val lastNotCompleted = completionHistoryRepository.getLastHistoryEntryByStatus( -// routineId = routine.id!!, -// matchingStatuses = listOf(HistoricalStatus.NotCompleted), -// ) -// val streakStart = -// if (lastNotCompleted == null) routine.schedule.routineStartDate else currentDate -// -// streakRepository.insertStreak(routine.id!!, start = streakStart, end = null) -// } -// } - -// private suspend fun endStreak(routineId: Long, currentDate: LocalDate) { -// val existingStreak: Streak? = streakRepository.getLastStreak(routineId)?.let { -// if (it.end == null) it else null -// } -// existingStreak?.let { -// streakRepository.updateStreakById( -// id = it.id!!, -// start = it.start, -// end = currentDate.minus(DatePeriod(days = 1)), -// ) -// } -// } -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/use_cases/ToggleHistoricalStatusUseCase.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/use_cases/ToggleHistoricalStatusUseCase.kt deleted file mode 100644 index 3cd9cc65..00000000 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_history/use_cases/ToggleHistoricalStatusUseCase.kt +++ /dev/null @@ -1,494 +0,0 @@ -package com.rendox.routinetracker.core.domain.completion_history.use_cases - -import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository -import com.rendox.routinetracker.core.data.routine.RoutineRepository -import com.rendox.routinetracker.core.domain.completion_history.computePlanningStatus -import com.rendox.routinetracker.core.domain.completion_history.getPeriodRange -import com.rendox.routinetracker.core.domain.streak.BreakStreakUseCase -import com.rendox.routinetracker.core.domain.streak.ContinueStreakIfEndedUseCase -import com.rendox.routinetracker.core.domain.streak.StartStreakOrJoinStreaksUseCase -import com.rendox.routinetracker.core.domain.streak.DeleteStreakIfStartedUseCase -import com.rendox.routinetracker.core.logic.time.LocalDateRange -import com.rendox.routinetracker.core.logic.time.plusDays -import com.rendox.routinetracker.core.logic.time.rangeTo -import com.rendox.routinetracker.core.model.CompletionHistoryEntry -import com.rendox.routinetracker.core.model.HistoricalStatus -import com.rendox.routinetracker.core.model.PlanningStatus -import com.rendox.routinetracker.core.model.Routine -import com.rendox.routinetracker.core.model.Schedule -import com.rendox.routinetracker.core.model.onVacationHistoricalStatuses -import com.rendox.routinetracker.core.model.overCompletedStatuses -import com.rendox.routinetracker.core.model.sortedOutBacklogStatuses -import kotlinx.datetime.DatePeriod -import kotlinx.datetime.LocalDate -import kotlinx.datetime.minus - -class ToggleHistoricalStatusUseCase( - private val completionHistoryRepository: CompletionHistoryRepository, - private val routineRepository: RoutineRepository, - private val startStreakOrJoinStreaks: StartStreakOrJoinStreaksUseCase, - private val breakStreak: BreakStreakUseCase, - private val deleteStreakIfStarted: DeleteStreakIfStartedUseCase, - private val continueStreakIfEnded: ContinueStreakIfEndedUseCase -) { - private var currentPeriod: LocalDateRange? = null - private var lastVacationStatus: CompletionHistoryEntry? = null - - suspend operator fun invoke( - routineId: Long, - currentDate: LocalDate, - today: LocalDate, - ) { - if (currentDate > today) return - - val routine = routineRepository.getRoutineById(routineId) - val oldEntry = - completionHistoryRepository.getHistoryEntries(routineId, currentDate..currentDate) - .first() - - if (currentDate == today) { - deleteStreakIfStarted(routineId, currentDate) - continueStreakIfEnded(routineId, currentDate) - - if (oldEntry.status in sortedOutBacklogStatuses) { - undoSortingOutBacklog(routineId = routineId, currentDate = currentDate) - } - - completionHistoryRepository.deleteHistoryEntry(routineId, currentDate) - return - } - - lastVacationStatus = completionHistoryRepository.getLastHistoryEntryByStatus( - routineId = routine.id!!, - matchingStatuses = onVacationHistoricalStatuses, - ) - - val schedule = routine.schedule - currentPeriod = - if (schedule is Schedule.PeriodicSchedule) schedule.getPeriodRange( - currentDate = currentDate, - lastVacationEndDate = lastVacationStatus?.date, - ) - else null - - println("ToggleHistoricalStatusUseCase oldStatus = ${oldEntry.status}") - when (oldEntry.status) { - HistoricalStatus.Completed -> { - val wasCompletedLater = completionHistoryRepository.checkIfStatusWasCompletedLater( - routineId = routineId, - date = currentDate - ) - if (wasCompletedLater) { - val overCompletedEntry = - completionHistoryRepository.getFirstHistoryEntryByStatus( - routineId = routineId, - matchingStatuses = overCompletedStatuses, - minDate = currentDate.plusDays(1), - )!! - val overCompletedEntryNewStatus = when (overCompletedEntry.status) { - HistoricalStatus.OverCompleted -> - HistoricalStatus.SortedOutBacklog - - HistoricalStatus.OverCompletedOnVacation -> - HistoricalStatus.SortedOutBacklogOnVacation - - else -> throw IllegalStateException() - } - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = overCompletedEntry.date, - newStatus = overCompletedEntryNewStatus, - newScheduleDeviation = 1F, - newTimesCompleted = 1F, - ) - - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.CompletedLater, - newScheduleDeviation = -1F, - newTimesCompleted = 0F, - ) - } else { - when (computeCurrentDatePlanningStatus(routine, currentDate)) { - PlanningStatus.Planned -> { - breakStreak( - routineId = routineId, - date = currentDate, - ) - - val newScheduleDeviation = - if (routine.schedule.backlogEnabled) -1F else 0F - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.NotCompleted, - newScheduleDeviation = newScheduleDeviation, - newTimesCompleted = 0F, - ) - } - - PlanningStatus.AlreadyCompleted -> { - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.AlreadyCompleted, - newScheduleDeviation = -1F, - newTimesCompleted = 0F, - ) - undoCompletingAhead(routine, currentDate) - } - - else -> throw IllegalArgumentException() - } - } - } - - HistoricalStatus.CompletedLater -> { - val sortedOutBacklogEntry = - completionHistoryRepository.getFirstHistoryEntryByStatus( - routineId = routineId, - matchingStatuses = sortedOutBacklogStatuses, - minDate = currentDate.plusDays(1), - )!! - val sortedOutBacklogEntryNewStatus = when (sortedOutBacklogEntry.status) { - HistoricalStatus.SortedOutBacklog -> - HistoricalStatus.OverCompleted - - HistoricalStatus.SortedOutBacklogOnVacation -> - HistoricalStatus.OverCompletedOnVacation - - else -> throw IllegalStateException() - } - - val overCompletedScheduleDeviation = - if (routine.schedule.cancelDuenessIfDoneAhead) 1F else 0F - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = sortedOutBacklogEntry.date, - newStatus = sortedOutBacklogEntryNewStatus, - newScheduleDeviation = overCompletedScheduleDeviation, - newTimesCompleted = 1F, - ) - - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.Completed, - newScheduleDeviation = 0F, - newTimesCompleted = 1F, - ) - } - - HistoricalStatus.NotCompleted -> { - startStreakOrJoinStreaks( - routine = routine, - date = currentDate, - ) - - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.Completed, - newScheduleDeviation = 0F, - newTimesCompleted = 1F, - ) - } - - HistoricalStatus.AlreadyCompleted -> { - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.Completed, - newScheduleDeviation = 0F, - newTimesCompleted = 1F, - ) - completeAhead(routine, currentDate) - } - - HistoricalStatus.Skipped -> { - when (computeCurrentDatePlanningStatus(routine, currentDate)) { - PlanningStatus.Backlog -> { - sortOutBacklog( - routine = routine, - completionHistoryRepository = completionHistoryRepository, - startStreakOrJoinStreaks = startStreakOrJoinStreaks, - currentDate = currentDate, - ) - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.SortedOutBacklog, - newScheduleDeviation = 1F, - newTimesCompleted = 1F, - ) - } - - - PlanningStatus.NotDue -> { - val newScheduleDeviation = - if (routine.schedule.cancelDuenessIfDoneAhead) 1F else 0F - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.OverCompleted, - newScheduleDeviation = newScheduleDeviation, - newTimesCompleted = 1F, - ) - - println("ToggleHistoricalStatusUseCase newScheduleDeviation = $newScheduleDeviation") - completeAhead( - routine, currentDate - ) - - startStreakOrJoinStreaks( - routine = routine, - date = currentDate, - ) - } - - else -> throw IllegalArgumentException() - } - } - - HistoricalStatus.SortedOutBacklog -> { - undoSortingOutBacklog(routineId = routineId, currentDate = currentDate) - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.Skipped, - newScheduleDeviation = 0F, - newTimesCompleted = 0F, - ) - } - - HistoricalStatus.OverCompleted -> { - deleteStreakIfStarted(routineId, currentDate) - - undoCompletingAhead(routine, currentDate) - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.Skipped, - newScheduleDeviation = 0F, - newTimesCompleted = 0F, - ) - } - - HistoricalStatus.NotCompletedOnVacation -> { - val scheduleDeviation = getCurrentScheduleDeviation(routine, currentDate) - val lastNotCompleted = completionHistoryRepository.getLastHistoryEntryByStatus( - routineId = routine.id!!, - matchingStatuses = listOf(HistoricalStatus.NotCompleted), - maxDate = currentDate.minus(DatePeriod(days = 1)), - ) - if (scheduleDeviation < 0 && lastNotCompleted != null) { - sortOutBacklog( - routine = routine, - completionHistoryRepository = completionHistoryRepository, - startStreakOrJoinStreaks = startStreakOrJoinStreaks, - currentDate = currentDate, - ) - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.SortedOutBacklogOnVacation, - newScheduleDeviation = 1F, - newTimesCompleted = 1F, - ) - } else { - startStreakOrJoinStreaks( - routine = routine, - date = currentDate, - ) - - val newScheduleDeviation = - if (routine.schedule.cancelDuenessIfDoneAhead) 1F else 0F - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.OverCompletedOnVacation, - newScheduleDeviation = newScheduleDeviation, - newTimesCompleted = 1F, - ) - } - } - - HistoricalStatus.OverCompletedOnVacation -> { - deleteStreakIfStarted(routineId, currentDate) - - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.NotCompletedOnVacation, - newScheduleDeviation = 0F, - newTimesCompleted = 0F, - ) - } - - HistoricalStatus.SortedOutBacklogOnVacation -> { - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - date = currentDate, - newStatus = HistoricalStatus.NotCompletedOnVacation, - newScheduleDeviation = 0F, - newTimesCompleted = 0F, - ) - - undoSortingOutBacklog(routineId = routineId, currentDate = currentDate) - } - } - } - - private suspend fun computeCurrentDatePlanningStatus( - routine: Routine, date: LocalDate - ): PlanningStatus? { - val dateBeforeCurrent = date.minus(DatePeriod(days = 1)) - val schedule = routine.schedule - val numOfTimesCompletedInCurrentPeriod = - completionHistoryRepository.getTotalTimesCompletedInPeriod( - routineId = routine.id!!, - startDate = currentPeriod?.start ?: schedule.routineStartDate, - endDate = dateBeforeCurrent, - ) - val periodSeparationEnabled = - schedule is Schedule.PeriodicSchedule && schedule.periodSeparationEnabled - val periodRange = currentPeriod - val startDate = if (periodSeparationEnabled && periodRange != null) { - periodRange.start - } else { - schedule.routineStartDate - } - val currentScheduleDeviation = - completionHistoryRepository.getScheduleDeviationInPeriod( - routineId = routine.id!!, - startDate = startDate, - endDate = date, - ) - val scheduleDeviationInCurrentPeriod = periodRange?.let { - completionHistoryRepository.getScheduleDeviationInPeriod( - routineId = routine.id!!, - startDate = it.start, - endDate = date, - ) - } - println("period = $startDate..$date") - println("currentScheduleDeviation = $currentScheduleDeviation") - - return routine.computePlanningStatus( - validationDate = date, - currentScheduleDeviation = currentScheduleDeviation, - actualDate = dateBeforeCurrent, - numOfTimesCompletedInCurrentPeriod = numOfTimesCompletedInCurrentPeriod, - scheduleDeviationInCurrentPeriod = scheduleDeviationInCurrentPeriod, - lastVacationEndDate = lastVacationStatus?.date, - ) - } - - private suspend fun getCurrentScheduleDeviation(routine: Routine, date: LocalDate): Double { - val schedule = routine.schedule - val periodSeparationEnabled = - schedule is Schedule.PeriodicSchedule && schedule.periodSeparationEnabled - val periodRange = currentPeriod - return completionHistoryRepository.getScheduleDeviationInPeriod( - routineId = routine.id!!, - startDate = if (periodSeparationEnabled && periodRange != null) { - periodRange.start - } else { - schedule.routineStartDate - }, - endDate = date, - ) - } - - private suspend fun undoSortingOutBacklog(routineId: Long, currentDate: LocalDate) { - val completedLaterEntry = completionHistoryRepository.getLastHistoryEntryByStatus( - routineId = routineId, - matchingStatuses = listOf(HistoricalStatus.CompletedLater), - maxDate = currentDate.minus(DatePeriod(days = 1)), - )!! - - breakStreak( - routineId = routineId, - date = completedLaterEntry.date, - ) - - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routineId, - newStatus = HistoricalStatus.NotCompleted, - date = completedLaterEntry.date, - newScheduleDeviation = completedLaterEntry.scheduleDeviation, - newTimesCompleted = completedLaterEntry.timesCompleted, - ) - - completionHistoryRepository.deleteCompletedLaterBackupEntry( - routineId, - completedLaterEntry.date - ) - } - - private suspend fun completeAhead( - routine: Routine, - currentDate: LocalDate, - ) { - val schedule = routine.schedule - val periodSeparationEnabled = - schedule is Schedule.PeriodicSchedule && schedule.periodSeparationEnabled - - println("ToggleHistoricalStatusUseCase periodSeparationEnabled = $periodSeparationEnabled") - println("ToggleHistoricalStatusUseCase currentPeriod = $currentPeriod") - - if (schedule.cancelDuenessIfDoneAhead) { - val nextNotCompletedEntry = - completionHistoryRepository.getFirstHistoryEntryByStatus( - routineId = routine.id!!, - matchingStatuses = listOf(HistoricalStatus.NotCompleted), - minDate = currentDate.plusDays(1), - ) - if (nextNotCompletedEntry != null && - (!periodSeparationEnabled - || nextNotCompletedEntry.date in currentPeriod!!) - ) { - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routine.id!!, - date = nextNotCompletedEntry.date, - newStatus = HistoricalStatus.AlreadyCompleted, - newScheduleDeviation = -1F, - newTimesCompleted = 0F, - ) - } - } - } - - private suspend fun undoCompletingAhead( - routine: Routine, - currentDate: LocalDate, - ) { - val schedule = routine.schedule - val periodSeparationEnabled = - schedule is Schedule.PeriodicSchedule && schedule.periodSeparationEnabled - - if (schedule.cancelDuenessIfDoneAhead) { - val lastCompletedAheadEntry = - completionHistoryRepository.getLastHistoryEntryByStatus( - routineId = routine.id!!, - matchingStatuses = listOf(HistoricalStatus.AlreadyCompleted), - minDate = currentDate.plusDays(1), - ) - if (lastCompletedAheadEntry != null && - (!periodSeparationEnabled - || lastCompletedAheadEntry.date in currentPeriod!!) - ) { - val newScheduleDeviation = - if (routine.schedule.backlogEnabled) -1F else 0F - completionHistoryRepository.updateHistoryEntryByDate( - routineId = routine.id!!, - date = lastCompletedAheadEntry.date, - newStatus = HistoricalStatus.NotCompleted, - newScheduleDeviation = newScheduleDeviation, - newTimesCompleted = 0F, - ) - } - } - } -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_time/GetRoutineCompletionTimeUseCase.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_time/GetHabitCompletionTimeUseCase.kt similarity index 64% rename from core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_time/GetRoutineCompletionTimeUseCase.kt rename to core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_time/GetHabitCompletionTimeUseCase.kt index deb3d5d8..5cce192f 100644 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_time/GetRoutineCompletionTimeUseCase.kt +++ b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/completion_time/GetHabitCompletionTimeUseCase.kt @@ -1,31 +1,31 @@ package com.rendox.routinetracker.core.domain.completion_time import com.rendox.routinetracker.core.data.completion_time.CompletionTimeRepository -import com.rendox.routinetracker.core.data.routine.RoutineRepository +import com.rendox.routinetracker.core.data.habit.HabitRepository import com.rendox.routinetracker.core.database.di.toInt import com.rendox.routinetracker.core.logic.time.AnnualDate import com.rendox.routinetracker.core.model.Schedule import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalTime -class GetRoutineCompletionTimeUseCase( - private val routineRepository: RoutineRepository, +class GetHabitCompletionTimeUseCase( + private val habitRepository: HabitRepository, private val completionTimeRepository: CompletionTimeRepository, ) { - suspend operator fun invoke(routineId: Long, date: LocalDate): LocalTime? { + suspend operator fun invoke(habitId: Long, date: LocalDate): LocalTime? { val completionTimeFromSpecificDate = - completionTimeRepository.getCompletionTime(routineId, date) + completionTimeRepository.getCompletionTime(habitId, date) completionTimeFromSpecificDate?.let { return it } - val routine = routineRepository.getRoutineById(routineId) - val completionTimeFromSchedule = date.getIndex(routine.schedule)?.let { - routineRepository.getDueDateSpecificCompletionTime( - routineId = routineId, dueDateNumber = it + val habit = habitRepository.getHabitById(habitId) + val completionTimeFromSchedule = date.getIndex(habit.schedule)?.let { + habitRepository.getDueDateSpecificCompletionTime( + routineId = habitId, dueDateNumber = it ) } completionTimeFromSchedule?.let { return it } - return routine.defaultCompletionTime + return habit.defaultCompletionTime } private fun LocalDate.getIndex(schedule: Schedule): Int? { diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/di/CompletionHistoryDomainModule.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/di/CompletionHistoryDomainModule.kt index 6e1c0317..944efc90 100644 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/di/CompletionHistoryDomainModule.kt +++ b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/di/CompletionHistoryDomainModule.kt @@ -1,37 +1,23 @@ package com.rendox.routinetracker.core.domain.di -import com.rendox.routinetracker.core.domain.completion_history.use_cases.GetRoutineStatusUseCase -import com.rendox.routinetracker.core.domain.completion_history.use_cases.InsertRoutineStatusUseCase -import com.rendox.routinetracker.core.domain.completion_history.use_cases.ToggleHistoricalStatusUseCase +import com.rendox.routinetracker.core.domain.completion_history.HabitComputeStatusUseCase +import com.rendox.routinetracker.core.domain.completion_history.InsertHabitCompletionUseCase import org.koin.dsl.module val completionHistoryDomainModule = module { single { - InsertRoutineStatusUseCase( + HabitComputeStatusUseCase( + habitRepository = get(), + vacationRepository = get(), completionHistoryRepository = get(), - routineRepository = get(), - startStreakOrJoinStreaks = get(), - breakStreak = get(), ) } single { - GetRoutineStatusUseCase( - routineRepository = get(), + InsertHabitCompletionUseCase( completionHistoryRepository = get(), - insertRoutineStatus = get(), - ) - } - - single { - ToggleHistoricalStatusUseCase( - completionHistoryRepository = get(), - routineRepository = get(), - startStreakOrJoinStreaks = get(), - breakStreak = get(), - deleteStreakIfStarted = get(), - continueStreakIfEnded = get(), + habitRepository = get(), ) } } \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/di/RoutineDomainModule.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/di/HabitDomainModule.kt similarity index 60% rename from core/domain/src/main/java/com/rendox/routinetracker/core/domain/di/RoutineDomainModule.kt rename to core/domain/src/main/java/com/rendox/routinetracker/core/domain/di/HabitDomainModule.kt index a9d81fc9..3f5ade5f 100644 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/di/RoutineDomainModule.kt +++ b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/di/HabitDomainModule.kt @@ -1,13 +1,13 @@ package com.rendox.routinetracker.core.domain.di -import com.rendox.routinetracker.core.domain.completion_time.GetRoutineCompletionTimeUseCase +import com.rendox.routinetracker.core.domain.completion_time.GetHabitCompletionTimeUseCase import org.koin.dsl.module -val routineDomainModule = module { +val habitDomainModule = module { single { - GetRoutineCompletionTimeUseCase( - routineRepository = get(), + GetHabitCompletionTimeUseCase( + habitRepository = get(), completionTimeRepository = get(), ) } diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/di/StreakDomainModule.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/di/StreakDomainModule.kt deleted file mode 100644 index 65afa7c5..00000000 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/di/StreakDomainModule.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.rendox.routinetracker.core.domain.di - -import com.rendox.routinetracker.core.domain.streak.BreakStreakUseCase -import com.rendox.routinetracker.core.domain.streak.ContinueStreakIfEndedUseCase -import com.rendox.routinetracker.core.domain.streak.DeleteStreakIfStartedUseCase -import com.rendox.routinetracker.core.domain.streak.GetDisplayStreaksUseCase -import com.rendox.routinetracker.core.domain.streak.StartStreakOrJoinStreaksUseCase -import org.koin.dsl.module - -val streakDomainModule = module { - single { - BreakStreakUseCase( - completionHistoryRepository = get(), - streakRepository = get(), - ) - } - - single { - ContinueStreakIfEndedUseCase( - streakRepository = get() - ) - } - - single { - DeleteStreakIfStartedUseCase( - streakRepository = get() - ) - } - - single { - StartStreakOrJoinStreaksUseCase( - streakRepository = get(), - completionHistoryRepository = get(), - ) - } - - single { - GetDisplayStreaksUseCase( - streakRepository = get(), - completionHistoryRepository = get(), - routineRepository = get(), - ) - } -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/BreakStreakUseCase.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/BreakStreakUseCase.kt deleted file mode 100644 index 0677b3eb..00000000 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/BreakStreakUseCase.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.rendox.routinetracker.core.domain.streak - -import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository -import com.rendox.routinetracker.core.data.streak.StreakRepository -import com.rendox.routinetracker.core.logic.time.plusDays -import com.rendox.routinetracker.core.model.Streak -import com.rendox.routinetracker.core.model.completedStatuses -import kotlinx.datetime.DatePeriod -import kotlinx.datetime.LocalDate -import kotlinx.datetime.minus - -class BreakStreakUseCase( - private val completionHistoryRepository: CompletionHistoryRepository, - private val streakRepository: StreakRepository, -) { - suspend operator fun invoke(routineId: Long, date: LocalDate) { - val oldStreak = streakRepository.getStreakByDate( - routineId = routineId, - dateWithinStreak = date, - ) - - if (oldStreak != null) { - streakRepository.deleteStreakById(id = oldStreak.id!!) - - if (oldStreak.startDate != date) { - streakRepository.insertStreak( - routineId = routineId, - streak = Streak( - startDate = oldStreak.startDate, - endDate = date.minus(DatePeriod(days = 1)), - ), - ) - } - - val completedEntry = completionHistoryRepository.getFirstHistoryEntryByStatus( - routineId = routineId, - matchingStatuses = completedStatuses, - minDate = date.plusDays(1), - ) - - if (completedEntry != null) { - streakRepository.insertStreak( - routineId = routineId, - streak = Streak( - startDate = completedEntry.date, - endDate = oldStreak.endDate, - ) - ) - } - } - } -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/ContinueStreakIfEndedUseCase.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/ContinueStreakIfEndedUseCase.kt deleted file mode 100644 index 7de947f3..00000000 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/ContinueStreakIfEndedUseCase.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.rendox.routinetracker.core.domain.streak - -import com.rendox.routinetracker.core.data.streak.StreakRepository -import kotlinx.datetime.LocalDate - -class ContinueStreakIfEndedUseCase( - private val streakRepository: StreakRepository -) { - suspend operator fun invoke(routineId: Long, currentDate: LocalDate) { - val latestStreak = streakRepository.getLastStreak(routineId) - if (latestStreak?.endDate == currentDate) { - streakRepository.updateStreakById( - id = latestStreak.id!!, - start = latestStreak.startDate, - end = null, - ) - } - } -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/DeleteStreakIfStartedUseCase.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/DeleteStreakIfStartedUseCase.kt deleted file mode 100644 index dc247796..00000000 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/DeleteStreakIfStartedUseCase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.rendox.routinetracker.core.domain.streak - -import com.rendox.routinetracker.core.data.streak.StreakRepository -import kotlinx.datetime.LocalDate - -class DeleteStreakIfStartedUseCase( - private val streakRepository: StreakRepository -) { - suspend operator fun invoke(routineId: Long, currentDate: LocalDate) { - val latestStreak = streakRepository.getLastStreak(routineId) - if (latestStreak?.startDate == currentDate) { - streakRepository.deleteStreakById(latestStreak.id!!) - } - } -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/GetDisplayStreaksUseCase.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/GetDisplayStreaksUseCase.kt deleted file mode 100644 index eb14620e..00000000 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/GetDisplayStreaksUseCase.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.rendox.routinetracker.core.domain.streak - -import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository -import com.rendox.routinetracker.core.data.routine.RoutineRepository -import com.rendox.routinetracker.core.data.streak.StreakRepository -import com.rendox.routinetracker.core.model.DisplayStreak -import kotlinx.datetime.DatePeriod -import kotlinx.datetime.LocalDate -import kotlinx.datetime.minus - -class GetDisplayStreaksUseCase( - private val streakRepository: StreakRepository, - private val completionHistoryRepository: CompletionHistoryRepository, - private val routineRepository: RoutineRepository, -) { - suspend operator fun invoke( - routineId: Long, - afterDateInclusive: LocalDate? = null, - beforeDateInclusive: LocalDate? = null, - today: LocalDate, - ): List = streakRepository.getAllStreaks( - routineId = routineId, - afterDateInclusive = afterDateInclusive, - beforeDateInclusive = beforeDateInclusive, - ).map { - val endDate = it.endDate - val streakEnd: LocalDate = if (endDate != null) { - endDate - } else { - val routineEndDate = - routineRepository.getRoutineById(routineId).schedule.routineEndDate - if (routineEndDate != null && routineEndDate < today) { - routineEndDate - } else { - val completedToday = - completionHistoryRepository.getLastHistoryEntry(routineId)?.date == today - if (completedToday) today else today.minus(DatePeriod(days = 1)) - } - } - - DisplayStreak( - startDate = it.startDate, - endDate = streakEnd, - ) - } -} \ No newline at end of file diff --git a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/StartStreakOrJoinStreaksUseCase.kt b/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/StartStreakOrJoinStreaksUseCase.kt deleted file mode 100644 index cce6a33a..00000000 --- a/core/domain/src/main/java/com/rendox/routinetracker/core/domain/streak/StartStreakOrJoinStreaksUseCase.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.rendox.routinetracker.core.domain.streak - -import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository -import com.rendox.routinetracker.core.data.streak.StreakRepository -import com.rendox.routinetracker.core.logic.time.plusDays -import com.rendox.routinetracker.core.model.HistoricalStatus -import com.rendox.routinetracker.core.model.Routine -import com.rendox.routinetracker.core.model.Streak -import com.rendox.routinetracker.core.model.completedStatuses -import com.rendox.routinetracker.core.model.failedStatuses -import kotlinx.datetime.DatePeriod -import kotlinx.datetime.LocalDate -import kotlinx.datetime.minus - -class StartStreakOrJoinStreaksUseCase( - private val streakRepository: StreakRepository, - private val completionHistoryRepository: CompletionHistoryRepository, -) { - suspend operator fun invoke(routine: Routine, date: LocalDate) { - val previousStreak = streakRepository.getStreakByDate( - routineId = routine.id!!, - dateWithinStreak = date.minus(DatePeriod(days = 1)), - ) - val nextCompletedEntry = completionHistoryRepository.getFirstHistoryEntryByStatus( - routineId = routine.id!!, - matchingStatuses = completedStatuses, - minDate = date.plusDays(1), - ) - - val firstFailedStatusAfterCurrentDate = - completionHistoryRepository.getFirstHistoryEntryByStatus( - routineId = routine.id!!, - matchingStatuses = failedStatuses, - minDate = date.plusDays(1), - ) - - val nextStreak: Streak? = nextCompletedEntry?.let { - streakRepository.getStreakByDate( - routineId = routine.id!!, - dateWithinStreak = it.date, - ) - } - - if (previousStreak != null) { - if (nextStreak != null && (firstFailedStatusAfterCurrentDate == null || firstFailedStatusAfterCurrentDate.date > nextStreak.startDate)) { - streakRepository.updateStreakById( - id = previousStreak.id!!, - start = previousStreak.startDate, - end = nextStreak.endDate, - ) - if (previousStreak.id != nextStreak.id) { - streakRepository.deleteStreakById(id = nextStreak.id!!) - } - } else { - streakRepository.updateStreakById( - id = previousStreak.id!!, - start = previousStreak.startDate, - end = firstFailedStatusAfterCurrentDate - ?.date?.minus(DatePeriod(days = 1)), - ) - } - } else { - val lastNotCompleted = completionHistoryRepository.getLastHistoryEntryByStatus( - routineId = routine.id!!, - matchingStatuses = listOf(HistoricalStatus.NotCompleted), - ) - val streakStart = - if (lastNotCompleted == null) routine.schedule.routineStartDate else date - - if (nextStreak != null && (firstFailedStatusAfterCurrentDate == null || firstFailedStatusAfterCurrentDate.date > nextStreak.startDate)) { - streakRepository.updateStreakById( - id = nextStreak.id!!, - start = streakStart, - end = nextStreak.endDate, - ) - } else { - streakRepository.insertStreak( - routineId = routine.id!!, - streak = Streak( - startDate = streakStart, - endDate = firstFailedStatusAfterCurrentDate - ?.date?.minus(DatePeriod(days = 1)), - ), - ) - } - } - } -} \ No newline at end of file diff --git a/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/GetRoutineStatusUseCaseTest.kt b/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/GetRoutineStatusUseCaseTest.kt deleted file mode 100644 index 85c58fb0..00000000 --- a/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/GetRoutineStatusUseCaseTest.kt +++ /dev/null @@ -1,327 +0,0 @@ -package com.rendox.routinetracker.core.domain.completion_history - -import com.google.common.truth.Truth.assertThat -import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository -import com.rendox.routinetracker.core.data.di.completionHistoryDataModule -import com.rendox.routinetracker.core.data.di.routineDataModule -import com.rendox.routinetracker.core.data.di.streakDataModule -import com.rendox.routinetracker.core.data.routine.RoutineRepository -import com.rendox.routinetracker.core.database.completion_history.CompletionHistoryLocalDataSource -import com.rendox.routinetracker.core.database.routine.RoutineLocalDataSource -import com.rendox.routinetracker.core.database.streak.StreakLocalDataSource -import com.rendox.routinetracker.core.domain.completion_history.use_cases.GetRoutineStatusUseCase -import com.rendox.routinetracker.core.domain.completion_history.use_cases.InsertRoutineStatusUseCase -import com.rendox.routinetracker.core.domain.di.streakDomainModule -import com.rendox.routinetracker.core.logic.time.plusDays -import com.rendox.routinetracker.core.logic.time.rangeTo -import com.rendox.routinetracker.core.model.CompletionHistoryEntry -import com.rendox.routinetracker.core.model.HistoricalStatus -import com.rendox.routinetracker.core.model.PlanningStatus -import com.rendox.routinetracker.core.model.Routine -import com.rendox.routinetracker.core.model.RoutineStatus -import com.rendox.routinetracker.core.model.Schedule -import com.rendox.routinetracker.core.model.StatusEntry -import com.rendox.routinetracker.core.testcommon.fakes.routine.CompletionHistoryLocalDataSourceFake -import com.rendox.routinetracker.core.testcommon.fakes.routine.RoutineData -import com.rendox.routinetracker.core.testcommon.fakes.routine.RoutineLocalDataSourceFake -import com.rendox.routinetracker.core.testcommon.fakes.routine.StreakLocalDataSourceFake -import kotlinx.coroutines.test.runTest -import kotlinx.datetime.DatePeriod -import kotlinx.datetime.DayOfWeek -import kotlinx.datetime.LocalDate -import kotlinx.datetime.Month -import kotlinx.datetime.minus -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import org.koin.dsl.module -import org.koin.test.KoinTest -import org.koin.test.get -import kotlin.random.Random -import kotlin.random.nextInt - -class GetRoutineStatusUseCaseTest : KoinTest { - - private lateinit var insertRoutineStatusIntoHistory: InsertRoutineStatusUseCase - private lateinit var getRoutineStatusList: GetRoutineStatusUseCase - private lateinit var routineRepository: RoutineRepository - private lateinit var completionHistoryRepository: CompletionHistoryRepository - - private val routineId = 1L - private val routineStartDate = LocalDate(2023, Month.OCTOBER, 11) - private val routineEndDate = LocalDate(2023, Month.NOVEMBER, 12) - - private var weeklyScheduleByNumOfDueDays = Schedule.WeeklyScheduleByNumOfDueDays( - numOfDueDays = 5, - numOfDueDaysInFirstPeriod = 4, - startDayOfWeek = DayOfWeek.MONDAY, - backlogEnabled = true, - cancelDuenessIfDoneAhead = true, - routineStartDate = routineStartDate, - routineEndDate = routineEndDate, - ) - - - private val testModule = module { - single { - RoutineData() - } - - single { - RoutineLocalDataSourceFake(routineData = get()) - } - - single { - CompletionHistoryLocalDataSourceFake(routineData = get()) - } - - single { - StreakLocalDataSourceFake(routineData = get()) - } - } - - @Before - fun setUp() { - startKoin { - modules( - routineDataModule, - completionHistoryDataModule, - streakDataModule, - streakDomainModule, - testModule, - ) - } - - routineRepository = get() - completionHistoryRepository = get() - - insertRoutineStatusIntoHistory = InsertRoutineStatusUseCase( - completionHistoryRepository = get(), - routineRepository = get(), - startStreakOrJoinStreaks = get(), - breakStreak = get(), - ) - - getRoutineStatusList = GetRoutineStatusUseCase( - routineRepository = get(), - completionHistoryRepository = get(), - insertRoutineStatus = insertRoutineStatusIntoHistory, - ) - } - - @After - fun tearDown() { - stopKoin() - } - - @Test - fun `get routine status for dates prior to routine start, returns empty list`() = runTest { - val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = weeklyScheduleByNumOfDueDays, - ) - - routineRepository.insertRoutine(routine) - - val randomDateBeforeRoutineStart = - routineStartDate.minus(DatePeriod(days = Random.nextInt(1..50))) - - assertThat( - getRoutineStatusList( - routineId = routineId, - dates = randomDateBeforeRoutineStart..routineStartDate.minus(DatePeriod(days = 1)), - today = LocalDate(2022, Month.NOVEMBER, 19), - ) - ).isEqualTo(emptyList()) - } - - @Test - fun `WeeklyScheduleByNumOfDueDays, test due not due`() = runTest { - val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = weeklyScheduleByNumOfDueDays, - ) - - routineRepository.insertRoutine(routine) - - val expectedStatuses = mutableListOf() - repeat(4) { expectedStatuses.add(PlanningStatus.Planned) } - expectedStatuses.add(PlanningStatus.NotDue) - repeat(5) { expectedStatuses.add(PlanningStatus.Planned) } - repeat(2) { expectedStatuses.add(PlanningStatus.NotDue) } - - assertThat( - getRoutineStatusList( - routineId = routineId, - dates = routineStartDate..LocalDate(2023, Month.OCTOBER, 22), - today = routineStartDate, - ) - ).isEqualTo( - expectedStatuses.mapIndexed { index, status -> - StatusEntry(routineStartDate.plusDays(index), status) - } - ) - assertThat(routineRepository.getRoutineById(routineId)).isEqualTo(routine) - } - - @Test - fun `WeeklyScheduleByNumOfDueDays, test history pre-population`() = runTest { - val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = weeklyScheduleByNumOfDueDays, - ) - - routineRepository.insertRoutine(routine) - - val thirdWeekPeriod = - LocalDate(2023, Month.OCTOBER, 23)..LocalDate(2023, Month.OCTOBER, 29) - - val expectedStatusesOnThirdWeek = mutableListOf() - repeat(2) { expectedStatusesOnThirdWeek.add(HistoricalStatus.Skipped) } - repeat(3) { expectedStatusesOnThirdWeek.add(HistoricalStatus.NotCompleted) } - repeat(2) { expectedStatusesOnThirdWeek.add(PlanningStatus.Planned) } - - assertThat( - getRoutineStatusList( - routineId = routineId, - dates = thirdWeekPeriod, - today = LocalDate(2023, Month.OCTOBER, 28), - ) - ).isEqualTo( - expectedStatusesOnThirdWeek.mapIndexed { index, status -> - StatusEntry(thirdWeekPeriod.first().plusDays(index), status) - } - ) - } - - @Test - fun `WeeklyScheduleByNumOfDueDays, test backlog`() = runTest { - val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = weeklyScheduleByNumOfDueDays, - ) - - routineRepository.insertRoutine(routine) - - - val forthWeekPeriod = - LocalDate(2023, Month.OCTOBER, 30)..LocalDate(2023, Month.NOVEMBER, 5) - - val expectedStatusesOnForthWeek = mutableListOf() - repeat(5) { expectedStatusesOnForthWeek.add(PlanningStatus.Planned) } - repeat(2) { expectedStatusesOnForthWeek.add(PlanningStatus.Backlog) } - - assertThat( - getRoutineStatusList( - routineId = routineId, - dates = forthWeekPeriod, - today = LocalDate(2023, Month.OCTOBER, 28), - ) - ).isEqualTo( - expectedStatusesOnForthWeek.mapIndexed { index, status -> - StatusEntry(forthWeekPeriod.first().plusDays(index), status) - } - ) - } - - @Test - fun `WeeklyScheduleByNumOfDueDays, test cancelDuenessIfDoneAhead`() = runTest { - val schedule = Schedule.WeeklyScheduleByNumOfDueDays( - numOfDueDays = 4, - numOfDueDaysInFirstPeriod = null, - backlogEnabled = true, - cancelDuenessIfDoneAhead = true, - routineStartDate = LocalDate(2023, Month.NOVEMBER, 6), - ) - val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = schedule, - ) - routineRepository.insertRoutine(routine) - - val completedDays = - LocalDate(2023, Month.NOVEMBER, 6)..LocalDate(2023, Month.NOVEMBER, 9) - completedDays.forEachIndexed { _, date -> - completionHistoryRepository.insertHistoryEntry( - routineId = routineId, - entry = CompletionHistoryEntry( - date = date, - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - } - - val overCompletedDays = - LocalDate(2023, Month.NOVEMBER, 10)..LocalDate(2023, Month.NOVEMBER, 12) - overCompletedDays.forEachIndexed { _, date -> - completionHistoryRepository.insertHistoryEntry( - routineId = routineId, - entry = CompletionHistoryEntry( - date = date, - status = HistoricalStatus.OverCompleted, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - } - - val expectedStatusesOnSixthsWeek = mutableListOf() - repeat(3) { expectedStatusesOnSixthsWeek.add(PlanningStatus.AlreadyCompleted) } - expectedStatusesOnSixthsWeek.add(PlanningStatus.Planned) - repeat(3) { expectedStatusesOnSixthsWeek.add(PlanningStatus.NotDue) } - - assertThat( - getRoutineStatusList( - routineId = routineId, - dates = LocalDate(2023, Month.NOVEMBER, 13)..LocalDate(2023, Month.NOVEMBER, 19), - today = LocalDate(2023, Month.NOVEMBER, 13), - ) - ).isEqualTo( - expectedStatusesOnSixthsWeek.mapIndexed { index, status -> - StatusEntry(LocalDate(2023, Month.NOVEMBER, 13).plusDays(index), status) - } - ) - } - - @Test - fun `get routine status after end date test`() = runTest { - val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = weeklyScheduleByNumOfDueDays, - ) - - routineRepository.insertRoutine(routine) - - val weekAfterEndDatePeriod = - LocalDate(2023, Month.NOVEMBER, 13)..LocalDate(2023, Month.NOVEMBER, 19) - - assertThat( - getRoutineStatusList( - routineId = routineId, - dates = weekAfterEndDatePeriod, - today = LocalDate(2023, Month.NOVEMBER, 19), - ) - ).isEqualTo(emptyList()) - - val randomDateBeforeRoutineStart = - routineStartDate.minus(DatePeriod(days = Random.nextInt(1..50))) - - assertThat( - getRoutineStatusList( - routineId = routineId, - dates = randomDateBeforeRoutineStart..routineStartDate.minus(DatePeriod(days = 1)), - today = LocalDate(2023, Month.NOVEMBER, 19), - ) - ).isEqualTo(emptyList()) - } -} \ No newline at end of file diff --git a/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/HabitComputeStatusUseCaseTest.kt b/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/HabitComputeStatusUseCaseTest.kt new file mode 100644 index 00000000..ac992448 --- /dev/null +++ b/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/HabitComputeStatusUseCaseTest.kt @@ -0,0 +1,785 @@ +package com.rendox.routinetracker.core.domain.completion_history + +import com.google.common.truth.Truth.assertThat +import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository +import com.rendox.routinetracker.core.data.habit.HabitRepository +import com.rendox.routinetracker.core.data.vacation.VacationRepository +import com.rendox.routinetracker.core.logic.time.LocalDateRange +import com.rendox.routinetracker.core.logic.time.plusDays +import com.rendox.routinetracker.core.logic.time.rangeTo +import com.rendox.routinetracker.core.model.Habit +import com.rendox.routinetracker.core.model.HabitStatus +import com.rendox.routinetracker.core.model.Schedule +import com.rendox.routinetracker.core.model.Vacation +import com.rendox.routinetracker.core.testcommon.fakes.habit.CompletionHistoryRepositoryFake +import com.rendox.routinetracker.core.testcommon.fakes.habit.HabitData +import com.rendox.routinetracker.core.testcommon.fakes.habit.HabitRepositoryFake +import com.rendox.routinetracker.core.testcommon.fakes.habit.VacationRepositoryFake +import kotlinx.coroutines.test.runTest +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.DayOfWeek +import kotlinx.datetime.LocalDate +import kotlinx.datetime.minus +import org.junit.Before +import org.junit.Test + +class HabitComputeStatusUseCaseTest { + private lateinit var completionHistoryRepository: CompletionHistoryRepository + private lateinit var computeStatus: HabitComputeStatusUseCase + private lateinit var habitRepository: HabitRepository + private lateinit var vacationRepository: VacationRepository + + private val defaultSchedule = Schedule.WeeklyScheduleByDueDaysOfWeek( + dueDaysOfWeek = listOf( + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.SUNDAY, + ), + startDayOfWeek = DayOfWeek.MONDAY, + backlogEnabled = true, + completingAheadEnabled = true, + periodSeparationEnabled = false, + startDate = LocalDate(2023, 12, 4), + ) + + private val defaultHabit = Habit.YesNoHabit( + id = 1L, + name = "", + schedule = defaultSchedule, + ) + + @Before + fun setUp() = runTest { + val habitData = HabitData() + habitRepository = HabitRepositoryFake(habitData) + vacationRepository = VacationRepositoryFake(habitData) + completionHistoryRepository = CompletionHistoryRepositoryFake(habitData) + + habitRepository.insertHabit(defaultHabit) + + computeStatus = HabitComputeStatusUseCase( + habitRepository = habitRepository, + completionHistoryRepository = completionHistoryRepository, + vacationRepository = vacationRepository, + ) + } + + @Test + fun `future date, due, not completed, assert status is Planned`() = runTest { + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 14), // Thursday + today = defaultSchedule.startDate, + ) + assertThat(habitStatus).isEqualTo(HabitStatus.Planned) + } + + @Test + fun `due, completed, assert status is Completed no matter the date`() = runTest { + val lastDueDate = LocalDate(2023, 12, 27) + val dueDates = mutableListOf() + for (date in defaultSchedule.startDate..lastDueDate) { + if (defaultSchedule.isDue(validationDate = date)) dueDates.add(date) + } + for (validationDate in dueDates) { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord(date = validationDate), + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = validationDate, + today = LocalDate(2023, 12, 14), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.Completed) + } + } + + @Test + fun `due, completed, assert status is PartiallyCompleted no matter the date`() = runTest { + val lastDueDate = LocalDate(2023, 12, 27) + val dueDates = mutableListOf() + for (date in defaultSchedule.startDate..lastDueDate) { + if (defaultSchedule.isDue(validationDate = date)) dueDates.add(date) + } + for (validationDate in dueDates) { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = validationDate, + numOfTimesCompleted = 0.5F, + ), + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = validationDate, + today = LocalDate(2023, 12, 14), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.PartiallyCompleted) + } + } + + @Test + fun `due, completed more times than planned, assert status is OverCompleted no matter the date`() = + runTest { + val lastDueDate = LocalDate(2023, 12, 27) + val dueDates = + getDueDatesInPeriod(defaultSchedule.startDate..lastDueDate, defaultSchedule) + + for (validationDate in dueDates) { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = validationDate, + numOfTimesCompleted = 2F, + ), + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = validationDate, + today = LocalDate(2023, 12, 14), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.OverCompleted) + } + } + + @Test + fun `future date, due, backlog, completed more times than planned, assert status is SortedOutBacklog`() = + runTest { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 7), // Wednesday + numOfTimesCompleted = 2F, + ) + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 7), // Wednesday + today = LocalDate(2023, 12, 7), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.SortedOutBacklog) + } + + @Test + fun `future date, not due, backlog, completed, assert status is SortedOutBacklog`() = runTest { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 7), // Wednesday + numOfTimesCompleted = 2F, + ), + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 7), // Wednesday + today = LocalDate(2023, 12, 7), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.SortedOutBacklog) + } + + @Test + fun `future date, not due, not completed, assert status is NotDue`() = runTest { + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 8), // Friday + today = defaultSchedule.startDate, + ) + assertThat(habitStatus).isEqualTo(HabitStatus.NotDue) + } + + @Test + fun `future date, backlog, not completed, assert status is Backlog`() = runTest { + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 12), // Tuesday + today = LocalDate(2023, 12, 11), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.Backlog) + } + + @Test + fun `completed on time, not due, not completed, assert status is NotDue`() = runTest { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 6), // Wednesday + ) + + ) + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 7), // Thursday + ) + ) + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 10), // Sunday + ) + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 12), + today = LocalDate(2023, 12, 11), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.NotDue) + } + + @Test + fun `future date, backlog, but backlog is disabled, not completed, assert status is NotDue`() = + runTest { + val schedule = defaultSchedule.copy(backlogEnabled = false) + val habit = defaultHabit.copy(id = 2L, schedule = schedule) + habitRepository.insertHabit(habit) + + val habitStatus = computeStatus( + habitId = 2L, + validationDate = LocalDate(2023, 12, 11), + today = LocalDate(2023, 12, 10), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.NotDue) + } + + @Test + fun `future date, backlog, but period separation enabled, another period, not completed, assert status is NotDue`() = + runTest { + val schedule = + defaultSchedule.copy(periodSeparationEnabled = true, backlogEnabled = true) + val habit = defaultHabit.copy(id = 2L, schedule = schedule) + habitRepository.insertHabit(habit) + + val habitStatus = computeStatus( + habitId = 2L, + validationDate = LocalDate(2023, 12, 11), + today = LocalDate(2023, 12, 10), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.NotDue) + } + + @Test + fun `future date, already completed, assert status is FutureDateAlreadyCompleted`() = runTest { + for (dayIndex in 2..5) { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = defaultSchedule.startDate.plusDays(dayIndex), + ) + ) + } + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 13), + today = LocalDate(2023, 12, 9), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.FutureDateAlreadyCompleted) + } + + @Test + fun `future date, already completed, but completing ahead disabled, assert status is Planned`() = + runTest { + val schedule = defaultSchedule.copy(completingAheadEnabled = false) + val habit = defaultHabit.copy(id = 2L, schedule = schedule) + habitRepository.insertHabit(habit) + + for (dayIndex in 2..5) { + completionHistoryRepository.insertCompletion( + habitId = 2L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = defaultSchedule.startDate.plusDays(dayIndex), // Wednesday + ) + ) + } + + val habitStatus = computeStatus( + habitId = 2L, + validationDate = LocalDate(2023, 12, 13), + today = LocalDate(2023, 12, 9), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.Planned) + } + + @Test + fun `future date, already completed, but period separation enabled, assert status is Planned`() = + runTest { + val schedule = + defaultSchedule.copy(periodSeparationEnabled = true, completingAheadEnabled = true) + val habit = defaultHabit.copy(id = 2L, schedule = schedule) + habitRepository.insertHabit(habit) + + for (dayIndex in 2..5) { + completionHistoryRepository.insertCompletion( + habitId = 2L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = defaultSchedule.startDate.plusDays(dayIndex), // Wednesday + ) + ) + } + + val habitStatus = computeStatus( + habitId = 2L, + validationDate = LocalDate(2023, 12, 13), + today = LocalDate(2023, 12, 9), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.Planned) + } + + @Test + fun `past date, due, not completed, assert status is Failed`() = runTest { + val today = LocalDate(2023, 12, 24) + val dueDates = getDueDatesInPeriod(defaultSchedule.startDate..today, defaultSchedule) + + for (validationDate in dueDates) { + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 13), // Tuesday + today = today, + ) + assertThat(habitStatus).isEqualTo(HabitStatus.Failed) + } + } + + @Test + fun `past date, due, completed later, assert status is CompletedLater`() = runTest { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 8), // Friday + ) + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 7), // Thursday + today = LocalDate(2023, 12, 24), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.CompletedLater) + } + + @Test + fun `past date, due, completed later but not compensated for current date, assert status is Failed`() = + runTest { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 8), // Friday + ) + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 6), // Wednesday + today = LocalDate(2023, 12, 24), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.Failed) + } + + @Test + fun `past date, due, compensated later for two days, assert statuses of both are is CompletedLater`() = + runTest { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 8), // Friday + numOfTimesCompleted = 2F, + ) + ) + + val statusOnWednesday = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 6), // Wednesday + today = LocalDate(2023, 12, 24), + ) + assertThat(statusOnWednesday).isEqualTo(HabitStatus.CompletedLater) + + val statusOnThursday = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 7), // Thursday + today = LocalDate(2023, 12, 24), + ) + assertThat(statusOnThursday).isEqualTo(HabitStatus.CompletedLater) + } + + @Test + fun `past date, due, completed previously but not compensated for current date, assert status is Failed`() = + runTest { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 8), // Friday + ) + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 10), // Sunday + today = LocalDate(2023, 12, 24), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.Failed) + } + + @Test + fun `past date, completed both earlier and later, assert status is AlreadyCompleted`() = + runTest { + val startDate = LocalDate(2023, 12, 6) + val endDate = LocalDate(2023, 12, 8) + + for (date in startDate..endDate) { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = date, + ) + ) + } + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 11), + ) + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 10), + today = LocalDate(2023, 12, 11), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.PastDateAlreadyCompleted) + } + + @Test + fun `on vacation, not completed, assert status is OnVacation no matter the date`() = runTest { + val vacationStartDate = LocalDate(2023, 12, 4) + val vacationEndDate = LocalDate(2023, 12, 10) + + vacationRepository.insertVacation( + habitId = 1L, + vacation = Vacation( + startDate = vacationStartDate, + endDate = vacationEndDate, + ), + ) + + for (date in vacationStartDate..vacationEndDate) { + val habitStatus = computeStatus( + habitId = 1L, + validationDate = date, + today = LocalDate(2023, 12, 7), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.OnVacation) + } + } + + @Test + fun `on vacation, no backlog, completed, assert status is OverCompleted`() = runTest { + val vacationStartDate = LocalDate(2023, 12, 4) + val vacationEndDate = LocalDate(2023, 12, 10) + + vacationRepository.insertVacation( + habitId = 1L, + vacation = Vacation( + startDate = vacationStartDate, + endDate = vacationEndDate, + ), + ) + + for (date in vacationStartDate..vacationEndDate) { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord(date = date) + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = date, + today = LocalDate(2023, 12, 13), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.OverCompleted) + } + } + + @Test + fun `on vacation, backlog, completed, assert only necessary dates have SortedOutBacklog status`() = + runTest { + val vacationStartDate = LocalDate(2023, 12, 11) + val vacationEndDate = LocalDate(2023, 12, 17) + + vacationRepository.insertVacation( + habitId = 1L, + vacation = Vacation( + startDate = vacationStartDate, + endDate = vacationEndDate, + ), + ) + + val today = LocalDate(2023, 12, 17) + + val resultingStatuses = mutableListOf() + val expectedStatuses = mutableListOf() + + for (date in vacationStartDate..vacationEndDate) { + if (date <= LocalDate(2023, 12, 13)) { + expectedStatuses.add(HabitStatus.SortedOutBacklog) + } else { + expectedStatuses.add(HabitStatus.OverCompleted) + } + + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord(date = date) + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = date, + today = today, + ) + resultingStatuses.add(habitStatus) + } + + assertThat(resultingStatuses).containsExactlyElementsIn(expectedStatuses).inOrder() + } + + @Test + fun `over completed and sorted out backlog at the same time, assert previous days have CompletedLater statuses`() = + runTest { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 13), + numOfTimesCompleted = 5F, + ) + ) + + val previousDueDates = listOf( + LocalDate(2023, 12, 6), + LocalDate(2023, 12, 7), + LocalDate(2023, 12, 10), + ) + + val expectedStatuses = + previousDueDates.associateWith { HabitStatus.CompletedLater } + val resultingStatuses = previousDueDates.associateWith { + computeStatus( + habitId = 1L, + validationDate = it, + today = LocalDate(2023, 12, 18), + ) + } + + assertThat(resultingStatuses).containsExactlyEntriesIn(expectedStatuses).inOrder() + } + + @Test + fun `over completed and sorted out backlog at the same time, assert next days have AlreadyCompleted status`() = + runTest { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 13), + numOfTimesCompleted = 5F, + ) + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 14), + today = LocalDate(2023, 12, 18), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.PastDateAlreadyCompleted) + } + + @Test + fun `over completed and sorted out backlog at the same time, assert status is SortedOutBacklog`() = + runTest { + completionHistoryRepository.insertCompletion( + habitId = 1L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 13), + numOfTimesCompleted = 5F, + ) + ) + + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 13), + today = LocalDate(2023, 12, 18), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.SortedOutBacklog) + } + + @Test + fun `date before habit start date, assert status is NotStarted`() = runTest { + val dateBeforeStartDate = defaultSchedule.startDate.minus(DatePeriod(days = 1)) + val habitStatus = computeStatus( + habitId = 1L, + validationDate = dateBeforeStartDate, + today = dateBeforeStartDate, + ) + assertThat(habitStatus).isEqualTo(HabitStatus.NotStarted) + } + + @Test + fun `date after habit end date, assert status is Finished`() = runTest { + val endDate = LocalDate(2023, 12, 31) + val schedule = defaultSchedule.copy(endDate = endDate) + val habit = defaultHabit.copy(id = 2L, schedule = schedule) + habitRepository.insertHabit(habit) + + val habitStatus = computeStatus( + habitId = 2L, + validationDate = endDate.plusDays(1), + today = schedule.startDate, + ) + assertThat(habitStatus).isEqualTo(HabitStatus.Finished) + } + + @Test + fun `past date, not due, not completed, no backlog, assert status is Skipped`() = runTest { + val habitStatus = computeStatus( + habitId = 1L, + validationDate = LocalDate(2023, 12, 5), // Tuesday + today = LocalDate(2024, 1, 1), + ) + assertThat(habitStatus).isEqualTo(HabitStatus.Skipped) + } + + @Test + fun `backlog disabled, negative schedule deviation, assert over completing completes ahead`() = runTest { + val schedule = Schedule.WeeklyScheduleByDueDaysOfWeek( + dueDaysOfWeek = listOf( + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.SATURDAY, + DayOfWeek.SUNDAY, + ), + startDayOfWeek = DayOfWeek.MONDAY, + backlogEnabled = false, + completingAheadEnabled = true, + periodSeparationEnabled = true, + startDate = LocalDate(2023, 12 , 4), + ) + val habit = defaultHabit.copy(id = 2L, schedule = schedule) + habitRepository.insertHabit(habit) + + completionHistoryRepository.insertCompletion( + habitId = 2L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 6), + numOfTimesCompleted = 1F, + ) + ) + completionHistoryRepository.insertCompletion( + habitId = 2L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2023, 12, 8), + numOfTimesCompleted = 1F, + ) + ) + + assertThat( + computeStatus( + habitId = 2L, + validationDate = LocalDate(2023, 12, 9), + today = LocalDate(2024, 1, 11), + ) + ).isEqualTo(HabitStatus.PastDateAlreadyCompleted) + + assertThat( + computeStatus( + habitId = 2L, + validationDate = LocalDate(2023, 12, 10), + today = LocalDate(2024, 1, 11), + ) + ).isEqualTo(HabitStatus.PastDateAlreadyCompleted) + } + + @Test + fun `assert sorting out backlog in the past revokes both previous failed status and future backlog`() = runTest { + val schedule = Schedule.MonthlyScheduleByDueDatesIndices( + dueDatesIndices = listOf(2, 8), + backlogEnabled = true, + completingAheadEnabled = true, + periodSeparationEnabled = true, + startDate = LocalDate(2024, 1 , 1), + weekDaysMonthRelated = emptyList(), + ) + val habit = defaultHabit.copy(id = 2L, schedule = schedule) + habitRepository.insertHabit(habit) + + completionHistoryRepository.insertCompletion( + habitId = 2L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2024, 1, 10), + numOfTimesCompleted = 1F, + ) + ) + + assertThat( + computeStatus( + habitId = 2L, + validationDate = LocalDate(2024, 1, 8), + today = LocalDate(2024, 1, 11), + ) + ).isEqualTo(HabitStatus.CompletedLater) // instead of Failed + + assertThat( + computeStatus( + habitId = 2L, + validationDate = LocalDate(2024, 1, 12), + today = LocalDate(2024, 1, 11), + ) + ).isEqualTo(HabitStatus.NotDue) // instead of Backlog + } + + @Test + fun `assert sorting out backlog in the future does not revoke future backlog`() = runTest { + val schedule = Schedule.MonthlyScheduleByDueDatesIndices( + dueDatesIndices = listOf(2, 8), + backlogEnabled = true, + completingAheadEnabled = true, + periodSeparationEnabled = true, + startDate = LocalDate(2024, 1 , 1), + weekDaysMonthRelated = emptyList(), + ) + val habit = defaultHabit.copy(id = 2L, schedule = schedule) + habitRepository.insertHabit(habit) + + completionHistoryRepository.insertCompletion( + habitId = 2L, + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = LocalDate(2024, 1, 11), + numOfTimesCompleted = 1F, + ) + ) + + assertThat( + computeStatus( + habitId = 2L, + validationDate = LocalDate(2024, 1, 12), + today = LocalDate(2024, 1, 11), + ) + ).isEqualTo(HabitStatus.Backlog) + } + + companion object { + private fun getDueDatesInPeriod( + period: LocalDateRange, + schedule: Schedule, + ): List { + val dueDates = mutableListOf() + for (date in period) { + if (schedule.isDue(validationDate = date)) dueDates.add(date) + } + return dueDates + } + } +} \ No newline at end of file diff --git a/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/InsertRoutineStatusUseCaseTest.kt b/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/InsertRoutineStatusUseCaseTest.kt deleted file mode 100644 index 80eb4536..00000000 --- a/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/InsertRoutineStatusUseCaseTest.kt +++ /dev/null @@ -1,1659 +0,0 @@ -package com.rendox.routinetracker.core.domain.completion_history - -import com.google.common.truth.Truth.assertThat -import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository -import com.rendox.routinetracker.core.data.di.completionHistoryDataModule -import com.rendox.routinetracker.core.data.di.routineDataModule -import com.rendox.routinetracker.core.data.di.streakDataModule -import com.rendox.routinetracker.core.data.routine.RoutineRepository -import com.rendox.routinetracker.core.data.streak.StreakRepository -import com.rendox.routinetracker.core.database.completion_history.CompletionHistoryLocalDataSource -import com.rendox.routinetracker.core.database.routine.RoutineLocalDataSource -import com.rendox.routinetracker.core.database.streak.StreakLocalDataSource -import com.rendox.routinetracker.core.domain.completion_history.use_cases.InsertRoutineStatusUseCase -import com.rendox.routinetracker.core.domain.di.streakDomainModule -import com.rendox.routinetracker.core.logic.time.WeekDayMonthRelated -import com.rendox.routinetracker.core.logic.time.WeekDayNumberMonthRelated -import com.rendox.routinetracker.core.logic.time.plusDays -import com.rendox.routinetracker.core.logic.time.rangeTo -import com.rendox.routinetracker.core.model.CompletionHistoryEntry -import com.rendox.routinetracker.core.model.HistoricalStatus -import com.rendox.routinetracker.core.model.Routine -import com.rendox.routinetracker.core.model.Schedule -import com.rendox.routinetracker.core.model.Streak -import com.rendox.routinetracker.core.testcommon.fakes.routine.CompletionHistoryLocalDataSourceFake -import com.rendox.routinetracker.core.testcommon.fakes.routine.RoutineData -import com.rendox.routinetracker.core.testcommon.fakes.routine.RoutineLocalDataSourceFake -import com.rendox.routinetracker.core.testcommon.fakes.routine.StreakLocalDataSourceFake -import kotlinx.coroutines.test.runTest -import kotlinx.datetime.DatePeriod -import kotlinx.datetime.DayOfWeek -import kotlinx.datetime.LocalDate -import kotlinx.datetime.Month -import kotlinx.datetime.minus -import kotlinx.datetime.plus -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import org.koin.dsl.module -import org.koin.test.KoinTest -import org.koin.test.get -import kotlin.test.assertFailsWith - -class InsertRoutineStatusUseCaseTest : KoinTest { - - private lateinit var routineRepository: RoutineRepository - private lateinit var completionHistoryRepository: CompletionHistoryRepository - private lateinit var insertRoutineStatus: InsertRoutineStatusUseCase - private lateinit var streakRepository: StreakRepository - private val today = LocalDate(2030, Month.JANUARY, 1) - - private val testModule = module { - single { RoutineData() } - - single { - RoutineLocalDataSourceFake(routineData = get()) - } - - single { - CompletionHistoryLocalDataSourceFake(routineData = get()) - } - - single { - StreakLocalDataSourceFake(routineData = get()) - } - } - - @Before - fun setUp() { - startKoin { - modules( - routineDataModule, - completionHistoryDataModule, - streakDataModule, - streakDomainModule, - testModule, - ) - } - - routineRepository = get() - completionHistoryRepository = get() - streakRepository = get() - - insertRoutineStatus = InsertRoutineStatusUseCase( - completionHistoryRepository = completionHistoryRepository, - routineRepository = routineRepository, - startStreakOrJoinStreaks = get(), - breakStreak = get(), - ) - } - - @After - fun tearDown() { - stopKoin() - } - - @Test - fun `every day schedule, standard check`() = runTest { - val routineId = 1L - val routineStartDate = LocalDate(2023, Month.OCTOBER, 1) - - val routineEndDate = routineStartDate.plusDays(5) - - val schedule = Schedule.EveryDaySchedule( - routineStartDate = routineStartDate, - vacationStartDate = routineStartDate.plusDays(3), - vacationEndDate = routineStartDate.plusDays(3), - routineEndDate = routineEndDate, - ) - - val expectedHistory = listOf( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 1), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 2), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 3), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 4), - status = HistoricalStatus.NotCompletedOnVacation, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 5), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 6), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - ) - - val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = schedule, - ) - - routineRepository.insertRoutine(routine) - - insertRoutineStatus( - routineId = routineId, - currentDate = expectedHistory[0].date, - completedOnCurrentDate = true, - today = today, - ) - - assertThat( - streakRepository.getAllStreaks(routineId) - ).isEqualTo( - listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 1), - endDate = null, - ) - ) - ) - - insertRoutineStatus( - routineId = routineId, - currentDate = expectedHistory[1].date, - completedOnCurrentDate = true, - today = today, - ) - - assertThat( - streakRepository.getAllStreaks(routineId) - ).isEqualTo( - listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 1), - endDate = null, - ) - ) - ) - - insertRoutineStatus( - routineId = routineId, - currentDate = expectedHistory[2].date, - completedOnCurrentDate = false, - today = today, - ) - - assertThat( - streakRepository.getAllStreaks(routineId) - ).isEqualTo( - listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 1), - endDate = LocalDate(2023, Month.OCTOBER, 2), - ) - ) - ) - - insertRoutineStatus( - routineId = routineId, - currentDate = expectedHistory[3].date, - completedOnCurrentDate = false, - today = today, - ) - - assertThat( - streakRepository.getAllStreaks(routineId) - ).isEqualTo( - listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 1), - endDate = LocalDate(2023, Month.OCTOBER, 2), - ) - ) - ) - - insertRoutineStatus( - routineId = routineId, - currentDate = expectedHistory[4].date, - completedOnCurrentDate = true, - today = today, - ) - - assertThat( - streakRepository.getAllStreaks(routineId) - ).isEqualTo( - listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 1), - endDate = LocalDate(2023, Month.OCTOBER, 2), - ), - Streak( - id = 2, - startDate = LocalDate(2023, Month.OCTOBER, 5), - endDate = null, - ) - ) - ) - - insertRoutineStatus( - routineId = routineId, - currentDate = expectedHistory[5].date, - completedOnCurrentDate = false, - today = today, - ) - - assertThat( - streakRepository.getAllStreaks(routineId) - ).isEqualTo( - listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 1), - endDate = LocalDate(2023, Month.OCTOBER, 2), - ), - Streak( - id = 2, - startDate = LocalDate(2023, Month.OCTOBER, 5), - endDate = LocalDate(2023, Month.OCTOBER, 5), - ) - ) - ) - - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = routineStartDate..routineEndDate, - ) - ).isEqualTo(expectedHistory) - - assertFailsWith { - insertRoutineStatus( - routineId = routineId, - currentDate = routineStartDate.minus(DatePeriod(days = 1)), - completedOnCurrentDate = false, - today = today, - ) - } - - assertFailsWith { - insertRoutineStatus( - routineId = routineId, - currentDate = routineEndDate.plus(DatePeriod(days = 1)), - completedOnCurrentDate = false, - today = today, - ) - } - } - - @Test - fun `weekly schedule, due on explicit days of week, periodic separation enabled`() = runTest { - val routineId = 1L - val routineStartDate = LocalDate(2023, Month.OCTOBER, 2) // Monday - - val schedule = Schedule.WeeklyScheduleByDueDaysOfWeek( - routineStartDate = routineStartDate, - backlogEnabled = true, - cancelDuenessIfDoneAhead = true, - periodSeparationEnabled = true, - dueDaysOfWeek = listOf( - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.FRIDAY, - DayOfWeek.SATURDAY, - ), - startDayOfWeek = DayOfWeek.MONDAY, - ) - - val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = schedule, - ) - - routineRepository.insertRoutine(routine) - - val completionHistory = listOf( - false, // Monday - true, // Tuesday - true, // Wednesday - false, // Thursday - false, // Friday + backlog - false, // Saturday + backlog - true, // Sunday - backlog - - false, // Monday - true, // Tuesday - true, // Wednesday - true, // Thursday - backlog - false, // Friday + backlog - true, // Saturday - true, // Sunday - backlog - - false, // Monday - false, // Tuesday + backlog - ) - - val firstSixDaysOfFirstWeek = listOf( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 2), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 3), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 4), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 5), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 6), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 7), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ), - ) - - completionHistory.slice(0..5).forEachIndexed { index, isCompleted -> - insertRoutineStatus( - routineId = routineId, - currentDate = routineStartDate.plusDays(index), - completedOnCurrentDate = isCompleted, - today = today, - ) - } - - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = firstSixDaysOfFirstWeek.first().date..firstSixDaysOfFirstWeek.last().date, - ) - ).isEqualTo(firstSixDaysOfFirstWeek) - - val firstSixDaysOfFirstWeekExpectedStreaks = listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 2), - endDate = LocalDate(2023, Month.OCTOBER, 5), - ) - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .isEqualTo(firstSixDaysOfFirstWeekExpectedStreaks) - - insertRoutineStatus( - routineId = routineId, - currentDate = LocalDate(2023, Month.OCTOBER, 8), - completedOnCurrentDate = completionHistory[6], - today = today, - ) - - val firstWeek = mutableListOf() - firstWeek.addAll(firstSixDaysOfFirstWeek) - firstWeek.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 8), - status = HistoricalStatus.SortedOutBacklog, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - firstWeek[5] = firstWeek[5].copy( - status = HistoricalStatus.CompletedLater, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId, firstWeek.first().date..firstWeek.last().date - ) - ).isEqualTo(firstWeek) - - val firstWeekExpectedSteaks = listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 2), - endDate = LocalDate(2023, Month.OCTOBER, 5), - ), - Streak( - id = 2, - startDate = LocalDate(2023, Month.OCTOBER, 7), - endDate = null, - ) - ) - assertThat(streakRepository.getAllStreaks(routineId)).isEqualTo(firstWeekExpectedSteaks) - - val secondWeek = listOf( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 9), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 10), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 11), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 12), - status = HistoricalStatus.OverCompleted, - scheduleDeviation = 1F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 13), - status = HistoricalStatus.AlreadyCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 14), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 15), - status = HistoricalStatus.OverCompleted, - scheduleDeviation = 1F, - timesCompleted = 1F, - ), - ) - - completionHistory.slice(7..13).forEachIndexed { index, isCompleted -> - insertRoutineStatus( - routineId = routineId, - currentDate = routineStartDate.plusDays(7 + index), - completedOnCurrentDate = isCompleted, - today = today, - ) - } - - val fullHistorySoFar = mutableListOf() - fullHistorySoFar.addAll(firstWeek) - fullHistorySoFar.addAll(secondWeek) - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId, routineStartDate..fullHistorySoFar.last().date - ) - ).isEqualTo(fullHistorySoFar) - - val lastTwoDays = listOf( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 16), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 17), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ), - ) - fullHistorySoFar.addAll(lastTwoDays) - - completionHistory.slice(14..15).forEachIndexed { index, isCompleted -> - insertRoutineStatus( - routineId = routineId, - currentDate = routineStartDate.plusDays(14 + index), - completedOnCurrentDate = isCompleted, - today = today, - ) - } - - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId, routineStartDate..fullHistorySoFar.last().date - ) - ).isEqualTo(fullHistorySoFar) - - val fullStreakHistory = listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 2), - endDate = LocalDate(2023, Month.OCTOBER, 5), - ), - Streak( - id = 2, - startDate = LocalDate(2023, Month.OCTOBER, 7), - endDate = LocalDate(2023, Month.OCTOBER, 16), - ) - ) - - assertThat( - streakRepository.getAllStreaks(routineId) - ).isEqualTo(fullStreakHistory) - } - - @Test - fun `weekly schedule, due on explicit days of week, periodic separation disabled`() = runTest { - val routineId = 1L - val routineStartDate = LocalDate(2023, Month.OCTOBER, 2) // Monday - - val schedule = Schedule.WeeklyScheduleByDueDaysOfWeek( - routineStartDate = routineStartDate, - backlogEnabled = true, - cancelDuenessIfDoneAhead = true, - periodSeparationEnabled = false, - dueDaysOfWeek = listOf( - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.FRIDAY, - DayOfWeek.SATURDAY, - ), - startDayOfWeek = DayOfWeek.MONDAY, - ) - - val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = schedule, - ) - - routineRepository.insertRoutine(routine) - - val completionHistory = listOf( - false, // Monday - true, // Tuesday - true, // Wednesday - false, // Thursday - false, // Friday + backlog - false, // Saturday + backlog - true, // Sunday - backlog - - false, // Monday - true, // Tuesday - true, // Wednesday - true, // Thursday - backlog - false, // Friday + backlog - true, // Saturday - true, // Sunday - backlog - - false, // Monday - false, // Tuesday + backlog - ) - - val firstSixDaysOfFirstWeek = listOf( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 2), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 3), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 4), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 5), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 6), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 7), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ), - ) - - completionHistory.slice(0..5).forEachIndexed { index, isCompleted -> - insertRoutineStatus( - routineId = routineId, - currentDate = routineStartDate.plusDays(index), - completedOnCurrentDate = isCompleted, - today = today, - ) - } - - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = firstSixDaysOfFirstWeek.first().date..firstSixDaysOfFirstWeek.last().date, - ) - ).isEqualTo(firstSixDaysOfFirstWeek) - - val firstSixDaysOfFirstWeekStreaks = listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 2), - endDate = LocalDate(2023, Month.OCTOBER, 5), - ) - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .isEqualTo(firstSixDaysOfFirstWeekStreaks) - - insertRoutineStatus( - routineId = routineId, - currentDate = LocalDate(2023, Month.OCTOBER, 8), - completedOnCurrentDate = completionHistory[6], - today = today, - ) - - val firstWeek = mutableListOf() - firstWeek.addAll(firstSixDaysOfFirstWeek) - firstWeek.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 8), - status = HistoricalStatus.SortedOutBacklog, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - firstWeek[5] = firstWeek[5].copy(status = HistoricalStatus.CompletedLater) - - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId, firstWeek.first().date..firstWeek.last().date - ) - ).isEqualTo(firstWeek) - - val firstWeekStreaks = listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 2), - endDate = LocalDate(2023, Month.OCTOBER, 5), - ), - Streak( - id = 2, - startDate = LocalDate(2023, Month.OCTOBER, 7), - endDate = null, - ) - ) - assertThat(streakRepository.getAllStreaks(routineId)).isEqualTo(firstWeekStreaks) - - val secondWeek = listOf( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 9), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 10), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 11), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 12), - status = HistoricalStatus.SortedOutBacklog, - scheduleDeviation = 1F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 13), - status = HistoricalStatus.CompletedLater, - scheduleDeviation = -1F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 14), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 15), - status = HistoricalStatus.SortedOutBacklog, - scheduleDeviation = 1F, - timesCompleted = 1F, - ), - ) - - completionHistory.slice(7..13).forEachIndexed { index, isCompleted -> - insertRoutineStatus( - routineId = routineId, - currentDate = routineStartDate.plusDays(7 + index), - completedOnCurrentDate = isCompleted, - today = today, - ) - } - - val fullHistorySoFar = mutableListOf() - fullHistorySoFar.addAll(firstWeek) - fullHistorySoFar.addAll(secondWeek) - fullHistorySoFar[4] = fullHistorySoFar[4].copy(status = HistoricalStatus.CompletedLater) - - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId, routineStartDate..fullHistorySoFar.last().date - ) - ).isEqualTo(fullHistorySoFar) - - val secondWeekStreaks = listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 2), - endDate = null, - ), - ) - assertThat(streakRepository.getAllStreaks(routineId)).isEqualTo(secondWeekStreaks) - - val lastTwoDays = listOf( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 16), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 17), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ), - ) - fullHistorySoFar.addAll(lastTwoDays) - - completionHistory.slice(14..15).forEachIndexed { index, isCompleted -> - insertRoutineStatus( - routineId = routineId, - currentDate = routineStartDate.plusDays(14 + index), - completedOnCurrentDate = isCompleted, - today = today, - ) - } - - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = routineStartDate..fullHistorySoFar.last().date, - ) - ).isEqualTo(fullHistorySoFar) - - println(fullHistorySoFar) - - val expectedStreaks = listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 2), - endDate = LocalDate(2023, Month.OCTOBER, 16), - ) - ) - assertThat(streakRepository.getAllStreaks(routineId)).isEqualTo(expectedStreaks) - } - - @Test - fun `custom date schedule, backlog enabled, completing ahead disabled`() = runTest { - val routineId = 1L - val routineStartDate = LocalDate(2023, Month.OCTOBER, 2) // Monday - - val schedule = Schedule.CustomDateSchedule( - routineStartDate = routineStartDate, - backlogEnabled = true, - cancelDuenessIfDoneAhead = false, - dueDates = listOf( - routineStartDate.plusDays(1), - routineStartDate.plusDays(2), - routineStartDate.plusDays(7), - routineStartDate.plusDays(12), - routineStartDate.plusDays(14), - ) - ) - - val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = schedule, - ) - - routineRepository.insertRoutine(routine) - - val completionHistory = listOf( - false, - true, - false, // + backlog - false, - false, - true, // - backlog - false, - true, - false, - true, // + done ahead - false, - false, - false, // + backlog, because completing ahead disabled - false, - true, - ) - - completionHistory.slice(0..4).forEachIndexed { index, isCompleted -> - insertRoutineStatus( - routineId = routineId, - currentDate = routineStartDate.plusDays(index), - completedOnCurrentDate = isCompleted, - today = today, - ) - } - - val expectedEntriesList = mutableListOf( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 2), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 3), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 4), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 5), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 6), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - ) - - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId, routineStartDate..routineStartDate.plusDays(4) - ) - ).isEqualTo(expectedEntriesList) - - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 7), - status = HistoricalStatus.SortedOutBacklog, - scheduleDeviation = 1F, - timesCompleted = 1F, - ), - ) - expectedEntriesList[2] = CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 4), - status = HistoricalStatus.CompletedLater, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - - completionHistory.slice(5..14).forEachIndexed { index, isCompleted -> - insertRoutineStatus( - routineId = routineId, - currentDate = routineStartDate.plusDays(5 + index), - completedOnCurrentDate = isCompleted, - today = today, - ) - } - - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 8), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 9), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 10), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 11), - status = HistoricalStatus.OverCompleted, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 12), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 13), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 14), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 15), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 16), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = routineStartDate..routineStartDate.plusDays(14) - ) - ).isEqualTo(expectedEntriesList) - } - - @Test - fun `custom date schedule, backlog disabled, completing ahead enabled`() = runTest { - val routineId = 1L - val routineStartDate = LocalDate(2023, Month.OCTOBER, 2) // Monday - - val schedule = Schedule.CustomDateSchedule( - routineStartDate = routineStartDate, - backlogEnabled = false, - cancelDuenessIfDoneAhead = true, - dueDates = listOf( - routineStartDate.plusDays(1), - routineStartDate.plusDays(2), - routineStartDate.plusDays(7), - routineStartDate.plusDays(12), - routineStartDate.plusDays(14), - ) - ) - - val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = schedule, - ) - - routineRepository.insertRoutine(routine) - - val completionHistory = listOf( - false, - true, - false, // not completed but no backlog, because it's disabled - false, - false, - true, // + done ahead - false, - true, - false, - true, // + done ahead - false, - false, - false, // - done ahead - false, - true, - ) - - completionHistory.slice(0..14).forEachIndexed { index, isCompleted -> - insertRoutineStatus( - routineId = routineId, - currentDate = routineStartDate.plusDays(index), - completedOnCurrentDate = isCompleted, - today = today, - ) - } - - val expectedEntriesList = listOf( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 2), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 3), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 4), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 5), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 6), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 7), - status = HistoricalStatus.OverCompleted, - scheduleDeviation = 1F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 8), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 9), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 10), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 11), - status = HistoricalStatus.OverCompleted, - scheduleDeviation = 1F, - timesCompleted = 1F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 12), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 13), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 14), - status = HistoricalStatus.AlreadyCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 15), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ), - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 16), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ), - ) - - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId, - routineStartDate..routineStartDate.plusDays(14) - ) - ).isEqualTo(expectedEntriesList) - } - - @Test - fun `monthly schedule, not completed for a long time`() = runTest { - val routineId = 1L - val routineStartDate = LocalDate(2023, Month.OCTOBER, 2) // Monday - - val schedule = Schedule.MonthlyScheduleByDueDatesIndices( - dueDatesIndices = listOf(1, 6, 9, 11, 26, 30), - includeLastDayOfMonth = true, - weekDaysMonthRelated = listOf( - WeekDayMonthRelated(DayOfWeek.TUESDAY, WeekDayNumberMonthRelated.Third), - WeekDayMonthRelated(DayOfWeek.TUESDAY, WeekDayNumberMonthRelated.Forth), - WeekDayMonthRelated(DayOfWeek.THURSDAY, WeekDayNumberMonthRelated.Fifth), - ), - startFromRoutineStart = false, - periodSeparationEnabled = false, - backlogEnabled = true, - cancelDuenessIfDoneAhead = true, - routineStartDate = routineStartDate, - vacationStartDate = LocalDate(2023, Month.OCTOBER, 16), - vacationEndDate = LocalDate(2023, Month.OCTOBER, 22), - ) - - val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = schedule, - ) - - routineRepository.insertRoutine(routine) - - val completionHistory = mutableListOf() - repeat(17) { completionHistory.add(false) } - repeat(2) { completionHistory.add(true) } - repeat(15) { completionHistory.add(false) } - completionHistory.forEachIndexed { index, isCompleted -> - insertRoutineStatus( - routineId = routineId, - currentDate = routineStartDate.plusDays(index), - completedOnCurrentDate = isCompleted, - today = today, - ) - } - - val expectedEntriesList = mutableListOf() - repeat(4) { - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, it + 2), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - } - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 6), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - repeat(2) { - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, it + 7), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - } - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 9), - status = HistoricalStatus.CompletedLater, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 10), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 11), - status = HistoricalStatus.CompletedLater, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - repeat(4) { - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, it + 12), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - } - repeat(3) { - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, it + 16), - status = HistoricalStatus.NotCompletedOnVacation, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - } - repeat(2) { - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, it + 19), - status = HistoricalStatus.SortedOutBacklogOnVacation, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - } - repeat(2) { - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, it + 21), - status = HistoricalStatus.NotCompletedOnVacation, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - } - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 23), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 24), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 25), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, 26), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - repeat(3) { - expectedEntriesList.add( - CompletionHistoryEntry( - date = LocalDate(2023, Month.OCTOBER, it + 27), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - } - repeat(3) { - expectedEntriesList.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(it + 28), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - } - repeat(3) { - expectedEntriesList.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(it + 31), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - } - - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId, - routineStartDate..routineStartDate.plusDays(33) - ) - ).isEqualTo(expectedEntriesList) - - val expectedStreaks = listOf( - Streak( - id = 1, - startDate = LocalDate(2023, Month.OCTOBER, 9), - endDate = LocalDate(2023, Month.OCTOBER, 23), - ) - ) - assertThat(streakRepository.getAllStreaks(routineId)).isEqualTo(expectedStreaks) - } - - @Test - fun testPeriodicCustomSchedule() = runTest { - val routineId = 1L - val routineStartDate = LocalDate(2023, Month.NOVEMBER, 1) - - val schedule = Schedule.PeriodicCustomSchedule( - numOfDueDays = 1, - numOfDaysInPeriod = 2, - backlogEnabled = true, - cancelDuenessIfDoneAhead = true, - routineStartDate = routineStartDate, - vacationStartDate = routineStartDate.plusDays(8), - vacationEndDate = routineStartDate.plusDays(10), - ) - - val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = schedule, - ) - - routineRepository.insertRoutine(routine) - - val history = mutableListOf() - - history.add( - CompletionHistoryEntry( - date = routineStartDate, - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(1), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(2), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(3), - status = HistoricalStatus.OverCompleted, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(4), - status = HistoricalStatus.AlreadyCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(5), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(6), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(7), - status = HistoricalStatus.CompletedLater, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(8), - status = HistoricalStatus.NotCompletedOnVacation, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(9), - status = HistoricalStatus.SortedOutBacklogOnVacation, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(10), - status = HistoricalStatus.OverCompletedOnVacation, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(11), - status = HistoricalStatus.AlreadyCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(12), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(13), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(14), - status = HistoricalStatus.CompletedLater, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(15), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(16), - status = HistoricalStatus.SortedOutBacklog, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(17), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(18), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(19), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(20), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - - val completion = listOf( - true, - false, - true, - true, - false, - false, - false, - false, - false, - true, - true, - false, - false, - false, - false, - true, - true, - false, - false, - false, - false, - ) - - val today = routineStartDate.plusDays(22) - completion.forEachIndexed { index, completed -> - insertRoutineStatus( - routineId = routineId, - currentDate = routineStartDate.plusDays(index), - completedOnCurrentDate = completed, - today = today, - ) - } - - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = routineStartDate..routineStartDate.plusDays(21), - ) - ).containsExactlyElementsIn(history) - - val expectedStreaks = listOf( - Streak( - id = 1, - startDate = routineStartDate, - endDate = routineStartDate.plusDays(17), - ) - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(expectedStreaks) - } -} \ No newline at end of file diff --git a/core/domain/src/test/java/com/rendox/routinetracker/core/domain/routine/schedule/ScheduleGetPeriodRangeTest.kt b/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleGetPeriodRangeTest.kt similarity index 89% rename from core/domain/src/test/java/com/rendox/routinetracker/core/domain/routine/schedule/ScheduleGetPeriodRangeTest.kt rename to core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleGetPeriodRangeTest.kt index 4c98606e..2338f9d1 100644 --- a/core/domain/src/test/java/com/rendox/routinetracker/core/domain/routine/schedule/ScheduleGetPeriodRangeTest.kt +++ b/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleGetPeriodRangeTest.kt @@ -1,7 +1,6 @@ -package com.rendox.routinetracker.core.domain.routine.schedule +package com.rendox.routinetracker.core.domain.completion_history import com.google.common.truth.Truth.assertThat -import com.rendox.routinetracker.core.domain.completion_history.getPeriodRange import com.rendox.routinetracker.core.logic.time.LocalDateRange import com.rendox.routinetracker.core.logic.time.atEndOfMonth import com.rendox.routinetracker.core.logic.time.plusDays @@ -25,7 +24,7 @@ class ScheduleGetPeriodRangeTest { val schedule = Schedule.WeeklyScheduleByDueDaysOfWeek( dueDaysOfWeek = emptyList(), startDayOfWeek = null, - routineStartDate = routineStartDate, + startDate = routineStartDate, ) // still first week @@ -47,7 +46,7 @@ class ScheduleGetPeriodRangeTest { val schedule = Schedule.WeeklyScheduleByDueDaysOfWeek( dueDaysOfWeek = emptyList(), startDayOfWeek = DayOfWeek.SUNDAY, - routineStartDate = routineStartDate, + startDate = routineStartDate, ) val firstWeek = routineStartDate..LocalDate(2023, Month.OCTOBER, 21) @@ -63,10 +62,10 @@ class ScheduleGetPeriodRangeTest { val routineStartDate = LocalDate(2023, Month.OCTOBER, 18) val schedule = Schedule.MonthlyScheduleByDueDatesIndices( - routineStartDate = routineStartDate, + startDate = routineStartDate, dueDatesIndices = emptyList(), weekDaysMonthRelated = emptyList(), - startFromRoutineStart = true, + startFromHabitStart = true, ) val firstMonth = routineStartDate..routineStartDate @@ -86,10 +85,10 @@ class ScheduleGetPeriodRangeTest { val routineStartDate = LocalDate(2023, Month.OCTOBER, 18) val schedule = Schedule.MonthlyScheduleByDueDatesIndices( - routineStartDate = routineStartDate, + startDate = routineStartDate, dueDatesIndices = emptyList(), weekDaysMonthRelated = emptyList(), - startFromRoutineStart = false, + startFromHabitStart = false, ) val firstMonth = routineStartDate..routineStartDate.atEndOfMonth @@ -105,9 +104,9 @@ class ScheduleGetPeriodRangeTest { val routineStartDate = LocalDate(2024, Month.FEBRUARY, 29) val schedule = Schedule.AnnualScheduleByDueDates( - routineStartDate = routineStartDate, + startDate = routineStartDate, dueDates = emptyList(), - startFromRoutineStart = true, + startFromHabitStart = true, ) val firstYear = routineStartDate..LocalDate(2025, Month.FEBRUARY, 28) @@ -125,9 +124,9 @@ class ScheduleGetPeriodRangeTest { val routineStartDate = LocalDate(2023, Month.MARCH, 1) val schedule = Schedule.AnnualScheduleByDueDates( - routineStartDate = routineStartDate, + startDate = routineStartDate, dueDates = emptyList(), - startFromRoutineStart = true, + startFromHabitStart = true, ) val firstYear = routineStartDate..LocalDate(2024, Month.FEBRUARY, 29) @@ -145,9 +144,9 @@ class ScheduleGetPeriodRangeTest { val routineStartDate = LocalDate(2023, Month.SEPTEMBER, 30) val schedule = Schedule.AnnualScheduleByDueDates( - routineStartDate = routineStartDate, + startDate = routineStartDate, dueDates = emptyList(), - startFromRoutineStart = true, + startFromHabitStart = true, ) val firstYear = routineStartDate..LocalDate(2024, Month.SEPTEMBER, 29) @@ -165,9 +164,9 @@ class ScheduleGetPeriodRangeTest { val routineStartDate = LocalDate(2023, Month.SEPTEMBER, 30) val schedule = Schedule.AnnualScheduleByDueDates( - routineStartDate = routineStartDate, + startDate = routineStartDate, dueDates = emptyList(), - startFromRoutineStart = false, + startFromHabitStart = false, ) val firstYear = routineStartDate..LocalDate(2023, Month.DECEMBER, 31) @@ -186,8 +185,8 @@ class ScheduleGetPeriodRangeTest { val numOfDaysInPeriod = Random.nextInt(99) - val schedule = Schedule.PeriodicCustomSchedule( - routineStartDate = routineStartDate, + val schedule = Schedule.AlternateDaysSchedule( + startDate = routineStartDate, numOfDueDays = 0, numOfDaysInPeriod = numOfDaysInPeriod, ) @@ -211,8 +210,8 @@ class ScheduleGetPeriodRangeTest { fun assertPeriodicCustomScheduleRestartsPeriodAfterVacation() { val routineStartDate = LocalDate(2023, Month.NOVEMBER, 1) - val schedule = Schedule.PeriodicCustomSchedule( - routineStartDate = routineStartDate, + val schedule = Schedule.AlternateDaysSchedule( + startDate = routineStartDate, numOfDueDays = 1, numOfDaysInPeriod = 2, vacationStartDate = LocalDate(2023, Month.NOVEMBER, 3), @@ -242,8 +241,8 @@ class ScheduleGetPeriodRangeTest { fun assertPeriodicScheduleDoesNotRestartPeriodBeforeVacation() { val routineStartDate = LocalDate(2023, Month.NOVEMBER, 1) - val schedule = Schedule.PeriodicCustomSchedule( - routineStartDate = routineStartDate, + val schedule = Schedule.AlternateDaysSchedule( + startDate = routineStartDate, numOfDueDays = 1, numOfDaysInPeriod = 2, vacationStartDate = LocalDate(2023, Month.NOVEMBER, 3), diff --git a/core/domain/src/test/java/com/rendox/routinetracker/core/domain/routine/schedule/ScheduleIsDueTest.kt b/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleIsDueTest.kt similarity index 90% rename from core/domain/src/test/java/com/rendox/routinetracker/core/domain/routine/schedule/ScheduleIsDueTest.kt rename to core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleIsDueTest.kt index 74160aa1..66204ec9 100644 --- a/core/domain/src/test/java/com/rendox/routinetracker/core/domain/routine/schedule/ScheduleIsDueTest.kt +++ b/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/ScheduleIsDueTest.kt @@ -1,7 +1,6 @@ -package com.rendox.routinetracker.core.domain.routine.schedule +package com.rendox.routinetracker.core.domain.completion_history import com.google.common.truth.Truth.assertThat -import com.rendox.routinetracker.core.domain.completion_history.isDue import com.rendox.routinetracker.core.logic.time.AnnualDate import com.rendox.routinetracker.core.logic.time.WeekDayMonthRelated import com.rendox.routinetracker.core.logic.time.WeekDayNumberMonthRelated @@ -25,7 +24,7 @@ class ScheduleIsDueTest { @Test fun everyDayScheduleIsDue() { val schedule: Schedule = Schedule.EveryDaySchedule( - routineStartDate = routineStartDate, + startDate = routineStartDate, ) val date1 = LocalDate(2023, (1..12).random(), (1..28).random()) val date2 = LocalDate(2024, Month.FEBRUARY, 29) @@ -49,11 +48,11 @@ class ScheduleIsDueTest { DayOfWeek.SATURDAY, ) val schedule1: Schedule = Schedule.WeeklyScheduleByDueDaysOfWeek( - routineStartDate = routineStartDate, + startDate = routineStartDate, dueDaysOfWeek = dueDaysOfWeek1, ) val schedule2: Schedule = Schedule.WeeklyScheduleByDueDaysOfWeek( - routineStartDate = routineStartDate, + startDate = routineStartDate, dueDaysOfWeek = dueDaysOfWeek2, ) @@ -86,7 +85,7 @@ class ScheduleIsDueTest { fun `weekly schedule, first period is short, assert fails with an exception`() { assertFailsWith { Schedule.WeeklyScheduleByNumOfDueDays( - routineStartDate = routineStartDate, + startDate = routineStartDate, numOfDueDays = 5, startDayOfWeek = DayOfWeek.MONDAY, numOfDueDaysInFirstPeriod = null, @@ -99,13 +98,13 @@ class ScheduleIsDueTest { val numOfDueDays = 5 val schedule = Schedule.WeeklyScheduleByNumOfDueDays( - routineStartDate = routineStartDate, + startDate = routineStartDate, numOfDueDays = numOfDueDays, startDayOfWeek = null, numOfDueDaysInFirstPeriod = null, ) - val futurePeriodStart = schedule.routineStartDate.plusDays(DateTimeUnit.WEEK.days) + val futurePeriodStart = schedule.startDate.plusDays(DateTimeUnit.WEEK.days) for (dayIndex in 0 until numOfDueDays) { assertThat( schedule.isDue(validationDate = futurePeriodStart.plusDays(dayIndex)) @@ -122,49 +121,49 @@ class ScheduleIsDueTest { @Test fun `weekly schedule due X days per week, assert works fine when first period is short`() { val schedule = Schedule.WeeklyScheduleByNumOfDueDays( - routineStartDate = LocalDate(2023, Month.NOVEMBER, 1), // wednesday + startDate = LocalDate(2023, Month.NOVEMBER, 1), // wednesday numOfDueDays = 4, startDayOfWeek = DayOfWeek.MONDAY, numOfDueDaysInFirstPeriod = 3, ) for (dayIndex in 0..2) { - val currentDate = schedule.routineStartDate.plusDays(dayIndex) + val currentDate = schedule.startDate.plusDays(dayIndex) assertThat( schedule.isDue(validationDate = currentDate) ).isTrue() } for (dayIndex in 3..4) { - val currentDate = schedule.routineStartDate.plusDays(dayIndex) + val currentDate = schedule.startDate.plusDays(dayIndex) assertThat( schedule.isDue(validationDate = currentDate) ).isFalse() } for (dayIndex in 5..8) { - val currentDate = schedule.routineStartDate.plusDays(dayIndex) + val currentDate = schedule.startDate.plusDays(dayIndex) assertThat( schedule.isDue(validationDate = currentDate) ).isTrue() } for (dayIndex in 9..11) { - val currentDate = schedule.routineStartDate.plusDays(dayIndex) + val currentDate = schedule.startDate.plusDays(dayIndex) assertThat( schedule.isDue(validationDate = currentDate) ).isFalse() } for (dayIndex in 12..15) { - val currentDate = schedule.routineStartDate.plusDays(dayIndex) + val currentDate = schedule.startDate.plusDays(dayIndex) assertThat( schedule.isDue(validationDate = currentDate) ).isTrue() } for (dayIndex in 16..18) { - val currentDate = schedule.routineStartDate.plusDays(dayIndex) + val currentDate = schedule.startDate.plusDays(dayIndex) assertThat( schedule.isDue(validationDate = currentDate) ).isFalse() @@ -194,9 +193,9 @@ class ScheduleIsDueTest { val schedule: Schedule = Schedule.MonthlyScheduleByDueDatesIndices( dueDatesIndices = dueDatesIndices, includeLastDayOfMonth = false, - startFromRoutineStart = true, + startFromHabitStart = true, weekDaysMonthRelated = emptyList(), - routineStartDate = routineStartDate, + startDate = routineStartDate, ) for (dueDate in dueDates) { @@ -225,9 +224,9 @@ class ScheduleIsDueTest { val schedule = Schedule.MonthlyScheduleByDueDatesIndices( dueDatesIndices = emptyList(), includeLastDayOfMonth = true, - startFromRoutineStart = true, + startFromHabitStart = true, weekDaysMonthRelated = emptyList(), - routineStartDate = routineStartDate, + startDate = routineStartDate, ) assertThat(schedule.isDue(februaryHeapYearLastDate, null)).isTrue() @@ -253,8 +252,8 @@ class ScheduleIsDueTest { WeekDayMonthRelated(DayOfWeek.WEDNESDAY, WeekDayNumberMonthRelated.Forth), WeekDayMonthRelated(DayOfWeek.THURSDAY, WeekDayNumberMonthRelated.Fifth), ), - startFromRoutineStart = true, - routineStartDate = routineStartDate, + startFromHabitStart = true, + startDate = routineStartDate, ) val firstMonday1 = LocalDate(2024, Month.FEBRUARY, 5) @@ -289,9 +288,9 @@ class ScheduleIsDueTest { assertFailsWith { // should fail because routine start date is not the first day of month Schedule.MonthlyScheduleByNumOfDueDays( - routineStartDate = routineStartDate.plusDays(Random.nextInt(1, 27)), + startDate = routineStartDate.plusDays(Random.nextInt(1, 27)), numOfDueDays = 18, - startFromRoutineStart = false, + startFromHabitStart = false, numOfDueDaysInFirstPeriod = null, ) } @@ -300,9 +299,9 @@ class ScheduleIsDueTest { @Test fun `MonthlyScheduleByNumOfDueDays, start from routine start, due on specified days`() { val schedule = Schedule.MonthlyScheduleByNumOfDueDays( - routineStartDate = routineStartDate, + startDate = routineStartDate, numOfDueDays = Random.nextInt(2, 30), - startFromRoutineStart = true, + startFromHabitStart = true, numOfDueDaysInFirstPeriod = null, ) @@ -324,8 +323,8 @@ class ScheduleIsDueTest { @Test fun `MonthlyScheduleByNumOfDueDays, assert num of due days more than 28 doesn't introduce backlog`() { val schedule = Schedule.MonthlyScheduleByNumOfDueDays( - routineStartDate = routineStartDate, - startFromRoutineStart = true, + startDate = routineStartDate, + startFromHabitStart = true, numOfDueDaysInFirstPeriod = null, numOfDueDays = 29, ) @@ -364,10 +363,10 @@ class ScheduleIsDueTest { val numOfDaysInPeriod = Random.nextInt(2, 100) val dueDaysNumber = numOfDaysInPeriod / 2 - val schedule: Schedule = Schedule.PeriodicCustomSchedule( + val schedule: Schedule = Schedule.AlternateDaysSchedule( numOfDueDays = dueDaysNumber, numOfDaysInPeriod = numOfDaysInPeriod, - routineStartDate = routineStartDate, + startDate = routineStartDate, ) for (dueDayNumber in 1..dueDaysNumber) { @@ -441,7 +440,7 @@ class ScheduleIsDueTest { notDueDates.add(LocalDate(2024, Month.SEPTEMBER, 30)) val schedule = Schedule.CustomDateSchedule( - routineStartDate = routineStartDate, + startDate = routineStartDate, dueDates = dueDates, ) @@ -533,8 +532,8 @@ class ScheduleIsDueTest { val schedule = Schedule.AnnualScheduleByDueDates( dueDates = dueDates, - routineStartDate = routineStartDate, - startFromRoutineStart = false, + startDate = routineStartDate, + startFromHabitStart = false, ) for (dueDate in expectedDueDates) { @@ -554,8 +553,8 @@ class ScheduleIsDueTest { fun `AnnualScheduleByNumOfDueDays, first period is short, fails with an exception`() { assertFailsWith { Schedule.AnnualScheduleByNumOfDueDays( - routineStartDate = routineStartDate.plusDays(Random.nextInt(1, 365)), - startFromRoutineStart = false, + startDate = routineStartDate.plusDays(Random.nextInt(1, 365)), + startFromHabitStart = false, numOfDueDays = 0, numOfDueDaysInFirstPeriod = null, ) @@ -566,9 +565,9 @@ class ScheduleIsDueTest { fun `AnnualScheduleByNumOfDueDays, start from routine start, due on specified days`() { val schedule = Schedule.AnnualScheduleByNumOfDueDays( - routineStartDate = routineStartDate, + startDate = routineStartDate, numOfDueDays = Random.nextInt(2, 364), - startFromRoutineStart = true, + startFromHabitStart = true, numOfDueDaysInFirstPeriod = null, ) diff --git a/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/ToggleHistoricalStatusUseCaseTest.kt b/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/ToggleHistoricalStatusUseCaseTest.kt deleted file mode 100644 index 0face1d9..00000000 --- a/core/domain/src/test/java/com/rendox/routinetracker/core/domain/completion_history/ToggleHistoricalStatusUseCaseTest.kt +++ /dev/null @@ -1,1036 +0,0 @@ -package com.rendox.routinetracker.core.domain.completion_history - -import com.google.common.truth.Truth.assertThat -import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository -import com.rendox.routinetracker.core.data.di.completionHistoryDataModule -import com.rendox.routinetracker.core.data.di.routineDataModule -import com.rendox.routinetracker.core.data.di.streakDataModule -import com.rendox.routinetracker.core.data.routine.RoutineRepository -import com.rendox.routinetracker.core.data.streak.StreakRepository -import com.rendox.routinetracker.core.database.completion_history.CompletionHistoryLocalDataSource -import com.rendox.routinetracker.core.database.routine.RoutineLocalDataSource -import com.rendox.routinetracker.core.database.streak.StreakLocalDataSource -import com.rendox.routinetracker.core.domain.completion_history.use_cases.ToggleHistoricalStatusUseCase -import com.rendox.routinetracker.core.domain.di.streakDomainModule -import com.rendox.routinetracker.core.logic.time.plusDays -import com.rendox.routinetracker.core.logic.time.rangeTo -import com.rendox.routinetracker.core.model.CompletionHistoryEntry -import com.rendox.routinetracker.core.model.HistoricalStatus -import com.rendox.routinetracker.core.model.Routine -import com.rendox.routinetracker.core.model.Schedule -import com.rendox.routinetracker.core.model.Streak -import com.rendox.routinetracker.core.testcommon.fakes.routine.CompletionHistoryLocalDataSourceFake -import com.rendox.routinetracker.core.testcommon.fakes.routine.RoutineData -import com.rendox.routinetracker.core.testcommon.fakes.routine.RoutineLocalDataSourceFake -import com.rendox.routinetracker.core.testcommon.fakes.routine.StreakLocalDataSourceFake -import kotlinx.coroutines.test.runTest -import kotlinx.datetime.LocalDate -import kotlinx.datetime.Month -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.koin.core.context.startKoin -import org.koin.core.context.stopKoin -import org.koin.dsl.module -import org.koin.test.KoinTest -import org.koin.test.get - -class ToggleHistoricalStatusUseCaseTest : KoinTest { - - private lateinit var routineRepository: RoutineRepository - private lateinit var completionHistoryRepository: CompletionHistoryRepository - private lateinit var toggleHistoricalStatus: ToggleHistoricalStatusUseCase - private lateinit var streakRepository: StreakRepository - - private val testModule = module { - single { RoutineData() } - - single { - RoutineLocalDataSourceFake(routineData = get()) - } - - single { - CompletionHistoryLocalDataSourceFake(routineData = get()) - } - - single { - StreakLocalDataSourceFake(routineData = get()) - } - } - - private val routineId = 1L - private val routineStartDate = LocalDate(2023, Month.NOVEMBER, 1) - private val routineEndDate = routineStartDate.plusDays(20) - private val schedule = Schedule.PeriodicCustomSchedule( - numOfDueDays = 1, - numOfDaysInPeriod = 2, - backlogEnabled = true, - cancelDuenessIfDoneAhead = true, - routineStartDate = routineStartDate, - routineEndDate = routineEndDate, - vacationStartDate = routineStartDate.plusDays(8), - vacationEndDate = routineStartDate.plusDays(10), - ) - private val routine = Routine.YesNoRoutine( - id = routineId, - name = "", - schedule = schedule, - ) - - private lateinit var initialHistory: List - private lateinit var initialStreaks: List - - @Before - fun setUp() = runTest { - startKoin { - modules( - routineDataModule, - completionHistoryDataModule, - streakDataModule, - streakDomainModule, - testModule, - ) - } - - routineRepository = get() - completionHistoryRepository = get() - streakRepository = get() - - toggleHistoricalStatus = ToggleHistoricalStatusUseCase( - completionHistoryRepository = completionHistoryRepository, - routineRepository = routineRepository, - startStreakOrJoinStreaks = get(), - breakStreak = get(), - deleteStreakIfStarted = get(), - continueStreakIfEnded = get(), - ) - - routineRepository.insertRoutine(routine) - - val history = mutableListOf() - - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(0), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(1), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(2), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(3), - status = HistoricalStatus.OverCompleted, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(4), - status = HistoricalStatus.AlreadyCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(5), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(6), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(7), - status = HistoricalStatus.CompletedLater, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(8), - status = HistoricalStatus.NotCompletedOnVacation, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(9), - status = HistoricalStatus.SortedOutBacklogOnVacation, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(10), - status = HistoricalStatus.OverCompletedOnVacation, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(11), - status = HistoricalStatus.AlreadyCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(12), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(13), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(14), - status = HistoricalStatus.CompletedLater, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(15), - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(16), - status = HistoricalStatus.SortedOutBacklog, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(17), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(18), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(19), - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - history.add( - CompletionHistoryEntry( - date = routineStartDate.plusDays(20), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - - initialHistory = history - initialStreaks = listOf( - Streak( - id = 1, - startDate = routineStartDate, - endDate = routineStartDate.plusDays(17), - ) - ) - - for (entry in history) { - completionHistoryRepository.insertHistoryEntry( - routineId = routineId, - entry = entry, - ) - } - - for (streak in initialStreaks) { - streakRepository.insertStreak( - routineId = routineId, - streak = Streak( - startDate = streak.startDate, - endDate = streak.endDate, - ) - ) - } - } - - @After - fun tearDown() { - stopKoin() - } - - @Test - fun `toggle first (Completed) entry back and forth, assert statuses change correctly`() = runTest { - val date = routineStartDate - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntryByDate(routineId, date) - ).isEqualTo( - CompletionHistoryEntry( - date = date, - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - - assertThat( - completionHistoryRepository.getHistoryEntryByDate(routineId, date) - ).isEqualTo( - CompletionHistoryEntry( - date = date, - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - } - - @Test - fun `toggle first (Completed) entry back and forth, assert streaks change correctly`() = runTest { - val date = routineStartDate - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - val newStreaks: List = initialStreaks.toMutableList().also { - it[0] = it[0].copy(startDate = routineStartDate.plusDays(2)) - } - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(newStreaks) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - } - - @Test - fun `toggle second (Skipped) entry back and forth, assert statuses change correctly`() = runTest { - val date = routineStartDate.plusDays(1) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntryByDate(routineId, date) - ).isEqualTo( - CompletionHistoryEntry( - date = date, - status = HistoricalStatus.OverCompleted, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - } - - @Test - fun `toggle second (Skipped) entry back and forth, assert streaks change correctly`() = runTest { - val date = routineStartDate.plusDays(1) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - } - - @Test - fun `toggle third (Completed) entry back and forth, assert statuses change correctly`() = runTest { - val date = routineStartDate.plusDays(2) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntryByDate(routineId, date) - ).isEqualTo( - CompletionHistoryEntry( - date = date, - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - - assertThat( - completionHistoryRepository.getHistoryEntryByDate(routineId, date) - ).isEqualTo( - CompletionHistoryEntry( - date = date, - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - } - - @Test - fun `toggle third (Completed) entry back and forth, assert streaks change correctly`() = runTest { - val date = routineStartDate.plusDays(2) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - val newStreaks = listOf( - Streak( - id = 1, - startDate = routineStartDate, - endDate = routineStartDate.plusDays(1), - ), - Streak( - id = 2, - startDate = routineStartDate.plusDays(3), - endDate = routineStartDate.plusDays(17), - ) - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(newStreaks) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - } - - @Test - fun `toggle forth (OverCompleted) entry back and forth, assert statuses change correctly`() = runTest { - val date = routineStartDate.plusDays(3) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntryByDate(routineId, date) - ).isEqualTo( - CompletionHistoryEntry( - date = date, - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntryByDate(routineId, date) - ).isEqualTo( - CompletionHistoryEntry( - date = date, - status = HistoricalStatus.OverCompleted, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - } - - @Test - fun `toggle forth (OverCompleted) entry back and forth, assert streaks change correctly`() = runTest { - val date = routineStartDate.plusDays(3) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - } - - @Test - fun `toggle fifth (AlreadyCompleted) entry back and forth, assert statuses change correctly`() = runTest { - val date = routineStartDate.plusDays(4) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntryByDate(routineId, date) - ).isEqualTo( - CompletionHistoryEntry( - date = date, - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntryByDate(routineId, date) - ).isEqualTo( - CompletionHistoryEntry( - date = date, - status = HistoricalStatus.AlreadyCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - } - - @Test - fun `toggle fifth (OverCompleted) entry back and forth, assert streaks change correctly`() = runTest { - val date = routineStartDate.plusDays(4) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - } - - @Test - fun `toggle eighth (CompletedLater) entry back and forth, assert statuses change correctly`() = runTest { - val date = routineStartDate.plusDays(7) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - val newHistory = mutableListOf() - newHistory.addAll(initialHistory) - newHistory[7] = CompletionHistoryEntry( - date = date, - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - newHistory[9] = CompletionHistoryEntry( - date = routineStartDate.plusDays(9), - status = HistoricalStatus.OverCompletedOnVacation, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = routineStartDate..routineEndDate, - ) - ).isEqualTo(newHistory) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = routineStartDate..routineEndDate, - ) - ).isEqualTo(initialHistory) - } - - @Test - fun `toggle eighth (CompletedLater) entry back and forth, assert streaks change correctly`() = runTest { - val date = routineStartDate.plusDays(7) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - } - - @Test - fun `toggle ninth (NotCompletedOnVacation) entry back and forth, assert statuses change correctly`() = runTest { - val date = routineStartDate.plusDays(8) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntryByDate(routineId, date) - ).isEqualTo( - CompletionHistoryEntry( - date = date, - status = HistoricalStatus.OverCompletedOnVacation, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - ) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntryByDate(routineId, date) - ).isEqualTo( - CompletionHistoryEntry( - date = date, - status = HistoricalStatus.NotCompletedOnVacation, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - ) - } - - @Test - fun `toggle ninth (NotCompletedOnVacation) entry back and forth, assert streaks change correctly`() = runTest { - val date = routineStartDate.plusDays(8) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - } - - @Test - fun `toggle tenth (SortedOutBacklogOnVacation) entry back and forth, assert statuses change correctly`() = runTest { - val date = routineStartDate.plusDays(9) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - val newHistory = mutableListOf() - newHistory.addAll(initialHistory) - newHistory[9] = CompletionHistoryEntry( - date = date, - status = HistoricalStatus.NotCompletedOnVacation, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - newHistory[7] = CompletionHistoryEntry( - date = routineStartDate.plusDays(7), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = routineStartDate..routineEndDate, - ) - ).isEqualTo(newHistory) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = routineStartDate..routineEndDate, - ) - ).isEqualTo(initialHistory) - } - - @Test - fun `toggle tenth (SortedOutBacklogOnVacation) entry back and forth, assert streaks change correctly`() = runTest { - val date = routineStartDate.plusDays(9) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - val newStreaks = listOf( - Streak( - id = 1, - startDate = routineStartDate, - endDate = routineStartDate.plusDays(6), - ), - Streak( - id = 2, - startDate = routineStartDate.plusDays(10), - endDate = routineStartDate.plusDays(17), - ) - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(newStreaks) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - } - - @Test - fun `toggle fifteenth (CompletedLater) entry back and forth, assert statuses change correctly`() = runTest { - val date = routineStartDate.plusDays(14) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - val newHistory = mutableListOf() - newHistory.addAll(initialHistory) - newHistory[14] = CompletionHistoryEntry( - date = date, - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - newHistory[16] = CompletionHistoryEntry( - date = routineStartDate.plusDays(16), - status = HistoricalStatus.OverCompleted, - scheduleDeviation = 1F, - timesCompleted = 1F, - ) - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = routineStartDate..routineEndDate, - ) - ).isEqualTo(newHistory) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = routineStartDate..routineEndDate, - ) - ).isEqualTo(initialHistory) - } - - @Test - fun `toggle fifteenth (CompletedLater) entry back and forth, assert streaks change correctly`() = runTest { - val date = routineStartDate.plusDays(7) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - } - - @Test - fun `toggle seventeenth (SortedOutBacklog) entry back and forth, assert statuses change correctly`() = runTest { - val date = routineStartDate.plusDays(16) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - val newHistory = mutableListOf() - newHistory.addAll(initialHistory) - newHistory[16] = CompletionHistoryEntry( - date = date, - status = HistoricalStatus.Skipped, - scheduleDeviation = 0F, - timesCompleted = 0F, - ) - newHistory[14] = CompletionHistoryEntry( - date = routineStartDate.plusDays(14), - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = routineStartDate..routineEndDate, - ) - ).isEqualTo(newHistory) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntries( - routineId = routineId, - dates = routineStartDate..routineEndDate, - ) - ).isEqualTo(initialHistory) - } - - @Test - fun `toggle seventeenth (SortedOutBacklog) entry back and forth, assert streaks change correctly`() = runTest { - val date = routineStartDate.plusDays(16) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - val newStreaks = listOf( - Streak( - id = 1, - startDate = routineStartDate, - endDate = routineStartDate.plusDays(13), - ), - Streak( - id = 2, - startDate = routineStartDate.plusDays(15), - endDate = routineStartDate.plusDays(17), - ) - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(newStreaks) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - } - - @Test - fun `toggle twenty first (NotCompleted) entry back and forth, assert statuses change correctly`() = runTest { - val date = routineStartDate.plusDays(20) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat( - completionHistoryRepository.getHistoryEntryByDate(routineId, date) - ).isEqualTo( - CompletionHistoryEntry( - date = date, - status = HistoricalStatus.Completed, - scheduleDeviation = 0F, - timesCompleted = 1F, - ) - ) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - - assertThat( - completionHistoryRepository.getHistoryEntryByDate(routineId, date) - ).isEqualTo( - CompletionHistoryEntry( - date = date, - status = HistoricalStatus.NotCompleted, - scheduleDeviation = -1F, - timesCompleted = 0F, - ) - ) - } - - @Test - fun `toggle twenty first (NotCompleted) entry back and forth, assert streaks change correctly`() = runTest { - val date = routineStartDate.plusDays(20) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - val newStreaks = listOf( - Streak( - id = 1, - startDate = routineStartDate, - endDate = routineStartDate.plusDays(17), - ), - Streak( - id = 2, - startDate = routineStartDate.plusDays(20), - endDate = null, - ) - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(newStreaks) - - toggleHistoricalStatus( - routineId = routineId, - currentDate = date, - today = routineEndDate.plusDays(1), - ) - assertThat(streakRepository.getAllStreaks(routineId)) - .containsExactlyElementsIn(initialStreaks) - } - -// @Test -// fun `toggle first (completed) entry in present, assert historical entry gets deleted`() = runTest { -// val date = routineStartDate -// -// toggleHistoricalStatus( -// routineId = routineId, -// currentDate = date, -// today = date, -// ) -// assertThat(completionHistoryRepository.getHistoryEntryByDate(routineId, date)).isNull() -// } -// -// @Test -// fun `toggle first (completed) in present entry, assert streaks change correctly`() = runTest { -// val date = routineStartDate -// -// toggleHistoricalStatus( -// routineId = routineId, -// currentDate = date, -// today = date, -// ) -// assertThat(streakRepository.getAllStreaks(routineId)).isEmpty() -// } -} \ No newline at end of file diff --git a/core/model/src/main/java/com/rendox/routinetracker/core/model/Routine.kt b/core/model/src/main/java/com/rendox/routinetracker/core/model/Habit.kt similarity index 51% rename from core/model/src/main/java/com/rendox/routinetracker/core/model/Routine.kt rename to core/model/src/main/java/com/rendox/routinetracker/core/model/Habit.kt index 9a72b23b..26fedb09 100644 --- a/core/model/src/main/java/com/rendox/routinetracker/core/model/Routine.kt +++ b/core/model/src/main/java/com/rendox/routinetracker/core/model/Habit.kt @@ -1,9 +1,9 @@ package com.rendox.routinetracker.core.model -import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalTime -sealed class Routine { +sealed class Habit { abstract val id: Long? abstract val name: String abstract val description: String? @@ -12,7 +12,12 @@ sealed class Routine { abstract val schedule: Schedule abstract val defaultCompletionTime: LocalTime? - data class YesNoRoutine( + abstract class CompletionRecord { + abstract val date: LocalDate + abstract val numOfTimesCompleted: Float + } + + data class YesNoHabit( override val id: Long? = null, override val name: String, override val description: String? = null, @@ -20,11 +25,18 @@ sealed class Routine { override val progress: Float? = null, override val schedule: Schedule, override val defaultCompletionTime: LocalTime? = null, - ) : Routine() - - data class NumericalValueRoutineUnit( - val numOfUnitsPerSession: Int, - val unitsOfMeasure: String, - val sessionUnit: DateTimeUnit, - ) + ) : Habit() { + data class CompletionRecord( + override val date: LocalDate, + override val numOfTimesCompleted: Float = 1f, + ) : Habit.CompletionRecord() { + constructor( + date: LocalDate, + completed: Boolean, + ) : this( + date = date, + numOfTimesCompleted = if (completed) 1f else 0f, + ) + } + } } \ No newline at end of file diff --git a/core/model/src/main/java/com/rendox/routinetracker/core/model/HabitStatus.kt b/core/model/src/main/java/com/rendox/routinetracker/core/model/HabitStatus.kt new file mode 100644 index 00000000..b3b238b3 --- /dev/null +++ b/core/model/src/main/java/com/rendox/routinetracker/core/model/HabitStatus.kt @@ -0,0 +1,64 @@ +package com.rendox.routinetracker.core.model + +enum class HabitStatus { + /** The habit is due on this day because of its frequency */ + Planned, + + /** + * Although the habit is not due on this day, + * there is some backlog that needs to be dealt with. + */ + Backlog, + + /** The habit is not due on this day because of its frequency */ + NotDue, + + /** The user didn't complete the day and didn't need to, because of the habit's frequency. */ + Skipped, + + /** + * Although the habit is due on this date, which is in the past, the user may skip it + * because they had over completed one of the days earlier. + */ + PastDateAlreadyCompleted, + + /** + * Although the habit is due on this date, which is in the future, the user may skip it + * because they had over completed one of the days earlier. + */ + FutureDateAlreadyCompleted, + + /** + * The routine was due but the user failed to complete it on that day. + * However, they caught up on this backlog later. + */ + CompletedLater, + + /** The habit was due on this day and the user completed it. */ + Completed, + + /** The habit was due on this day. The user completed only a part of the planned amount. */ + PartiallyCompleted, + + /** + * The routine wasn't due on the day but the user still completed it. + */ + OverCompleted, + + /** + * The routine wasn't due on the day, but there was a backlog and the user completed it. + */ + SortedOutBacklog, + + /** The habit was due on this day but the user failed to complete it. */ + Failed, + + /** The habit is not due on this day because it's currently on vacation */ + OnVacation, + + /** The habit hasn't been started at the moment of the given date **/ + NotStarted, + + /** The habit has been finished at the moment of the given date. */ + Finished, +} \ No newline at end of file diff --git a/core/model/src/main/java/com/rendox/routinetracker/core/model/RoutineStatus.kt b/core/model/src/main/java/com/rendox/routinetracker/core/model/RoutineStatus.kt deleted file mode 100644 index 87041213..00000000 --- a/core/model/src/main/java/com/rendox/routinetracker/core/model/RoutineStatus.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.rendox.routinetracker.core.model - -sealed interface RoutineStatus - -enum class PlanningStatus : RoutineStatus { - - /** The routine is due on this day because of its frequency */ - Planned, - - /** - * Although the routine is not due on this day, - * there is some backlog that needs to be dealt with. - */ - Backlog, - - /** - * Although the routine is due on this day, the user may skip it - * because they had over completed one of the days earlier. - */ - AlreadyCompleted, - - /** The routine is not due on this day because of its frequency */ - NotDue, - - /** The routine is not due on this day because it's currently on vacation */ - OnVacation; -} - -enum class HistoricalStatus : RoutineStatus { - - /** - * The routine was due on the day but the user failed to complete it. - */ - NotCompleted, - - /** - * The routine was due on the day and the user completed it. - */ - Completed, - - /** - * The routine wasn't due on the day but the user still completed it. - */ - OverCompleted, - - /** - * The routine wasn't due on the day because it was currently - * on vacation but the user still completed the day. - */ - OverCompletedOnVacation, - - /** - * The routine wasn't due on the day because it was currently - * on vacation but the user still sorted out some backlog. - */ - SortedOutBacklogOnVacation, - - /** The user didn't complete the day and didn't need to, thanks to the routine's frequency */ - Skipped, - - /** - * The user didn't complete the day and didn't need to, - * because the routine was currently on vacation. - */ - NotCompletedOnVacation, - - /** - * The routine was due but the user failed to complete it on that day. - * However, they caught up on this backlog later. - */ - CompletedLater, - - /** - * The routine wasn't due on the day, but there was a backlog and the user completed it. - */ - SortedOutBacklog, - - /** - * Although the routine was due on this day, the user skipped it - * because they had over completed it earlier or completed it later. - */ - AlreadyCompleted, -} - -val completedStatuses = listOf( - HistoricalStatus.Completed, - HistoricalStatus.OverCompleted, - HistoricalStatus.OverCompletedOnVacation, - HistoricalStatus.SortedOutBacklog, - HistoricalStatus.SortedOutBacklogOnVacation, -) - -val onVacationHistoricalStatuses = listOf( - HistoricalStatus.NotCompletedOnVacation, - HistoricalStatus.OverCompletedOnVacation, - HistoricalStatus.SortedOutBacklogOnVacation, -) - -val sortedOutBacklogStatuses = listOf( - HistoricalStatus.SortedOutBacklog, - HistoricalStatus.SortedOutBacklogOnVacation, -) - -val overCompletedStatuses = listOf( - HistoricalStatus.OverCompleted, - HistoricalStatus.OverCompletedOnVacation, -) - -val failedStatuses = listOf( - HistoricalStatus.NotCompleted -) \ No newline at end of file diff --git a/core/model/src/main/java/com/rendox/routinetracker/core/model/Schedule.kt b/core/model/src/main/java/com/rendox/routinetracker/core/model/Schedule.kt index 02c2fa2a..f18e201f 100644 --- a/core/model/src/main/java/com/rendox/routinetracker/core/model/Schedule.kt +++ b/core/model/src/main/java/com/rendox/routinetracker/core/model/Schedule.kt @@ -1,7 +1,6 @@ package com.rendox.routinetracker.core.model import com.rendox.routinetracker.core.logic.time.AnnualDate -import com.rendox.routinetracker.core.logic.time.LocalDateRange import com.rendox.routinetracker.core.logic.time.WeekDayMonthRelated import kotlinx.datetime.DatePeriod import kotlinx.datetime.DateTimeUnit @@ -10,11 +9,11 @@ import kotlinx.datetime.LocalDate sealed class Schedule { - abstract val routineStartDate: LocalDate - abstract val routineEndDate: LocalDate? + abstract val startDate: LocalDate + abstract val endDate: LocalDate? abstract val backlogEnabled: Boolean - abstract val cancelDuenessIfDoneAhead: Boolean + abstract val completingAheadEnabled: Boolean abstract val vacationStartDate: LocalDate? abstract val vacationEndDate: LocalDate? @@ -22,30 +21,30 @@ sealed class Schedule { abstract val supportsScheduleDeviation: Boolean abstract val supportsPeriodSeparation: Boolean - sealed class PeriodicSchedule: Schedule() { + sealed class PeriodicSchedule : Schedule() { abstract val periodSeparationEnabled: Boolean abstract val correspondingPeriod: DatePeriod override val supportsScheduleDeviation = true } - sealed class NonPeriodicSchedule: Schedule() { + sealed class NonPeriodicSchedule : Schedule() { override val supportsPeriodSeparation = false } sealed interface ByNumOfDueDays { - fun getNumOfDueDatesInPeriod(period: LocalDateRange): Int + fun getNumOfDueDates(getForFirstPeriod: Boolean): Int } data class EveryDaySchedule( - override val routineStartDate: LocalDate, - override val routineEndDate: LocalDate? = null, + override val startDate: LocalDate, + override val endDate: LocalDate? = null, override val vacationStartDate: LocalDate? = null, override val vacationEndDate: LocalDate? = null, ) : NonPeriodicSchedule() { override val backlogEnabled: Boolean = false - override val cancelDuenessIfDoneAhead: Boolean = false + override val completingAheadEnabled: Boolean = false override val supportsScheduleDeviation = false } @@ -60,11 +59,11 @@ sealed class Schedule { override val startDayOfWeek: DayOfWeek? = null, override val backlogEnabled: Boolean = true, - override val cancelDuenessIfDoneAhead: Boolean = true, + override val completingAheadEnabled: Boolean = true, override val periodSeparationEnabled: Boolean = true, - override val routineStartDate: LocalDate, - override val routineEndDate: LocalDate? = null, + override val startDate: LocalDate, + override val endDate: LocalDate? = null, override val vacationStartDate: LocalDate? = null, override val vacationEndDate: LocalDate? = null, @@ -83,20 +82,21 @@ sealed class Schedule { val numOfDueDaysInFirstPeriod: Int? = null, override val startDayOfWeek: DayOfWeek? = null, - override val backlogEnabled: Boolean = true, - override val cancelDuenessIfDoneAhead: Boolean = true, + override val startDate: LocalDate, + override val endDate: LocalDate? = null, - override val routineStartDate: LocalDate, - override val routineEndDate: LocalDate? = null, + override val periodSeparationEnabled: Boolean = true, override val vacationStartDate: LocalDate? = null, override val vacationEndDate: LocalDate? = null, ) : WeeklySchedule(), ByNumOfDueDays { - override val periodSeparationEnabled: Boolean = false - override val supportsPeriodSeparation = false + override val backlogEnabled: Boolean = true + override val completingAheadEnabled: Boolean = true + override val supportsPeriodSeparation = true + override val supportsScheduleDeviation = false private val firstPeriodIsShort - get() = startDayOfWeek != null && routineStartDate.dayOfWeek != startDayOfWeek + get() = startDayOfWeek != null && startDate.dayOfWeek != startDayOfWeek init { check(numOfDueDays <= DateTimeUnit.WEEK.days) { @@ -105,26 +105,25 @@ sealed class Schedule { if (firstPeriodIsShort) { check(numOfDueDaysInFirstPeriod != null) { - "According to the routine's schedule, at the moment of the " + - "routineStartDate, the first time period has been already started " + + "According to the schedule, at the moment of the " + + "startDate, the first time period has been already started " + "and hence it's shorter than expected. Therefore, the number of due " + "days for this period is ambiguous. So it should be specified " + - "explicitly at the moment of the schedule (routine) creation." + "explicitly at the moment of the schedule creation." } } } - override fun getNumOfDueDatesInPeriod(period: LocalDateRange): Int { - numOfDueDaysInFirstPeriod?.let { - val isFirstPeriod = routineStartDate in period - if (isFirstPeriod) return it + override fun getNumOfDueDates(getForFirstPeriod: Boolean): Int = + if (getForFirstPeriod && numOfDueDaysInFirstPeriod != null) { + numOfDueDaysInFirstPeriod + } else { + numOfDueDays } - return numOfDueDays - } } sealed class MonthlySchedule : PeriodicSchedule() { - abstract val startFromRoutineStart: Boolean + abstract val startFromHabitStart: Boolean override val correspondingPeriod: DatePeriod get() = DatePeriod(months = 1) @@ -134,14 +133,14 @@ sealed class Schedule { val dueDatesIndices: List, val includeLastDayOfMonth: Boolean = false, val weekDaysMonthRelated: List, - override val startFromRoutineStart: Boolean = true, + override val startFromHabitStart: Boolean = true, override val periodSeparationEnabled: Boolean = true, override val backlogEnabled: Boolean = true, - override val cancelDuenessIfDoneAhead: Boolean = true, + override val completingAheadEnabled: Boolean = true, - override val routineStartDate: LocalDate, - override val routineEndDate: LocalDate? = null, + override val startDate: LocalDate, + override val endDate: LocalDate? = null, override val vacationStartDate: LocalDate? = null, override val vacationEndDate: LocalDate? = null, @@ -158,22 +157,23 @@ sealed class Schedule { data class MonthlyScheduleByNumOfDueDays( val numOfDueDays: Int, val numOfDueDaysInFirstPeriod: Int? = null, - override val startFromRoutineStart: Boolean = true, + override val startFromHabitStart: Boolean = true, - override val backlogEnabled: Boolean = true, - override val cancelDuenessIfDoneAhead: Boolean = true, + override val periodSeparationEnabled: Boolean = true, - override val routineStartDate: LocalDate, - override val routineEndDate: LocalDate? = null, + override val startDate: LocalDate, + override val endDate: LocalDate? = null, override val vacationStartDate: LocalDate? = null, override val vacationEndDate: LocalDate? = null, ) : MonthlySchedule(), ByNumOfDueDays { - override val periodSeparationEnabled = false - override val supportsPeriodSeparation = false + override val backlogEnabled: Boolean = true + override val completingAheadEnabled: Boolean = true + override val supportsPeriodSeparation = true + override val supportsScheduleDeviation = false private val firstPeriodIsShort - get() = !startFromRoutineStart && routineStartDate.dayOfMonth != 1 + get() = !startFromHabitStart && startDate.dayOfMonth != 1 init { check(numOfDueDays <= 31) { @@ -182,44 +182,43 @@ sealed class Schedule { if (firstPeriodIsShort) { check(numOfDueDaysInFirstPeriod != null) { - "According to the routine's schedule, at the moment of the " + - "routineStartDate, the first time period has been already started " + + "According to the schedule, at the moment of the " + + "startDate, the first time period has been already started " + "and hence it's shorter than expected. Therefore, the number of due " + "days for this period is ambiguous. So it should be specified " + - "explicitly at the moment of the schedule (routine) creation." + "explicitly at the moment of the schedule creation." } } } - override fun getNumOfDueDatesInPeriod(period: LocalDateRange): Int { - numOfDueDaysInFirstPeriod?.let { - val isFirstPeriod = routineStartDate in period - if (isFirstPeriod) return it + override fun getNumOfDueDates(getForFirstPeriod: Boolean): Int = + if (getForFirstPeriod && numOfDueDaysInFirstPeriod != null) { + numOfDueDaysInFirstPeriod + } else { + numOfDueDays } - return numOfDueDays - } } - data class PeriodicCustomSchedule( + data class AlternateDaysSchedule( val numOfDueDays: Int, val numOfDaysInPeriod: Int, override val backlogEnabled: Boolean = true, - override val cancelDuenessIfDoneAhead: Boolean = true, + override val completingAheadEnabled: Boolean = true, + override val periodSeparationEnabled: Boolean = true, - override val routineStartDate: LocalDate, - override val routineEndDate: LocalDate? = null, + override val startDate: LocalDate, + override val endDate: LocalDate? = null, override val vacationStartDate: LocalDate? = null, override val vacationEndDate: LocalDate? = null, ) : PeriodicSchedule(), ByNumOfDueDays { - override val periodSeparationEnabled = false - override val supportsPeriodSeparation = false + override val supportsPeriodSeparation = true override val correspondingPeriod: DatePeriod get() = DatePeriod(days = numOfDaysInPeriod) - override fun getNumOfDueDatesInPeriod(period: LocalDateRange): Int { + override fun getNumOfDueDates(getForFirstPeriod: Boolean): Int { return numOfDueDays } } @@ -228,10 +227,10 @@ sealed class Schedule { val dueDates: List, override val backlogEnabled: Boolean = true, - override val cancelDuenessIfDoneAhead: Boolean = true, + override val completingAheadEnabled: Boolean = true, - override val routineStartDate: LocalDate, - override val routineEndDate: LocalDate? = null, + override val startDate: LocalDate, + override val endDate: LocalDate? = null, override val vacationStartDate: LocalDate? = null, override val vacationEndDate: LocalDate? = null, @@ -240,7 +239,7 @@ sealed class Schedule { } sealed class AnnualSchedule : PeriodicSchedule() { - abstract val startFromRoutineStart: Boolean + abstract val startFromHabitStart: Boolean override val correspondingPeriod: DatePeriod get() = DatePeriod(years = 1) @@ -248,14 +247,14 @@ sealed class Schedule { data class AnnualScheduleByDueDates( val dueDates: List, - override val startFromRoutineStart: Boolean, + override val startFromHabitStart: Boolean, override val periodSeparationEnabled: Boolean = true, override val backlogEnabled: Boolean = true, - override val cancelDuenessIfDoneAhead: Boolean = true, + override val completingAheadEnabled: Boolean = true, - override val routineStartDate: LocalDate, - override val routineEndDate: LocalDate? = null, + override val startDate: LocalDate, + override val endDate: LocalDate? = null, override val vacationStartDate: LocalDate? = null, override val vacationEndDate: LocalDate? = null, @@ -266,22 +265,23 @@ sealed class Schedule { data class AnnualScheduleByNumOfDueDays( val numOfDueDays: Int, val numOfDueDaysInFirstPeriod: Int?, - override val startFromRoutineStart: Boolean, + override val startFromHabitStart: Boolean, - override val backlogEnabled: Boolean = true, - override val cancelDuenessIfDoneAhead: Boolean = true, + override val periodSeparationEnabled: Boolean = true, - override val routineStartDate: LocalDate, - override val routineEndDate: LocalDate? = null, + override val startDate: LocalDate, + override val endDate: LocalDate? = null, override val vacationStartDate: LocalDate? = null, override val vacationEndDate: LocalDate? = null, ) : AnnualSchedule(), ByNumOfDueDays { - override val periodSeparationEnabled = false - override val supportsPeriodSeparation = false + override val backlogEnabled: Boolean = true + override val completingAheadEnabled: Boolean = true + override val supportsPeriodSeparation = true + override val supportsScheduleDeviation = false private val firstPeriodIsShort - get() = !startFromRoutineStart && routineStartDate.dayOfYear != 1 + get() = !startFromHabitStart && startDate.dayOfYear != 1 init { check(numOfDueDays <= 366) { @@ -290,22 +290,21 @@ sealed class Schedule { if (firstPeriodIsShort) { check(numOfDueDaysInFirstPeriod != null) { - "According to the routine's schedule, at the moment of the " + - "routineStartDate, the first time period has been already started " + + "According to the schedule, at the moment of the " + + "startDate, the first time period has been already started " + "and hence it's shorter than expected. Therefore, the number of due " + "days for this period is ambiguous. So it should be specified " + - "explicitly at the moment of the schedule (routine) creation." + "explicitly at the moment of the schedule creation." } } } - override fun getNumOfDueDatesInPeriod(period: LocalDateRange): Int { - numOfDueDaysInFirstPeriod?.let { - val isFirstPeriod = routineStartDate in period - if (isFirstPeriod) return it + override fun getNumOfDueDates(getForFirstPeriod: Boolean): Int = + if (getForFirstPeriod && numOfDueDaysInFirstPeriod != null) { + numOfDueDaysInFirstPeriod + } else { + numOfDueDays } - return numOfDueDays - } } } diff --git a/core/model/src/main/java/com/rendox/routinetracker/core/model/StatusEntry.kt b/core/model/src/main/java/com/rendox/routinetracker/core/model/StatusEntry.kt deleted file mode 100644 index e15b8a6a..00000000 --- a/core/model/src/main/java/com/rendox/routinetracker/core/model/StatusEntry.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.rendox.routinetracker.core.model - -import kotlinx.datetime.LocalDate - -data class StatusEntry( - val date: LocalDate, - val status: RoutineStatus, -) - -data class CompletionHistoryEntry( - val date: LocalDate, - val status: HistoricalStatus, - val scheduleDeviation: Float, - val timesCompleted: Float, -) - -fun CompletionHistoryEntry.toStatusEntry() = StatusEntry( - date = date, - status = status, -) diff --git a/core/model/src/main/java/com/rendox/routinetracker/core/model/Vacation.kt b/core/model/src/main/java/com/rendox/routinetracker/core/model/Vacation.kt new file mode 100644 index 00000000..eac9a0d3 --- /dev/null +++ b/core/model/src/main/java/com/rendox/routinetracker/core/model/Vacation.kt @@ -0,0 +1,13 @@ +package com.rendox.routinetracker.core.model + +import kotlinx.datetime.LocalDate + +data class Vacation( + val id: Long? = null, + val startDate: LocalDate, + val endDate: LocalDate?, +) { + fun containsDate(date: LocalDate): Boolean { + return date >= startDate && (endDate == null || date <= endDate) + } +} diff --git a/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/habit/CompletionHistoryRepositoryFake.kt b/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/habit/CompletionHistoryRepositoryFake.kt new file mode 100644 index 00000000..ba0b213e --- /dev/null +++ b/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/habit/CompletionHistoryRepositoryFake.kt @@ -0,0 +1,87 @@ +package com.rendox.routinetracker.core.testcommon.fakes.habit + +import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository +import com.rendox.routinetracker.core.model.Habit +import kotlinx.coroutines.flow.update +import kotlinx.datetime.LocalDate + +class CompletionHistoryRepositoryFake( + private val habitData: HabitData +) : CompletionHistoryRepository { + override suspend fun getNumOfTimesCompletedInPeriod( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate? + ): Double { + return habitData.completionHistory.value.filter { + it.first == habitId + && (minDate == null || minDate <= it.second.date) + && (maxDate == null || it.second.date <= maxDate) + }.sumOf { it.second.numOfTimesCompleted.toDouble() } + } + + override suspend fun getRecordByDate(habitId: Long, date: LocalDate): Habit.CompletionRecord? { + return habitData.completionHistory.value + .find { it.first == habitId && it.second.date == date }?.second + } + + override suspend fun getLastCompletedRecord( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate?, + ): Habit.CompletionRecord? { + return habitData.completionHistory.value + .filter { + it.first == habitId + && (minDate == null || minDate <= it.second.date) + && (maxDate == null || it.second.date <= maxDate) + } + .maxByOrNull { it.second.date }?.second + } + + override suspend fun getFirstCompletedRecord( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate?, + ): Habit.CompletionRecord? { + return habitData.completionHistory.value + .filter { + it.first == habitId + && (minDate == null || minDate <= it.second.date) + && (maxDate == null || it.second.date <= maxDate) + } + .minByOrNull { it.second.date }?.second + } + + override suspend fun getRecordsInPeriod( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate? + ): List = habitData.completionHistory.value.filter { + it.first == habitId + && (minDate == null || minDate <= it.second.date) + && (maxDate == null || it.second.date <= maxDate) + }.map { it.second } + + override suspend fun insertCompletion( + habitId: Long, + completionRecord: Habit.CompletionRecord, + ) { + habitData.completionHistory.update { + it.toMutableList().apply { add(habitId to completionRecord) } + } + } + + override suspend fun deleteCompletionByDate(habitId: Long, date: LocalDate) { + habitData.completionHistory.update { completionHistory -> + val completionIndex = completionHistory.indexOfFirst { + it.first == habitId && it.second.date == date + } + if (completionIndex != -1) { + completionHistory.toMutableList().apply { removeAt(completionIndex) } + } else { + completionHistory + } + } + } +} \ No newline at end of file diff --git a/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/habit/HabitData.kt b/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/habit/HabitData.kt new file mode 100644 index 00000000..a499346c --- /dev/null +++ b/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/habit/HabitData.kt @@ -0,0 +1,19 @@ +package com.rendox.routinetracker.core.testcommon.fakes.habit + +import com.rendox.routinetracker.core.model.Habit +import com.rendox.routinetracker.core.model.Vacation +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.datetime.LocalTime + +class HabitData { + var listOfHabits = MutableStateFlow(emptyList()) + val dueDateCompletionTimes = MutableStateFlow(emptyList()) + val completionHistory = MutableStateFlow(emptyList>()) + val vacationHistory = MutableStateFlow(emptyList>()) +} + +data class DueDateCompletionTimeEntity( + val routineId: Long, + val dueDateNumber: Int, + val completionTime: LocalTime, +) \ No newline at end of file diff --git a/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/habit/HabitRepositoryFake.kt b/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/habit/HabitRepositoryFake.kt new file mode 100644 index 00000000..ffa4548a --- /dev/null +++ b/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/habit/HabitRepositoryFake.kt @@ -0,0 +1,49 @@ +package com.rendox.routinetracker.core.testcommon.fakes.habit + +import com.rendox.routinetracker.core.data.habit.HabitRepository +import com.rendox.routinetracker.core.model.Habit +import kotlinx.coroutines.flow.update +import kotlinx.datetime.LocalTime + +class HabitRepositoryFake( + private val habitData: HabitData +) : HabitRepository { + + override suspend fun getHabitById(id: Long): Habit = + habitData.listOfHabits.value[(id - 1).toInt()] + + override suspend fun insertHabit(habit: Habit) { + habitData.listOfHabits.update { + it.toMutableList().apply { add(habit) } + } + } + + override suspend fun getAllHabits(): List = habitData.listOfHabits.value + + override suspend fun updateDueDateSpecificCompletionTime( + time: LocalTime, routineId: Long, dueDateNumber: Int + ) { + habitData.dueDateCompletionTimes.update { dueDateCompletionTimes -> + val existingCompletionTime = dueDateCompletionTimes.find { + it.routineId == routineId && it.dueDateNumber == dueDateNumber + } + val newValue = DueDateCompletionTimeEntity(routineId, dueDateNumber, time) + if (existingCompletionTime == null) { + dueDateCompletionTimes.toMutableList().apply { add(newValue) } + } else { + val existingCompletionTimeIndex = + dueDateCompletionTimes.indexOf(existingCompletionTime) + dueDateCompletionTimes.toMutableList() + .apply { set(existingCompletionTimeIndex, newValue) } + } + } + } + + override suspend fun getDueDateSpecificCompletionTime( + routineId: Long, dueDateNumber: Int + ): LocalTime? { + return habitData.dueDateCompletionTimes.value.find { + it.routineId == routineId && it.dueDateNumber == dueDateNumber + }?.completionTime + } +} \ No newline at end of file diff --git a/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/habit/VacationRepositoryFake.kt b/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/habit/VacationRepositoryFake.kt new file mode 100644 index 00000000..60554753 --- /dev/null +++ b/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/habit/VacationRepositoryFake.kt @@ -0,0 +1,56 @@ +package com.rendox.routinetracker.core.testcommon.fakes.habit + +import com.rendox.routinetracker.core.data.vacation.VacationRepository +import com.rendox.routinetracker.core.model.Vacation +import kotlinx.coroutines.flow.update +import kotlinx.datetime.LocalDate + +class VacationRepositoryFake( + private val habitData: HabitData +) : VacationRepository { + override suspend fun getVacationByDate(habitId: Long, date: LocalDate): Vacation? { + return habitData.vacationHistory.value.firstOrNull { + val vacationEndDate = it.second.endDate + val dateIsWithinVacationPeriod = + (it.second.startDate <= date && vacationEndDate != null && vacationEndDate >= date) + it.first == habitId && + (dateIsWithinVacationPeriod || + ((it.second.startDate <= date && it.second.endDate == null))) + }?.second + } + + override suspend fun getPreviousVacation(habitId: Long, currentDate: LocalDate): Vacation? { + return habitData.vacationHistory.value.find { + val vacationEndDate = it.second.endDate + it.first == habitId && vacationEndDate != null && vacationEndDate < currentDate + }?.second + } + + override suspend fun getLastVacation(habitId: Long): Vacation? { + return habitData.vacationHistory.value.lastOrNull()?.second + } + + override suspend fun getVacationsInPeriod( + habitId: Long, + minDate: LocalDate?, + maxDate: LocalDate?, + ): List = habitData.vacationHistory.value.filter { + val vacationEndDate = it.second.endDate + it.first == habitId && + (minDate == null || minDate <= it.second.startDate || it.second.containsDate(minDate)) && + (maxDate == null || (vacationEndDate == null && it.second.startDate <= maxDate) || (vacationEndDate != null && vacationEndDate <= maxDate) || it.second.containsDate(maxDate)) + + }.map { it.second } + + override suspend fun insertVacation(habitId: Long, vacation: Vacation) { + habitData.vacationHistory.update { + it.toMutableList().apply { add(habitId to vacation) } + } + } + + override suspend fun deleteVacationById(id: Long) { + habitData.vacationHistory.update { + it.toMutableList().apply { removeAt((id - 1).toInt()) } + } + } +} \ No newline at end of file diff --git a/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/routine/CompletionHistoryLocalDataSourceFake.kt b/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/routine/CompletionHistoryLocalDataSourceFake.kt deleted file mode 100644 index a1cc0f44..00000000 --- a/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/routine/CompletionHistoryLocalDataSourceFake.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.rendox.routinetracker.core.testcommon.fakes.routine - -import com.rendox.routinetracker.core.database.completion_history.CompletionHistoryLocalDataSource -import com.rendox.routinetracker.core.logic.time.LocalDateRange -import com.rendox.routinetracker.core.logic.time.rangeTo -import com.rendox.routinetracker.core.model.CompletionHistoryEntry -import com.rendox.routinetracker.core.model.HistoricalStatus -import kotlinx.datetime.LocalDate - -class CompletionHistoryLocalDataSourceFake( - private val routineData: RoutineData -) : CompletionHistoryLocalDataSource { - - override suspend fun getHistoryEntries( - routineId: Long, - dates: LocalDateRange - ): List = - routineData.completionHistory - .filter { it.first == routineId && it.second.date in dates } - .map { it.second } - .sortedBy { it.date } - - override suspend fun getHistoryEntryByDate( - routineId: Long, - date: LocalDate - ): CompletionHistoryEntry? { - return routineData.completionHistory.firstOrNull { - it.first == routineId && it.second.date == date - }?.second - } - - override suspend fun insertHistoryEntry( - id: Long?, - routineId: Long, - entry: CompletionHistoryEntry, - ) { - routineData.completionHistory = - routineData.completionHistory.toMutableList().apply { add(Pair(routineId, entry)) } - if (entry.status == HistoricalStatus.CompletedLater) { - insertCompletedLaterDate(routineId, entry.date) - } - } - - override suspend fun deleteHistoryEntry(routineId: Long, date: LocalDate) { - val elementToRemove = routineData.completionHistory.find { - it.first == routineId && it.second.date == date - } - routineData.completionHistory = - routineData.completionHistory.toMutableList().apply { remove(elementToRemove) } - } - - override suspend fun updateHistoryEntryByDate( - routineId: Long, - date: LocalDate, - newStatus: HistoricalStatus?, - newScheduleDeviation: Float?, - newTimesCompleted: Float?, - ) { - val elementToUpdate = routineData.completionHistory.find { - it.first == routineId && it.second.date == date - } - elementToUpdate?.let { - val elementToUpdateIndex = routineData.completionHistory.indexOf(it) - val newValue = routineId to CompletionHistoryEntry( - date = date, - status = newStatus ?: it.second.status, - scheduleDeviation = newScheduleDeviation ?: it.second.scheduleDeviation, - timesCompleted = newTimesCompleted ?: it.second.timesCompleted, - ) - routineData.completionHistory = - routineData.completionHistory.toMutableList().apply { - set(elementToUpdateIndex, newValue) - } - if (newStatus == HistoricalStatus.CompletedLater) { - insertCompletedLaterDate(routineId, date) - } - } - } - - override suspend fun getFirstHistoryEntry(routineId: Long): CompletionHistoryEntry? = - routineData.completionHistory.firstOrNull()?.second - - override suspend fun getLastHistoryEntry(routineId: Long): CompletionHistoryEntry? = - routineData.completionHistory.lastOrNull()?.second - - override suspend fun getFirstHistoryEntryByStatus( - routineId: Long, - matchingStatuses: List, - minDate: LocalDate?, - maxDate: LocalDate?, - ): CompletionHistoryEntry? { - return routineData.completionHistory.firstOrNull { - it.first == routineId - && it.second.status in matchingStatuses - && (minDate == null || minDate <= it.second.date) - && (maxDate == null || it.second.date <= maxDate) - }?.second - } - - override suspend fun getLastHistoryEntryByStatus( - routineId: Long, - matchingStatuses: List, - minDate: LocalDate?, - maxDate: LocalDate?, - ): CompletionHistoryEntry? { - return routineData.completionHistory.lastOrNull { - it.first == routineId - && it.second.status in matchingStatuses - && (minDate == null || minDate <= it.second.date) - && (maxDate == null || it.second.date <= maxDate) - }?.second - } - - override suspend fun checkIfStatusWasCompletedLater(routineId: Long, date: LocalDate): Boolean = - routineData.completedLaterHistory.contains(Pair(routineId, date)) - - override suspend fun deleteCompletedLaterBackupEntry(routineId: Long, date: LocalDate) { - routineData.completedLaterHistory = - routineData.completedLaterHistory.toMutableList() - .apply { remove(Pair(routineId, date)) } - } - - private fun checkCompletedLater(routineId: Long, date: LocalDate): Boolean = - routineData.completedLaterHistory.contains(Pair(routineId, date)) - - private fun insertCompletedLaterDate(routineId: Long, date: LocalDate) { - val alreadyInserted = checkCompletedLater(routineId, date) - if (!alreadyInserted) { - routineData.completedLaterHistory = - routineData.completedLaterHistory.toMutableList().apply { - add(Pair(routineId, date)) - } - } - } - - override suspend fun getTotalTimesCompletedInPeriod( - routineId: Long, - startDate: LocalDate, - endDate: LocalDate - ): Double = routineData.completionHistory - .filter { it.second.date in startDate..endDate } - .map { it.second.timesCompleted } - .sum().toDouble() - - override suspend fun getScheduleDeviationInPeriod( - routineId: Long, - startDate: LocalDate, - endDate: LocalDate - ): Double { - return routineData.completionHistory - .filter { it.first == routineId && it.second.date in startDate..endDate } - .map { it.second.scheduleDeviation } - .sum().toDouble() - } -} \ No newline at end of file diff --git a/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/routine/RoutineData.kt b/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/routine/RoutineData.kt deleted file mode 100644 index 586d4744..00000000 --- a/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/routine/RoutineData.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.rendox.routinetracker.core.testcommon.fakes.routine - -import com.rendox.routinetracker.core.model.CompletionHistoryEntry -import com.rendox.routinetracker.core.model.Routine -import com.rendox.routinetracker.core.model.Streak -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalTime - -class RoutineData { - private val completionHistoryLock = Any() - var completionHistory = emptyList>() - get() { - synchronized(completionHistoryLock) { - return field - } - } - set(value) { - synchronized(completionHistoryLock) { - field = value - } - } - - private val listOfRoutinesLock = Any() - var listOfRoutines = emptyList() - get() { - synchronized(listOfRoutinesLock) { - return field - } - } - set(value) { - synchronized(listOfRoutinesLock) { - field = value - } - } - - private val completedLaterHistoryLock = Any() - var completedLaterHistory = emptyList>() - get() { - synchronized(completedLaterHistoryLock) { - return field - } - } - set(value) { - synchronized(completedLaterHistoryLock) { - field = value - } - } - - private val dueDateCompletionTimeLock = Any() - var dueDateCompletionTimes = emptyList() - get() { - synchronized(dueDateCompletionTimeLock) { - return field - } - } - set(value) { - synchronized(dueDateCompletionTimeLock) { - field = value - } - } - - private val listOfStreaksLock = Any() - var listOfStreaks = emptyList>() - get() { - synchronized(listOfStreaksLock) { - return field - } - } - set(value) { - synchronized(listOfStreaksLock) { - field = value - } - } -} - -data class DueDateCompletionTimeEntity( - val routineId: Long, - val dueDateNumber: Int, - val completionTime: LocalTime, -) \ No newline at end of file diff --git a/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/routine/RoutineLocalDataSourceFake.kt b/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/routine/RoutineLocalDataSourceFake.kt deleted file mode 100644 index c36ba6d1..00000000 --- a/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/routine/RoutineLocalDataSourceFake.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.rendox.routinetracker.core.testcommon.fakes.routine - -import com.rendox.routinetracker.core.database.routine.RoutineLocalDataSource -import com.rendox.routinetracker.core.model.Routine -import kotlinx.datetime.LocalTime - -class RoutineLocalDataSourceFake( - private val routineData: RoutineData -) : RoutineLocalDataSource { - - override suspend fun getRoutineById(routineId: Long): Routine = - routineData.listOfRoutines[(routineId - 1).toInt()] - - override suspend fun insertRoutine(routine: Routine) { - routineData.listOfRoutines = - routineData.listOfRoutines.toMutableList().apply { add(routine) } - } - - override suspend fun getAllRoutines(): List = - routineData.listOfRoutines - - override suspend fun updateDueDateSpecificCompletionTime( - newTime: LocalTime, routineId: Long, dueDateNumber: Int - ) { - val existingCompletionTime = routineData.dueDateCompletionTimes.find { - it.routineId == routineId && it.dueDateNumber == dueDateNumber - } - val newValue = DueDateCompletionTimeEntity(routineId, dueDateNumber, newTime) - if (existingCompletionTime == null) { - routineData.dueDateCompletionTimes = - routineData.dueDateCompletionTimes.toMutableList().apply { add(newValue) } - } else { - val existingCompletionTimeIndex = - routineData.dueDateCompletionTimes.indexOf(existingCompletionTime) - routineData.dueDateCompletionTimes = - routineData.dueDateCompletionTimes.toMutableList().apply { - set(existingCompletionTimeIndex, newValue) - } - } - } - - override suspend fun getDueDateSpecificCompletionTime( - routineId: Long, dueDateNumber: Int - ): LocalTime? { - return routineData.dueDateCompletionTimes.find { - it.routineId == routineId && it.dueDateNumber == dueDateNumber - }?.completionTime - } -} \ No newline at end of file diff --git a/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/routine/StreakLocalDataSourceFake.kt b/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/routine/StreakLocalDataSourceFake.kt deleted file mode 100644 index b5c71e76..00000000 --- a/core/testcommon/src/main/java/com/rendox/routinetracker/core/testcommon/fakes/routine/StreakLocalDataSourceFake.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.rendox.routinetracker.core.testcommon.fakes.routine - -import com.rendox.routinetracker.core.database.streak.StreakLocalDataSource -import com.rendox.routinetracker.core.model.Streak -import kotlinx.datetime.LocalDate - -class StreakLocalDataSourceFake( - private val routineData: RoutineData -) : StreakLocalDataSource { - - override suspend fun getAllStreaks( - routineId: Long, - afterDateInclusive: LocalDate?, - beforeDateInclusive: LocalDate?, - ): List { - return routineData.listOfStreaks - .filter { streakEntity -> - val streak = streakEntity.second - streakEntity.first == routineId - && (afterDateInclusive == null || streak.endDate?.let { it >= afterDateInclusive } ?: true) - && (beforeDateInclusive == null || streak.startDate <= beforeDateInclusive) - } - .map { - it.second.copy( - id = (routineData.listOfStreaks.indexOf(it) + 1).toLong() - ) - } - .sortedBy { it.startDate } - } - - override suspend fun getStreakByDate(routineId: Long, dateWithinStreak: LocalDate): Streak? { - val entry = routineData.listOfStreaks.firstOrNull { - it.first == routineId && it.second.contains(dateWithinStreak) - } - return entry?.let { - it.second.copy(id = (routineData.listOfStreaks.indexOf(it) + 1).toLong()) - } - } - - override suspend fun insertStreak(streak: Streak, routineId: Long) { - routineData.listOfStreaks = routineData.listOfStreaks.toMutableList().apply { - add(routineId to streak) - } - } - - - - override suspend fun getLastStreak(routineId: Long): Streak? { - return routineData.listOfStreaks - .filter { it.first == routineId } - .maxByOrNull { it.second.startDate } - ?.let { - it.second.copy(id = (routineData.listOfStreaks.indexOf(it) + 1).toLong()) - } - } - - override suspend fun deleteStreakById(id: Long) { - routineData.listOfStreaks = - routineData.listOfStreaks.toMutableList().apply { removeAt((id - 1).toInt()) } - } - - override suspend fun updateStreakById(id: Long, start: LocalDate, end: LocalDate?) { - routineData.listOfStreaks = - routineData.listOfStreaks.toMutableList().also { - val oldValue = it[(id - 1).toInt()] - it[(id - 1).toInt()] = - oldValue.copy(second = oldValue.second.copy(startDate = start, endDate = end)) - } - } - - private fun Streak.contains(date: LocalDate): Boolean { - val streakEnd = endDate - return startDate <= date && (streakEnd == null || date <= streakEnd) - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/rendox/routinetracker/core/ui/components/Calendar.kt b/core/ui/src/main/java/com/rendox/routinetracker/core/ui/components/Calendar.kt index 8db57122..22d5fbf2 100644 --- a/core/ui/src/main/java/com/rendox/routinetracker/core/ui/components/Calendar.kt +++ b/core/ui/src/main/java/com/rendox/routinetracker/core/ui/components/Calendar.kt @@ -142,7 +142,6 @@ fun CalendarMonthTitle( val randomOffset = ZoneOffset.MIN // it doesn't matter val date = Date.from(someDateInMonth.atStartOfDay().toInstant(randomOffset)) - println("derived java util date = $date") val fullStandaloneMonthNameFormatter = SimpleDateFormat("LLLL", locale) fullStandaloneMonthNameFormatter.format(date).replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() diff --git a/feature/add_edit_routine/src/androidTest/java/com/rendox/routinetracker/add_routine/ExampleInstrumentedTest.kt b/feature/add_edit_routine/src/androidTest/java/com/rendox/routinetracker/add_routine/ExampleInstrumentedTest.kt deleted file mode 100644 index ab636943..00000000 --- a/feature/add_edit_routine/src/androidTest/java/com/rendox/routinetracker/add_routine/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.rendox.routinetracker.add_routine - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.rendox.routinetracker.add_routine.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/AddRoutineScreen.kt b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/AddRoutineScreen.kt index bcff61be..e646a71d 100644 --- a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/AddRoutineScreen.kt +++ b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/AddRoutineScreen.kt @@ -33,12 +33,14 @@ import com.rendox.routinetracker.add_routine.navigation.AddRoutineDestination import com.rendox.routinetracker.add_routine.navigation.AddRoutineNavHost import com.rendox.routinetracker.add_routine.set_goal.rememberSetGoalPageState import com.rendox.routinetracker.add_routine.tweak_routine.rememberTweakRoutinePageState -import com.rendox.routinetracker.core.data.routine.RoutineRepository +import com.rendox.routinetracker.core.data.habit.HabitRepository +import com.rendox.routinetracker.core.ui.helpers.LocalLocale import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeout import org.koin.compose.koinInject +import java.time.temporal.WeekFields @OptIn(DelicateCoroutinesApi::class) @Composable @@ -46,7 +48,7 @@ internal fun AddRoutineRoute( modifier: Modifier = Modifier, navigateBackAndRecreate: () -> Unit, navigateBack: () -> Unit, - routineRepository: RoutineRepository = koinInject() + habitRepository: HabitRepository = koinInject() ) { val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() @@ -63,8 +65,7 @@ internal fun AddRoutineRoute( saveRoutine = { routine -> GlobalScope.launch { withTimeout(10_000L) { - println("resulting routine = $routine") - routineRepository.insertRoutine(routine) + habitRepository.insertHabit(routine) } } }, @@ -97,6 +98,8 @@ internal fun AddRoutineScreen( val navigateForwardButtonText = addRoutineScreenState.navigateForwardButtonText.asString().uppercase() + val startDayOfWeek = WeekFields.of(LocalLocale.current).firstDayOfWeek + AddRoutineBottomNavigation( navigateBackButtonText = navigateBackButtonText, navigateForwardButtonText = navigateForwardButtonText, @@ -104,7 +107,9 @@ internal fun AddRoutineScreen( currentScreenNumber = addRoutineScreenState.currentScreenNumber, numOfScreens = addRoutineScreenState.navDestinations.size, navigateBackButtonOnClick = addRoutineScreenState::navigateBackOrCancel, - navigateForwardButtonOnClick = addRoutineScreenState::navigateForwardOrSave, + navigateForwardButtonOnClick = { + addRoutineScreenState.navigateForwardOrSave(startDayOfWeek = startDayOfWeek) + }, ) } } @@ -181,7 +186,7 @@ private fun NavigationProgressIndicator( } @Composable -fun AddRoutineDestinationTopAppBar( +fun AddHabitDestinationTopAppBar( modifier: Modifier = Modifier, destination: AddRoutineDestination, ) { diff --git a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/AddRoutineScreenState.kt b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/AddRoutineScreenState.kt index 81003551..194037cc 100644 --- a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/AddRoutineScreenState.kt +++ b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/AddRoutineScreenState.kt @@ -14,12 +14,13 @@ import com.rendox.routinetracker.add_routine.choose_schedule.ChooseSchedulePageS import com.rendox.routinetracker.add_routine.choose_schedule.assembleSchedule import com.rendox.routinetracker.add_routine.navigation.AddRoutineDestination import com.rendox.routinetracker.add_routine.navigation.navigate -import com.rendox.routinetracker.add_routine.navigation.yesNoRoutineDestinations +import com.rendox.routinetracker.add_routine.navigation.yesNoHabitDestinations import com.rendox.routinetracker.add_routine.set_goal.SetGoalPageState import com.rendox.routinetracker.add_routine.tweak_routine.TweakRoutinePageState -import com.rendox.routinetracker.core.model.Routine +import com.rendox.routinetracker.core.model.Habit import com.rendox.routinetracker.core.ui.R import com.rendox.routinetracker.core.ui.helpers.UiText +import kotlinx.datetime.DayOfWeek import kotlinx.datetime.toKotlinLocalTime @Stable @@ -30,7 +31,7 @@ class AddRoutineScreenState( setGoalPageState: SetGoalPageState, chooseSchedulePageState: ChooseSchedulePageState, tweakRoutinePageState: TweakRoutinePageState, - private val saveRoutine: (Routine) -> Unit, + private val saveRoutine: (Habit) -> Unit, private val navigateBackAndRecreate: () -> Unit, private val navigateBack: () -> Unit, ) { @@ -46,7 +47,7 @@ class AddRoutineScreenState( var tweakRoutinePageState by mutableStateOf(tweakRoutinePageState) private set - var navDestinations by mutableStateOf(yesNoRoutineDestinations) + var navDestinations by mutableStateOf(yesNoHabitDestinations) private set var navigateBackButtonText: UiText by mutableStateOf(UiText.DynamicString("")) @@ -94,11 +95,11 @@ class AddRoutineScreenState( } } - fun navigateForwardOrSave() { + fun navigateForwardOrSave(startDayOfWeek: DayOfWeek) { val currentDestinationRoute = navBackStackEntry?.destination?.route ?: return if (currentDestinationRoute == navDestinations.last().route) { - val routine = assembleRoutine() + val routine = assembleRoutine(startDayOfWeek) saveRoutine(routine) navigateBackAndRecreate() return @@ -124,7 +125,9 @@ class AddRoutineScreenState( if (nextDestination == AddRoutineDestination.TweakRoutine) { tweakRoutinePageState.updateChosenSchedule( - chooseSchedulePageState.selectedSchedulePickerState.assembleSchedule() + chooseSchedulePageState.selectedSchedulePickerState.assembleSchedule( + startDayOfWeek = startDayOfWeek, + ) ) } @@ -133,23 +136,24 @@ class AddRoutineScreenState( private fun updateNavDestinations(routineType: RoutineTypeUi) { navDestinations = when (routineType) { - RoutineTypeUi.YesNoRoutine -> yesNoRoutineDestinations - RoutineTypeUi.MeasurableRoutine -> TODO() + RoutineTypeUi.YesNoHabit -> yesNoHabitDestinations + RoutineTypeUi.MeasurableHabit -> TODO() } } - private fun assembleRoutine(): Routine = when (chooseRoutineTypePageState.routineType) { - is RoutineTypeUi.YesNoRoutine -> Routine.YesNoRoutine( + private fun assembleRoutine(startDayOfWeek: DayOfWeek): Habit = when (chooseRoutineTypePageState.routineType) { + is RoutineTypeUi.YesNoHabit -> Habit.YesNoHabit( name = setGoalPageState.routineName, description = setGoalPageState.routineDescription, schedule = chooseSchedulePageState.selectedSchedulePickerState.assembleSchedule( - tweakRoutinePageState = tweakRoutinePageState + tweakRoutinePageState = tweakRoutinePageState, + startDayOfWeek = startDayOfWeek, ), sessionDurationMinutes = tweakRoutinePageState.sessionDuration?.toMinutes()?.toInt(), defaultCompletionTime = tweakRoutinePageState.sessionTime?.toKotlinLocalTime(), ) - is RoutineTypeUi.MeasurableRoutine -> TODO() + is RoutineTypeUi.MeasurableHabit -> TODO() } } @@ -161,7 +165,7 @@ fun rememberAddRoutineScreenState( setGoalPageState: SetGoalPageState, chooseSchedulePageState: ChooseSchedulePageState, tweakRoutinePageState: TweakRoutinePageState, - saveRoutine: (Routine) -> Unit, + saveRoutine: (Habit) -> Unit, navigateBackAndRecreate: () -> Unit, navigateBack: () -> Unit, ) = remember( diff --git a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_routine_type/ChooseRoutineTypePage.kt b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_routine_type/ChooseRoutineTypePage.kt index ee145369..43db8576 100644 --- a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_routine_type/ChooseRoutineTypePage.kt +++ b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_routine_type/ChooseRoutineTypePage.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.rendox.routinetracker.add_routine.AddRoutineDestinationTopAppBar +import com.rendox.routinetracker.add_routine.AddHabitDestinationTopAppBar import com.rendox.routinetracker.add_routine.navigation.AddRoutineDestination import com.rendox.routinetracker.feature.agenda.R @@ -29,11 +29,11 @@ fun ChooseRoutineTypePage( modifier = modifier.verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, ) { - AddRoutineDestinationTopAppBar( + AddHabitDestinationTopAppBar( destination = AddRoutineDestination.ChooseRoutineType ) - for (routineType in routineTypes) { + for (routineType in habitTypes) { ChooseRoutineTypeButton( modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp), routineType = routineType, diff --git a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_routine_type/ChooseRoutineTypePageState.kt b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_routine_type/ChooseRoutineTypePageState.kt index 9bc9967b..bc997928 100644 --- a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_routine_type/ChooseRoutineTypePageState.kt +++ b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_routine_type/ChooseRoutineTypePageState.kt @@ -11,7 +11,7 @@ import androidx.compose.runtime.setValue @Stable class ChooseRoutineTypePageState( - routineType: RoutineTypeUi = RoutineTypeUi.YesNoRoutine + routineType: RoutineTypeUi = RoutineTypeUi.YesNoHabit ) { var routineType: RoutineTypeUi by mutableStateOf(routineType) private set diff --git a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_routine_type/RoutineTypeUi.kt b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_routine_type/RoutineTypeUi.kt index 67f9e91a..91c349ea 100644 --- a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_routine_type/RoutineTypeUi.kt +++ b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_routine_type/RoutineTypeUi.kt @@ -11,30 +11,30 @@ sealed class RoutineTypeUi( @StringRes val descriptionId: Int, val inDevelopment: Boolean, ) { - object YesNoRoutine : RoutineTypeUi( + object YesNoHabit : RoutineTypeUi( routineTypeId = 1, - titleId = R.string.yes_no_routine_title, - descriptionId = R.string.yes_no_routine_description, + titleId = R.string.yes_no_habit_title, + descriptionId = R.string.yes_no_habit_description, inDevelopment = false, ) - object MeasurableRoutine : RoutineTypeUi( + object MeasurableHabit : RoutineTypeUi( routineTypeId = 2, - titleId = R.string.measurable_routine_title, - descriptionId = R.string.measurable_routine_description, + titleId = R.string.measurable_habit_title, + descriptionId = R.string.measurable_habit_description, inDevelopment = true, ) companion object { fun getTypeById(id: Int) = when (id) { - YesNoRoutine.routineTypeId -> YesNoRoutine - MeasurableRoutine.routineTypeId -> MeasurableRoutine + YesNoHabit.routineTypeId -> YesNoHabit + MeasurableHabit.routineTypeId -> MeasurableHabit else -> throw IllegalArgumentException() } } } -val routineTypes = listOf( - RoutineTypeUi.YesNoRoutine, - RoutineTypeUi.MeasurableRoutine, +val habitTypes = listOf( + RoutineTypeUi.YesNoHabit, + RoutineTypeUi.MeasurableHabit, ) \ No newline at end of file diff --git a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_schedule/AssembleSchedule.kt b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_schedule/AssembleSchedule.kt index fda24588..46c67b0f 100644 --- a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_schedule/AssembleSchedule.kt +++ b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_schedule/AssembleSchedule.kt @@ -1,5 +1,6 @@ package com.rendox.routinetracker.add_routine.choose_schedule +import com.kizitonwose.calendar.core.daysOfWeek import com.rendox.routinetracker.add_routine.choose_schedule.schedule_picker_states.AlternateDaysSchedulePickerState import com.rendox.routinetracker.add_routine.choose_schedule.schedule_picker_states.EveryDaySchedulePickerState import com.rendox.routinetracker.add_routine.choose_schedule.schedule_picker_states.MonthlySchedulePickerState @@ -8,24 +9,30 @@ import com.rendox.routinetracker.add_routine.choose_schedule.schedule_picker_sta import com.rendox.routinetracker.add_routine.tweak_routine.TweakRoutinePageState import com.rendox.routinetracker.core.model.Schedule import kotlinx.datetime.Clock +import kotlinx.datetime.DayOfWeek import kotlinx.datetime.TimeZone import kotlinx.datetime.toKotlinLocalDate import kotlinx.datetime.todayIn fun SchedulePickerState.assembleSchedule( - tweakRoutinePageState: TweakRoutinePageState? = null + tweakRoutinePageState: TweakRoutinePageState? = null, + startDayOfWeek: DayOfWeek, ): Schedule = when (this) { is EveryDaySchedulePickerState -> createEveryDaySchedule(tweakRoutinePageState) is WeeklySchedulePickerState -> convertToScheduleModel(tweakRoutinePageState) is MonthlySchedulePickerState -> convertToScheduleModel(tweakRoutinePageState) - is AlternateDaysSchedulePickerState -> convertToScheduleModel(tweakRoutinePageState) + is AlternateDaysSchedulePickerState -> convertToScheduleModel( + tweakRoutinePageState = tweakRoutinePageState, + startDayOfWeek = startDayOfWeek, + ) } private fun createEveryDaySchedule( tweakRoutinePageState: TweakRoutinePageState? ): Schedule.EveryDaySchedule = Schedule.EveryDaySchedule( - routineStartDate = tweakRoutinePageState?.startDate?.toKotlinLocalDate() + startDate = tweakRoutinePageState?.startDate?.toKotlinLocalDate() ?: Clock.System.todayIn(TimeZone.currentSystemDefault()), + endDate = tweakRoutinePageState?.endDate?.toKotlinLocalDate(), ) private fun WeeklySchedulePickerState.convertToScheduleModel( @@ -39,16 +46,16 @@ private fun WeeklySchedulePickerState.convertToScheduleModel( if (tweakRoutinePageState != null) { Schedule.WeeklyScheduleByDueDaysOfWeek( dueDaysOfWeek = specificDaysOfWeek, - routineStartDate = tweakRoutinePageState.startDate.toKotlinLocalDate(), - routineEndDate = tweakRoutinePageState.endDate?.toKotlinLocalDate(), + startDate = tweakRoutinePageState.startDate.toKotlinLocalDate(), + endDate = tweakRoutinePageState.endDate?.toKotlinLocalDate(), backlogEnabled = tweakRoutinePageState.backlogEnabled!!, - cancelDuenessIfDoneAhead = tweakRoutinePageState.completingAheadEnabled!!, + completingAheadEnabled = tweakRoutinePageState.completingAheadEnabled!!, periodSeparationEnabled = tweakRoutinePageState.periodSeparationEnabled!!, ) } else { Schedule.WeeklyScheduleByDueDaysOfWeek( dueDaysOfWeek = specificDaysOfWeek, - routineStartDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), + startDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), ) } } else { @@ -56,15 +63,14 @@ private fun WeeklySchedulePickerState.convertToScheduleModel( if (tweakRoutinePageState != null) { Schedule.WeeklyScheduleByNumOfDueDays( numOfDueDays = numOfDueDays.toInt(), - routineStartDate = tweakRoutinePageState.startDate.toKotlinLocalDate(), - routineEndDate = tweakRoutinePageState.endDate?.toKotlinLocalDate(), - backlogEnabled = tweakRoutinePageState.backlogEnabled!!, - cancelDuenessIfDoneAhead = tweakRoutinePageState.completingAheadEnabled!!, + startDate = tweakRoutinePageState.startDate.toKotlinLocalDate(), + endDate = tweakRoutinePageState.endDate?.toKotlinLocalDate(), + periodSeparationEnabled = tweakRoutinePageState.periodSeparationEnabled!!, ) } else { Schedule.WeeklyScheduleByNumOfDueDays( numOfDueDays = numOfDueDays.toInt(), - routineStartDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), + startDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), ) } } @@ -83,65 +89,66 @@ private fun MonthlySchedulePickerState.convertToScheduleModel( dueDatesIndices = specificDaysOfMonth, includeLastDayOfMonth = lastDayOfMonthSelected, weekDaysMonthRelated = emptyList(), // TODO add support for weekDaysMonthRelated, - routineStartDate = tweakRoutinePageState.startDate.toKotlinLocalDate(), - routineEndDate = tweakRoutinePageState.endDate?.toKotlinLocalDate(), + startDate = tweakRoutinePageState.startDate.toKotlinLocalDate(), + endDate = tweakRoutinePageState.endDate?.toKotlinLocalDate(), periodSeparationEnabled = tweakRoutinePageState.periodSeparationEnabled!!, backlogEnabled = tweakRoutinePageState.backlogEnabled!!, - cancelDuenessIfDoneAhead = tweakRoutinePageState.completingAheadEnabled!!, + completingAheadEnabled = tweakRoutinePageState.completingAheadEnabled!!, ) } else { Schedule.MonthlyScheduleByDueDatesIndices( dueDatesIndices = specificDaysOfMonth, includeLastDayOfMonth = lastDayOfMonthSelected, weekDaysMonthRelated = emptyList(), // TODO add support for weekDaysMonthRelated, - routineStartDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), + startDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), ) } } else { if (tweakRoutinePageState != null) { Schedule.MonthlyScheduleByNumOfDueDays( numOfDueDays = numOfDueDays.toInt(), - routineStartDate = tweakRoutinePageState.startDate.toKotlinLocalDate(), - routineEndDate = tweakRoutinePageState.endDate?.toKotlinLocalDate(), - backlogEnabled = tweakRoutinePageState.backlogEnabled!!, - cancelDuenessIfDoneAhead = tweakRoutinePageState.completingAheadEnabled!!, + startDate = tweakRoutinePageState.startDate.toKotlinLocalDate(), + endDate = tweakRoutinePageState.endDate?.toKotlinLocalDate(), + periodSeparationEnabled = tweakRoutinePageState.periodSeparationEnabled!!, ) } else { Schedule.MonthlyScheduleByNumOfDueDays( numOfDueDays = numOfDueDays.toInt(), - routineStartDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), + startDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), ) } } } private fun AlternateDaysSchedulePickerState.convertToScheduleModel( - tweakRoutinePageState: TweakRoutinePageState? + tweakRoutinePageState: TweakRoutinePageState?, + startDayOfWeek: DayOfWeek, ): Schedule { val numOfActivityDays = numOfActivityDays.toInt() val numOfRestDays = numOfRestDays.toInt() if (numOfActivityDays + numOfRestDays == 7) { - return Schedule.WeeklyScheduleByNumOfDueDays( - numOfDueDays = numOfActivityDays, - routineStartDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), + return Schedule.WeeklyScheduleByDueDaysOfWeek( + dueDaysOfWeek = daysOfWeek(startDayOfWeek).take(numOfActivityDays), + startDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), ) } return if (tweakRoutinePageState != null) { - Schedule.PeriodicCustomSchedule( + Schedule.AlternateDaysSchedule( numOfDueDays = numOfActivityDays, numOfDaysInPeriod = numOfActivityDays + numOfRestDays, - routineStartDate = tweakRoutinePageState.startDate.toKotlinLocalDate(), - routineEndDate = tweakRoutinePageState.endDate?.toKotlinLocalDate(), + startDate = tweakRoutinePageState.startDate.toKotlinLocalDate(), + endDate = tweakRoutinePageState.endDate?.toKotlinLocalDate(), backlogEnabled = tweakRoutinePageState.backlogEnabled!!, - cancelDuenessIfDoneAhead = tweakRoutinePageState.completingAheadEnabled!!, + completingAheadEnabled = tweakRoutinePageState.completingAheadEnabled!!, + periodSeparationEnabled = tweakRoutinePageState.periodSeparationEnabled!!, ) } else { - Schedule.PeriodicCustomSchedule( + Schedule.AlternateDaysSchedule( numOfDueDays = numOfActivityDays, numOfDaysInPeriod = numOfActivityDays + numOfRestDays, - routineStartDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), + startDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), ) } } \ No newline at end of file diff --git a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_schedule/ChooseSchedulePage.kt b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_schedule/ChooseSchedulePage.kt index 5631e3e8..59cbc94f 100644 --- a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_schedule/ChooseSchedulePage.kt +++ b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/choose_schedule/ChooseSchedulePage.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.rendox.routinetracker.add_routine.AddRoutineDestinationTopAppBar +import com.rendox.routinetracker.add_routine.AddHabitDestinationTopAppBar import com.rendox.routinetracker.add_routine.choose_schedule.schedule_pickers.ScheduleTypeUi import com.rendox.routinetracker.add_routine.choose_schedule.schedule_pickers.AlternateDaysSchedulePicker import com.rendox.routinetracker.add_routine.choose_schedule.schedule_pickers.EveryDaySchedulePicker @@ -26,7 +26,7 @@ fun ChooseSchedulePage( .padding(start = 8.dp, end = 16.dp) .verticalScroll(rememberScrollState()) ) { - AddRoutineDestinationTopAppBar(destination = AddRoutineDestination.ChooseSchedule) + AddHabitDestinationTopAppBar(destination = AddRoutineDestination.ChooseSchedule) EveryDaySchedulePicker( everyDaySchedulePickerState = chooseSchedulePageState.everyDaySchedulePickerState, diff --git a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/navigation/AddRoutineNestedNavigation.kt b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/navigation/AddRoutineNestedNavigation.kt index 0db515fa..2d6f31c7 100644 --- a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/navigation/AddRoutineNestedNavigation.kt +++ b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/navigation/AddRoutineNestedNavigation.kt @@ -14,7 +14,9 @@ import com.rendox.routinetracker.add_routine.choose_routine_type.ChooseRoutineTy import com.rendox.routinetracker.add_routine.choose_schedule.ChooseSchedulePage import com.rendox.routinetracker.add_routine.set_goal.SetGoalPage import com.rendox.routinetracker.add_routine.tweak_routine.TweakRoutinePage +import com.rendox.routinetracker.core.ui.helpers.LocalLocale import com.rendox.routinetracker.feature.agenda.R +import java.time.temporal.WeekFields sealed class AddRoutineDestination( val route: String, @@ -41,7 +43,7 @@ sealed class AddRoutineDestination( ) } -val yesNoRoutineDestinations = listOf( +val yesNoHabitDestinations = listOf( AddRoutineDestination.ChooseRoutineType, AddRoutineDestination.SetGoal, AddRoutineDestination.ChooseSchedule, @@ -78,8 +80,10 @@ internal fun AddRoutineNavHost( }, ) { composable(route = AddRoutineDestination.ChooseRoutineType.route) { + val startDayOfWeek = WeekFields.of(LocalLocale.current).firstDayOfWeek + ChooseRoutineTypePage( - navigateForward = addRoutineScreenState::navigateForwardOrSave, + navigateForward = { addRoutineScreenState.navigateForwardOrSave(startDayOfWeek) }, chooseRoutineTypePageState = addRoutineScreenState.chooseRoutineTypePageState, ) } diff --git a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/set_goal/SetGoalPage.kt b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/set_goal/SetGoalPage.kt index 952ff126..38ef4298 100644 --- a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/set_goal/SetGoalPage.kt +++ b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/set_goal/SetGoalPage.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp -import com.rendox.routinetracker.add_routine.AddRoutineDestinationTopAppBar +import com.rendox.routinetracker.add_routine.AddHabitDestinationTopAppBar import com.rendox.routinetracker.add_routine.navigation.AddRoutineDestination import com.rendox.routinetracker.feature.agenda.R @@ -23,7 +23,7 @@ fun SetGoalPage( setGoalPageState: SetGoalPageState, ) { Column(modifier = modifier.verticalScroll(rememberScrollState())) { - AddRoutineDestinationTopAppBar(destination = AddRoutineDestination.SetGoal) + AddHabitDestinationTopAppBar(destination = AddRoutineDestination.SetGoal) OutlinedTextField( modifier = Modifier diff --git a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/tweak_routine/TweakRoutinePage.kt b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/tweak_routine/TweakRoutinePage.kt index 33797023..c17b72b9 100644 --- a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/tweak_routine/TweakRoutinePage.kt +++ b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/tweak_routine/TweakRoutinePage.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.rendox.routinetracker.add_routine.AddRoutineDestinationTopAppBar +import com.rendox.routinetracker.add_routine.AddHabitDestinationTopAppBar import com.rendox.routinetracker.add_routine.navigation.AddRoutineDestination import com.rendox.routinetracker.core.ui.components.CustomIconSetting import com.rendox.routinetracker.core.ui.components.Setting @@ -83,7 +83,7 @@ fun TweakRoutinePage( Column( modifier = modifier.verticalScroll(rememberScrollState()) ) { - AddRoutineDestinationTopAppBar( + AddHabitDestinationTopAppBar( modifier = Modifier.padding(bottom = 8.dp), destination = AddRoutineDestination.TweakRoutine, ) @@ -154,9 +154,9 @@ fun TweakRoutinePage( isOn = completingAheadEnabled, onToggle = tweakRoutinePageState::updateCompletingAheadEnabled, ) + Divider(modifier = Modifier.padding(start = 16.dp)) } tweakRoutinePageState.periodSeparationEnabled?.let { periodSeparationEnabled -> - Divider(modifier = Modifier.padding(start = 16.dp)) Setting( title = stringResource(id = R.string.period_separation_setting_title), description = stringResource(id = R.string.period_separation_setting_description), diff --git a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/tweak_routine/TweakRoutinePageState.kt b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/tweak_routine/TweakRoutinePageState.kt index 12aefe79..a7fac846 100644 --- a/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/tweak_routine/TweakRoutinePageState.kt +++ b/feature/add_edit_routine/src/main/java/com/rendox/routinetracker/add_routine/tweak_routine/TweakRoutinePageState.kt @@ -117,7 +117,7 @@ class TweakRoutinePageState( if (chosenSchedule.supportsScheduleDeviation) chosenSchedule.backlogEnabled else null completingAheadEnabled = - if (chosenSchedule.supportsScheduleDeviation) chosenSchedule.cancelDuenessIfDoneAhead + if (chosenSchedule.supportsScheduleDeviation) chosenSchedule.completingAheadEnabled else null periodSeparationEnabled = if ( diff --git a/feature/add_edit_routine/src/main/res/values/strings.xml b/feature/add_edit_routine/src/main/res/values/strings.xml index 4ce58f6b..22717524 100644 --- a/feature/add_edit_routine/src/main/res/values/strings.xml +++ b/feature/add_edit_routine/src/main/res/values/strings.xml @@ -5,10 +5,10 @@ Which days work best for your schedule? Fine-tune your habit - Yes or No - Click a checkmark when you complete the habit. - Measurable - + Yes or No + Click a checkmark when you complete the habit. + Measurable + Establish a daily goal for a habit. E.g. read 10 pages of the book. diff --git a/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/AgendaList.kt b/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/AgendaList.kt index 594e8e35..c192d192 100644 --- a/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/AgendaList.kt +++ b/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/AgendaList.kt @@ -6,13 +6,11 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.paddingFromBaseline import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -24,7 +22,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -39,20 +36,14 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.recyclerview.widget.RecyclerView -import com.rendox.routinetracker.core.model.HistoricalStatus -import com.rendox.routinetracker.core.model.PlanningStatus -import com.rendox.routinetracker.core.model.RoutineStatus -import com.rendox.routinetracker.core.ui.helpers.LocalLocale +import com.rendox.routinetracker.core.model.HabitStatus import com.rendox.routinetracker.core.ui.theme.RoutineTrackerTheme import com.rendox.routinetracker.core.ui.theme.routineStatusColors -import kotlinx.datetime.toJavaLocalTime -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle class AgendaListAdapter( private val routineList: List, private val onRoutineClick: (Long) -> Unit, - private val onStatusCheckmarkClick: (Long, RoutineStatus) -> Unit, + private val onCheckmarkClick: (DisplayRoutine) -> Unit, ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AgendaListViewHolder { @@ -68,7 +59,7 @@ class AgendaListAdapter( holder.bind( routine = routine, onRoutineClick = { onRoutineClick(routine.id) }, - onStatusCheckmarkClick = { status -> onStatusCheckmarkClick(routine.id, status) }, + onStatusCheckmarkClick = { onCheckmarkClick(routine) }, ) } } @@ -79,7 +70,7 @@ class AgendaListViewHolder( fun bind( routine: DisplayRoutine, onRoutineClick: () -> Unit, - onStatusCheckmarkClick: (RoutineStatus) -> Unit, + onStatusCheckmarkClick: () -> Unit, ) { composeView.setContent { RoutineTrackerTheme { @@ -101,11 +92,11 @@ fun AgendaItem( modifier: Modifier = Modifier, routine: DisplayRoutine, onRoutineClick: () -> Unit, - onStatusCheckmarkClick: (RoutineStatus) -> Unit, + onStatusCheckmarkClick: () -> Unit, ) { - val locale = LocalLocale.current - val timeFormatter = - remember { DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale) } +// val locale = LocalLocale.current +// val timeFormatter = +// remember { DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale) } Row( modifier = modifier.alpha( @@ -167,8 +158,8 @@ fun AgendaItem( @Composable private fun StatusCheckmark( modifier: Modifier = Modifier, - status: RoutineStatus, - onClick: (RoutineStatus) -> Unit, + status: HabitStatus, + onClick: () -> Unit, statusToggleIsDisabled: Boolean, ) { val backgroundColor: Color @@ -176,91 +167,23 @@ private fun StatusCheckmark( var iconColor: Color? when (status) { - HistoricalStatus.NotCompleted -> { + HabitStatus.Failed -> { backgroundColor = MaterialTheme.routineStatusColors.failedBackgroundLight icon = Icons.Filled.Close iconColor = MaterialTheme.routineStatusColors.failedStroke } - PlanningStatus.Planned -> { - backgroundColor = MaterialTheme.colorScheme.surfaceVariant - icon = null - iconColor = null - } - - PlanningStatus.Backlog -> { - backgroundColor = MaterialTheme.routineStatusColors.pendingAgenda - icon = null - iconColor = null - } - - PlanningStatus.AlreadyCompleted -> { - backgroundColor = MaterialTheme.routineStatusColors.pendingAgenda - icon = null - iconColor = null - } - - PlanningStatus.NotDue -> { - backgroundColor = MaterialTheme.routineStatusColors.pendingAgenda - icon = null - iconColor = null - } - - PlanningStatus.OnVacation -> { - backgroundColor = MaterialTheme.routineStatusColors.pendingAgenda - icon = null - iconColor = null - } - - HistoricalStatus.Skipped -> { - backgroundColor = MaterialTheme.routineStatusColors.pendingAgenda - icon = null - iconColor = null - } - - HistoricalStatus.NotCompletedOnVacation -> { - backgroundColor = MaterialTheme.routineStatusColors.pendingAgenda - icon = null - iconColor = null - } - - HistoricalStatus.CompletedLater -> { + HabitStatus.Planned, HabitStatus.OnVacation, HabitStatus.NotDue, + HabitStatus.Backlog, HabitStatus.PastDateAlreadyCompleted, + HabitStatus.FutureDateAlreadyCompleted, HabitStatus.CompletedLater, + HabitStatus.NotStarted, HabitStatus.Finished, HabitStatus.Skipped -> { backgroundColor = MaterialTheme.routineStatusColors.pendingAgenda icon = null iconColor = null } - HistoricalStatus.AlreadyCompleted -> { - backgroundColor = MaterialTheme.routineStatusColors.pendingAgenda - icon = null - iconColor = null - } - - HistoricalStatus.Completed -> { - backgroundColor = MaterialTheme.routineStatusColors.completedBackgroundLight - icon = Icons.Filled.Done - iconColor = MaterialTheme.routineStatusColors.completedStroke - } - - HistoricalStatus.OverCompleted -> { - backgroundColor = MaterialTheme.routineStatusColors.completedBackgroundLight - icon = Icons.Filled.Done - iconColor = MaterialTheme.routineStatusColors.completedStroke - } - - HistoricalStatus.OverCompletedOnVacation -> { - backgroundColor = MaterialTheme.routineStatusColors.completedBackgroundLight - icon = Icons.Filled.Done - iconColor = MaterialTheme.routineStatusColors.completedStroke - } - - HistoricalStatus.SortedOutBacklogOnVacation -> { - backgroundColor = MaterialTheme.routineStatusColors.completedBackgroundLight - icon = Icons.Filled.Done - iconColor = MaterialTheme.routineStatusColors.completedStroke - } - - HistoricalStatus.SortedOutBacklog -> { + HabitStatus.Completed, HabitStatus.OverCompleted, HabitStatus.SortedOutBacklog, + HabitStatus.PartiallyCompleted -> { backgroundColor = MaterialTheme.routineStatusColors.completedBackgroundLight icon = Icons.Filled.Done iconColor = MaterialTheme.routineStatusColors.completedStroke @@ -282,8 +205,11 @@ private fun StatusCheckmark( .clip(CircleShape) .background(backgroundColor) .then( - if (statusToggleIsDisabled) Modifier - else Modifier.clickable { onClick(status) } + if (statusToggleIsDisabled) { + Modifier + } else { + Modifier.clickable(onClick = onClick) + } ) ) { icon?.let { @@ -320,34 +246,42 @@ private fun AgendaItemInListPreview() { private val routines = listOf( DisplayRoutine( name = "Do sports", - status = HistoricalStatus.Completed, + status = HabitStatus.Completed, completionTime = kotlinx.datetime.LocalTime(hour = 9, minute = 0), id = 1, hasGrayedOutLook = false, statusToggleIsDisabled = false, + type = DisplayRoutineType.YesNoHabit, + numOfTimesCompleted = 1f, ), DisplayRoutine( name = "Learn new English words", - status = PlanningStatus.Planned, + status = HabitStatus.Planned, completionTime = null, id = 2, hasGrayedOutLook = false, statusToggleIsDisabled = false, + type = DisplayRoutineType.YesNoHabit, + numOfTimesCompleted = 0f, ), DisplayRoutine( name = "Spend time outside", - status = HistoricalStatus.NotCompleted, + status = HabitStatus.Failed, completionTime = kotlinx.datetime.LocalTime(hour = 12, minute = 30), id = 3, hasGrayedOutLook = false, statusToggleIsDisabled = true, + type = DisplayRoutineType.YesNoHabit, + numOfTimesCompleted = 0f, ), DisplayRoutine( name = "Make my app", - status = HistoricalStatus.CompletedLater, + status = HabitStatus.CompletedLater, completionTime = kotlinx.datetime.LocalTime(hour = 17, minute = 0), id = 4, hasGrayedOutLook = true, statusToggleIsDisabled = false, + type = DisplayRoutineType.YesNoHabit, + numOfTimesCompleted = 0f, ) ) \ No newline at end of file diff --git a/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/AgendaScreen.kt b/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/AgendaScreen.kt index bb0bcc16..559312d9 100644 --- a/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/AgendaScreen.kt +++ b/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/AgendaScreen.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidViewBinding import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.rendox.routinetracker.core.model.RoutineStatus +import com.rendox.routinetracker.core.model.Habit import com.rendox.routinetracker.core.ui.helpers.LocalLocale import com.rendox.routinetracker.feature.agenda.databinding.AgendaRecyclerviewBinding import kotlinx.datetime.toJavaLocalDate @@ -56,8 +56,8 @@ internal fun AgendaRoute( today = LocalDate.now(), onAddRoutineClick = onAddRoutineClick, onRoutineClick = onRoutineClick, - onStatusCheckmarkClick = { routineId, status -> - viewModel.onRoutineStatusCheckmarkClick(routineId, currentDate, status) + insertCompletion = { routineId, completionRecord -> + viewModel.onRoutineComplete(routineId, completionRecord) }, onDateChange = { viewModel.onDateChange(it.toKotlinLocalDate()) }, onNotDueRoutinesVisibilityToggle = { @@ -75,7 +75,7 @@ internal fun AgendaScreen( today: LocalDate, onAddRoutineClick: () -> Unit, onRoutineClick: (Long) -> Unit, - onStatusCheckmarkClick: (Long, RoutineStatus) -> Unit, + insertCompletion: (Long, Habit.CompletionRecord) -> Unit, onDateChange: (LocalDate) -> Unit, onNotDueRoutinesVisibilityToggle: () -> Unit, ) { @@ -138,13 +138,21 @@ internal fun AgendaScreen( dateOnClick = onDateChange, today = today, ) - AgendaList( - routineList = routineList ?: emptyList(), - onRoutineClick = onRoutineClick, - onStatusCheckmarkClick = { routineId, status -> - onStatusCheckmarkClick(routineId, status) - }, - ) + AgendaList( + routineList = routineList ?: emptyList(), + onRoutineClick = onRoutineClick, + onStatusCheckmarkClick = { routine -> + when (routine.type) { + DisplayRoutineType.YesNoHabit -> { + val completion = Habit.YesNoHabit.CompletionRecord( + date = currentDate.toKotlinLocalDate(), + numOfTimesCompleted = if (routine.numOfTimesCompleted > 0F) 0F else 1F, + ) + insertCompletion(routine.id, completion) + } + } + }, + ) } } } @@ -153,13 +161,13 @@ internal fun AgendaScreen( private fun AgendaList( routineList: List, onRoutineClick: (Long) -> Unit, - onStatusCheckmarkClick: (Long, RoutineStatus) -> Unit, + onStatusCheckmarkClick: (DisplayRoutine) -> Unit, ) { AndroidViewBinding(AgendaRecyclerviewBinding::inflate) { val adapter = AgendaListAdapter( routineList = routineList, onRoutineClick = onRoutineClick, - onStatusCheckmarkClick = onStatusCheckmarkClick, + onCheckmarkClick = onStatusCheckmarkClick, ) agendaRecyclerview.adapter = adapter } diff --git a/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/AgendaScreenViewModel.kt b/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/AgendaScreenViewModel.kt index 6e1178f5..97f99dc3 100644 --- a/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/AgendaScreenViewModel.kt +++ b/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/AgendaScreenViewModel.kt @@ -2,15 +2,13 @@ package com.rendox.routinetracker.feature.agenda import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.rendox.routinetracker.core.data.routine.RoutineRepository -import com.rendox.routinetracker.core.domain.completion_history.use_cases.ToggleHistoricalStatusUseCase -import com.rendox.routinetracker.core.domain.completion_history.use_cases.GetRoutineStatusUseCase -import com.rendox.routinetracker.core.domain.completion_history.use_cases.InsertRoutineStatusUseCase -import com.rendox.routinetracker.core.domain.completion_time.GetRoutineCompletionTimeUseCase -import com.rendox.routinetracker.core.model.HistoricalStatus -import com.rendox.routinetracker.core.model.PlanningStatus -import com.rendox.routinetracker.core.model.Routine -import com.rendox.routinetracker.core.model.RoutineStatus +import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository +import com.rendox.routinetracker.core.data.habit.HabitRepository +import com.rendox.routinetracker.core.domain.completion_history.HabitComputeStatusUseCase +import com.rendox.routinetracker.core.domain.completion_history.InsertHabitCompletionUseCase +import com.rendox.routinetracker.core.model.Habit +import com.rendox.routinetracker.core.model.HabitStatus +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -18,6 +16,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate @@ -27,15 +26,13 @@ import kotlinx.datetime.todayIn class AgendaScreenViewModel( today: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), - private val routineRepository: RoutineRepository, - private val getRoutineStatus: GetRoutineStatusUseCase, - private val insertRoutineStatus: InsertRoutineStatusUseCase, - private val toggleHistoricalStatus: ToggleHistoricalStatusUseCase, - private val getRoutineCompletionTime: GetRoutineCompletionTimeUseCase, + private val habitRepository: HabitRepository, + private val insertHabitCompletion: InsertHabitCompletionUseCase, + private val computeHabitStatus: HabitComputeStatusUseCase, + private val completionHistoryRepository: CompletionHistoryRepository, ) : ViewModel() { - private val todayFlow = MutableStateFlow(today) - private val allRoutinesFlow = MutableStateFlow(emptyList()) + private val allRoutinesFlow = MutableStateFlow(emptyList()) private val cashedRoutinesFlow = MutableStateFlow(emptyMap>()) private val _hideNotDueRoutinesFlow = MutableStateFlow(false) @@ -50,7 +47,11 @@ class AgendaScreenViewModel( ) { currentDate, cashedRoutines, hideNotDueRoutines -> cashedRoutines[currentDate]?.let { currentDateRoutines -> currentDateRoutines.filter { - if (hideNotDueRoutines) it.status in dueRoutineStatuses else true + if (hideNotDueRoutines) { + it.status in dueOrCompletedStatuses + } else { + it.status !in nonExistentStatuses + } }.sortedBy { it.id } } }.stateIn( @@ -59,85 +60,77 @@ class AgendaScreenViewModel( started = SharingStarted.WhileSubscribed(5_000), ) - private val dueRoutineStatuses = listOf( - PlanningStatus.Planned, - PlanningStatus.Backlog, - HistoricalStatus.NotCompleted, - HistoricalStatus.Completed, - HistoricalStatus.OverCompleted, - HistoricalStatus.OverCompletedOnVacation, - HistoricalStatus.SortedOutBacklogOnVacation, - HistoricalStatus.SortedOutBacklog, - ) - init { viewModelScope.launch { - allRoutinesFlow.update { routineRepository.getAllRoutines() } + allRoutinesFlow.update { habitRepository.getAllHabits() } updateRoutinesForDate(_currentDateFlow.value) } } private fun updateRoutinesForDate(date: LocalDate) { + val cashedRoutinesForDate = MutableStateFlow(emptyList()) + val updateRoutineJobs = mutableListOf() for (routine in allRoutinesFlow.value) { - viewModelScope.launch { + val job = viewModelScope.launch { val routineForDate = getDisplayRoutine(routine, date) - cashedRoutinesFlow.update { cashedRoutines -> - cashedRoutines.toMutableMap().also { - val existingRoutines = it[date] ?: emptyList() - it[date] = existingRoutines.toMutableList().apply { - if (routineForDate != null) add(routineForDate) - } + cashedRoutinesForDate.update { cashedRoutines -> + cashedRoutines.toMutableList().apply { + add(routineForDate) } } } + updateRoutineJobs.add(job) + } + viewModelScope.launch { + updateRoutineJobs.joinAll() + cashedRoutinesFlow.update { cashedRoutines -> + cashedRoutines.toMutableMap().apply { + this[date] = cashedRoutinesForDate.value + } + } } } - private suspend fun getDisplayRoutine(routine: Routine, date: LocalDate): DisplayRoutine? { - val routineStatus: RoutineStatus? = getRoutineStatus( - routineId = routine.id!!, - date = date, + private suspend fun getDisplayRoutine(habit: Habit, date: LocalDate): DisplayRoutine { + val habitStatus: HabitStatus = computeHabitStatus( + habitId = habit.id!!, + validationDate = date, today = todayFlow.value, ) - return routineStatus?.let { - DisplayRoutine( - name = routine.name, - id = routine.id!!, - status = it, - completionTime = null, - hasGrayedOutLook = it !in dueRoutineStatuses, - statusToggleIsDisabled = date > todayFlow.value, - ) - } + val numOfTimesCompleted = completionHistoryRepository.getRecordByDate( + habitId = habit.id!!, + date = date, + )?.numOfTimesCompleted ?: 0F + + return DisplayRoutine( + name = habit.name, + id = habit.id!!, + type = DisplayRoutineType.YesNoHabit, + status = habitStatus, + numOfTimesCompleted = numOfTimesCompleted, + completionTime = null, + hasGrayedOutLook = habitStatus !in dueOrCompletedStatuses, + statusToggleIsDisabled = date > todayFlow.value, + ) } - fun onRoutineStatusCheckmarkClick( + fun onRoutineComplete( routineId: Long, - currentDate: LocalDate, - routineStatusBeforeClick: RoutineStatus, + completionRecord: Habit.CompletionRecord, ) { viewModelScope.launch { - if (routineStatusBeforeClick is PlanningStatus) { - insertRoutineStatus( - routineId = routineId, - currentDate = currentDate, - completedOnCurrentDate = true, - today = todayFlow.value - ) - } else { - toggleHistoricalStatus( - routineId = routineId, - currentDate = currentDate, - today = todayFlow.value, - ) - } + insertHabitCompletion( + habitId = routineId, + completionRecord = completionRecord, + today = todayFlow.value, + ) cashedRoutinesFlow.update { cashedRoutines -> val newCashedRoutinesValue = cashedRoutines.toMutableMap() for ((date, oldRoutineList) in cashedRoutines) { val routine = allRoutinesFlow.value.find { it.id == routineId }?.let { getDisplayRoutine( - routine = it, + habit = it, date = date, ) } @@ -163,25 +156,36 @@ class AgendaScreenViewModel( _hideNotDueRoutinesFlow.update { !it } } -// fun onAddRoutineClick() { -// viewModelScope.launch { -// routineRepository.insertRoutine(routineList.first()) -// allRoutinesFlow.update { routineRepository.getAllRoutines() } -// val currentDateRoutines = getRoutinesForDate(_currentDateFlow.value) -// cashedRoutinesFlow.update { -// mapOf(_currentDateFlow.value to currentDateRoutines) -// } -// println("current date = ${_currentDateFlow.value}") -// println("cashed routines flow = $cashedRoutinesFlow") -// } -// } + // TODO update todayFlow when the date changes (in case the screen is opened at midnight) + + companion object { + private val dueOrCompletedStatuses = listOf( + HabitStatus.Planned, + HabitStatus.Backlog, + HabitStatus.Failed, + HabitStatus.Completed, + HabitStatus.OverCompleted, + HabitStatus.SortedOutBacklog, + ) + + private val nonExistentStatuses = listOf( + HabitStatus.NotStarted, + HabitStatus.Finished, + ) + } } data class DisplayRoutine( val name: String, val id: Long, - val status: RoutineStatus, + val type: DisplayRoutineType, + val status: HabitStatus, + val numOfTimesCompleted: Float, val completionTime: LocalTime?, val hasGrayedOutLook: Boolean, val statusToggleIsDisabled: Boolean, -) \ No newline at end of file +) + +enum class DisplayRoutineType { + YesNoHabit, +} \ No newline at end of file diff --git a/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/RoutineTestData.kt b/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/RoutineTestData.kt deleted file mode 100644 index 2525c873..00000000 --- a/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/RoutineTestData.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.rendox.routinetracker.feature.agenda - -import com.rendox.routinetracker.core.model.Routine -import com.rendox.routinetracker.core.model.Schedule -import kotlinx.datetime.Clock -import kotlinx.datetime.DayOfWeek -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalTime -import kotlinx.datetime.TimeZone -import kotlinx.datetime.todayIn - -val routineList = listOf( - Routine.YesNoRoutine( - name = "Do sports", - description = "Stay fit and healthy", - sessionDurationMinutes = 75, - defaultCompletionTime = LocalTime(17, 0), - schedule = Schedule.EveryDaySchedule( - routineStartDate = LocalDate(2023, 11, 10), - ) - ), - Routine.YesNoRoutine( - name = "Work on my app", - description = null, - sessionDurationMinutes = 300, - defaultCompletionTime = LocalTime(10, 0), - schedule = Schedule.WeeklyScheduleByDueDaysOfWeek( - routineStartDate = LocalDate(2023, 11, 10), - dueDaysOfWeek = listOf(DayOfWeek.MONDAY, DayOfWeek.TUESDAY), - ) - ) -) \ No newline at end of file diff --git a/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/di/AgendaScreenModule.kt b/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/di/AgendaScreenModule.kt index 106883bc..a0429130 100644 --- a/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/di/AgendaScreenModule.kt +++ b/feature/agenda/src/main/java/com/rendox/routinetracker/feature/agenda/di/AgendaScreenModule.kt @@ -8,11 +8,10 @@ val agendaScreenModule = module { viewModel { AgendaScreenViewModel( - routineRepository = get(), - getRoutineStatus = get(), - getRoutineCompletionTime = get(), - insertRoutineStatus = get(), - toggleHistoricalStatus = get(), + habitRepository = get(), + insertHabitCompletion = get(), + computeHabitStatus = get(), + completionHistoryRepository = get(), ) } } \ No newline at end of file diff --git a/feature/routine_details/build.gradle.kts b/feature/routine_details/build.gradle.kts index bef175d7..6871e71f 100644 --- a/feature/routine_details/build.gradle.kts +++ b/feature/routine_details/build.gradle.kts @@ -32,4 +32,6 @@ android { dependencies { implementation(libs.kizitonwose.calendar.compose) implementation(libs.jetbrains.kotlinx.datetime) + + testImplementation(libs.jetbrains.kotlinx.coroutines.test) } \ No newline at end of file diff --git a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/RoutineDetailsScreen.kt b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/RoutineDetailsScreen.kt index 2bf18559..34cf081c 100644 --- a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/RoutineDetailsScreen.kt +++ b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/RoutineDetailsScreen.kt @@ -69,9 +69,10 @@ internal fun RoutineDetailsScreen( ), title = { Text( - routineName ?: "", + text = routineName ?: "", maxLines = 1, overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.headlineSmall, ) }, navigationIcon = { diff --git a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/RoutineDetailsViewModel.kt b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/RoutineDetailsViewModel.kt index 8c7c83eb..ea267af3 100644 --- a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/RoutineDetailsViewModel.kt +++ b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/RoutineDetailsViewModel.kt @@ -2,8 +2,8 @@ package com.rendox.routinetracker.routine_details import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.rendox.routinetracker.core.data.routine.RoutineRepository -import com.rendox.routinetracker.core.model.Routine +import com.rendox.routinetracker.core.data.habit.HabitRepository +import com.rendox.routinetracker.core.model.Habit import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update @@ -11,15 +11,15 @@ import kotlinx.coroutines.launch class RoutineDetailsViewModel( routineId: Long, - routineRepository: RoutineRepository, + habitRepository: HabitRepository, ) : ViewModel() { - private val _routineFlow: MutableStateFlow = MutableStateFlow(null) - val routineFlow = _routineFlow.asStateFlow() + private val _habitFlow: MutableStateFlow = MutableStateFlow(null) + val routineFlow = _habitFlow.asStateFlow() init { viewModelScope.launch { - _routineFlow.update { routineRepository.getRoutineById(routineId) } + _habitFlow.update { habitRepository.getHabitById(routineId) } } } } \ No newline at end of file diff --git a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/calendar/RoutineCalendar.kt b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/calendar/RoutineCalendar.kt index 5bca8c05..f8f16a96 100644 --- a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/calendar/RoutineCalendar.kt +++ b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/calendar/RoutineCalendar.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -20,9 +19,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.DayPosition -import com.rendox.routinetracker.core.model.HistoricalStatus -import com.rendox.routinetracker.core.model.PlanningStatus -import com.rendox.routinetracker.core.model.RoutineStatus +import com.rendox.routinetracker.core.model.HabitStatus import com.rendox.routinetracker.core.ui.components.CalendarMonthlyPaged import com.rendox.routinetracker.core.ui.theme.routineStatusColors import kotlinx.datetime.LocalDate @@ -35,8 +32,8 @@ fun RoutineCalendar( modifier: Modifier = Modifier, currentMonth: YearMonth, firstDayOfWeek: DayOfWeek, - routineCalendarDates: List, - onDateClick: (date: LocalDate, routineStatusBeforeClick: RoutineStatus) -> Unit, + routineCalendarDates: Map, + onDateClick: (date: LocalDate) -> Unit, onScrolledToNewMonth: (month: YearMonth) -> Unit, ) { CalendarMonthlyPaged( @@ -45,15 +42,13 @@ fun RoutineCalendar( firstDayOfWeek = firstDayOfWeek, onScrolledToNewMonth = onScrolledToNewMonth, dayContent = { calendarDay -> - val calendarDate: RoutineCalendarDate? = remember(routineCalendarDates) { - routineCalendarDates.find { - it.date == calendarDay.date.toKotlinLocalDate() - } + val calendarDate: CalendarDateData? = remember(routineCalendarDates) { + routineCalendarDates[calendarDay.date.toKotlinLocalDate()] } RoutineStatusDay( day = calendarDay, - routineStatus = calendarDate?.status, - includedInAStreak = calendarDate?.includedInStreak ?: false, + habitStatus = calendarDate?.status, + includedInStreak = calendarDate?.includedInStreak ?: false, onClick = onDateClick, ) }, @@ -64,9 +59,9 @@ fun RoutineCalendar( private fun RoutineStatusDay( modifier: Modifier = Modifier, day: CalendarDay, - routineStatus: RoutineStatus?, - includedInAStreak: Boolean, - onClick: (date: LocalDate, routineStatusBeforeClick: RoutineStatus) -> Unit, + habitStatus: HabitStatus?, + includedInStreak: Boolean, + onClick: (date: LocalDate) -> Unit, ) { val completedStroke = MaterialTheme.routineStatusColors.completedStroke val completedBackground = MaterialTheme.routineStatusColors.completedBackground @@ -75,64 +70,53 @@ private fun RoutineStatusDay( val failedBackground = MaterialTheme.routineStatusColors.failedBackground val skippedBackground = - if (includedInAStreak) MaterialTheme.routineStatusColors.completedBackgroundLight + if (includedInStreak) MaterialTheme.routineStatusColors.completedBackgroundLight else MaterialTheme.routineStatusColors.failedBackgroundLight val skippedStroke = - if (includedInAStreak) MaterialTheme.routineStatusColors.completedStroke + if (includedInStreak) MaterialTheme.routineStatusColors.completedStroke else MaterialTheme.routineStatusColors.failedStroke - val onVacationBackground = MaterialTheme.routineStatusColors.vacationBackground - val onVacationStroke = MaterialTheme.routineStatusColors.vacationStroke - val backgroundColor: Color = when (day.position) { DayPosition.InDate, DayPosition.OutDate -> Color.Transparent - else -> when (routineStatus) { + else -> when (habitStatus) { null -> Color.Transparent - - is PlanningStatus -> when (routineStatus) { - PlanningStatus.Planned -> MaterialTheme.routineStatusColors.pending - PlanningStatus.Backlog -> MaterialTheme.routineStatusColors.pending - PlanningStatus.AlreadyCompleted -> Color.Transparent - PlanningStatus.NotDue -> Color.Transparent - PlanningStatus.OnVacation -> Color.Transparent - } - - HistoricalStatus.NotCompleted -> failedBackground - HistoricalStatus.Completed -> completedBackground - HistoricalStatus.OverCompleted -> completedBackground - HistoricalStatus.OverCompletedOnVacation -> completedBackground - HistoricalStatus.SortedOutBacklog -> completedBackground - HistoricalStatus.SortedOutBacklogOnVacation -> completedBackground - HistoricalStatus.Skipped -> skippedBackground - HistoricalStatus.NotCompletedOnVacation -> onVacationBackground - HistoricalStatus.CompletedLater -> skippedBackground - HistoricalStatus.AlreadyCompleted -> skippedBackground + HabitStatus.Planned -> MaterialTheme.routineStatusColors.pending + HabitStatus.Backlog -> MaterialTheme.routineStatusColors.pending + HabitStatus.PastDateAlreadyCompleted -> skippedBackground + HabitStatus.FutureDateAlreadyCompleted -> Color.Transparent + HabitStatus.NotDue -> Color.Transparent + HabitStatus.OnVacation -> MaterialTheme.routineStatusColors.vacationBackground + HabitStatus.Failed -> failedBackground + HabitStatus.Completed -> completedBackground + HabitStatus.PartiallyCompleted -> completedBackground + HabitStatus.OverCompleted -> completedBackground + HabitStatus.SortedOutBacklog -> completedBackground + HabitStatus.Skipped -> skippedBackground + HabitStatus.CompletedLater -> skippedBackground + HabitStatus.NotStarted -> Color.Transparent + HabitStatus.Finished -> Color.Transparent } } val strokeColor: Color = when (day.position) { DayPosition.InDate, DayPosition.OutDate -> Color.Transparent - else -> when (routineStatus) { + else -> when (habitStatus) { null -> Color.Transparent - - is PlanningStatus -> when (routineStatus) { - PlanningStatus.Planned -> MaterialTheme.routineStatusColors.pendingStroke - PlanningStatus.Backlog -> MaterialTheme.routineStatusColors.pendingStroke - PlanningStatus.AlreadyCompleted -> Color.Transparent - PlanningStatus.NotDue -> Color.Transparent - PlanningStatus.OnVacation -> Color.Transparent - } - - HistoricalStatus.NotCompleted -> failedStroke - HistoricalStatus.Completed -> completedStroke - HistoricalStatus.OverCompleted -> completedStroke - HistoricalStatus.OverCompletedOnVacation -> completedStroke - HistoricalStatus.SortedOutBacklog -> completedStroke - HistoricalStatus.SortedOutBacklogOnVacation -> completedStroke - HistoricalStatus.Skipped -> skippedStroke - HistoricalStatus.NotCompletedOnVacation -> onVacationStroke - HistoricalStatus.CompletedLater -> skippedStroke - HistoricalStatus.AlreadyCompleted -> skippedStroke + HabitStatus.Planned -> MaterialTheme.routineStatusColors.pendingStroke + HabitStatus.Backlog -> MaterialTheme.routineStatusColors.pendingStroke + HabitStatus.PastDateAlreadyCompleted -> skippedStroke + HabitStatus.FutureDateAlreadyCompleted -> Color.Transparent + HabitStatus.NotDue -> Color.Transparent + HabitStatus.OnVacation -> MaterialTheme.routineStatusColors.vacationStroke + HabitStatus.Failed -> failedStroke + HabitStatus.Completed -> completedStroke + HabitStatus.PartiallyCompleted -> completedStroke + HabitStatus.OverCompleted -> completedStroke + HabitStatus.SortedOutBacklog -> completedStroke + HabitStatus.Skipped -> skippedStroke + HabitStatus.CompletedLater -> skippedStroke + HabitStatus.NotStarted -> Color.Transparent + HabitStatus.Finished -> Color.Transparent } } @@ -144,8 +128,8 @@ private fun RoutineStatusDay( .background(color = backgroundColor, shape = CircleShape) .border(border = BorderStroke(width = 2.dp, color = strokeColor), shape = CircleShape) .then( - if (routineStatus == null) Modifier - else Modifier.clickable { onClick(day.date.toKotlinLocalDate(), routineStatus) } + if (habitStatus == null) Modifier + else Modifier.clickable { onClick(day.date.toKotlinLocalDate()) } ) ) { Text( @@ -158,82 +142,4 @@ private fun RoutineStatusDay( }, ) } -} - -//@Preview( -// showSystemUi = true, showBackground = false, backgroundColor = 0xFF1A1B1E, -// wallpaper = Wallpapers.NONE, -//) -//@Composable -//private fun RoutineCalendarPreview() { -// CompositionLocalProvider(LocalLocale provides Locale.ITALIAN) { -// Box(modifier = Modifier.fillMaxSize()) { -// Card( -// modifier = Modifier -// .padding(16.dp) -// .wrapContentSize(), -// ) { -// RoutineCalendar( -// currentMonth = YearMonth.of(2023, java.time.Month.NOVEMBER), -// firstDayOfWeek = DayOfWeek.MONDAY, -// routineStatuses = statusList.mapIndexed { dayNumber, status -> -// StatusEntry( -// date = routineStartDate.plus(DatePeriod(days = dayNumber)), -// status = status, -// ) -// }, -// streakDates = streakDates, -// today = LocalDate(2023, Month.NOVEMBER, 23), -// ) -// } -// } -// } -//} -// -//val routineStartDate = LocalDate(2023, Month.NOVEMBER, 1) -// -//val statusList: List = listOf( -// HistoricalStatus.Completed, // 2023-11-1 -// HistoricalStatus.Completed, // 2023-11-2 -// HistoricalStatus.Skipped, // 2023-11-3 -// HistoricalStatus.Completed, // 2023-11-4 -// HistoricalStatus.Completed, // 2023-11-5 -// HistoricalStatus.Skipped, // 2023-11-6 -// HistoricalStatus.Skipped, // 2023-11-7 -// -// HistoricalStatus.NotCompleted, // 2023-11-8 -// HistoricalStatus.Skipped, // 2023-11-9 -// HistoricalStatus.Skipped, // 2023-11-10 -// HistoricalStatus.OverCompleted, // 2023-11-11 -// HistoricalStatus.Completed, // 2023-11-12 -// HistoricalStatus.NotCompletedOnVacation, // 2023-11-13 -// HistoricalStatus.NotCompletedOnVacation, // 2023-11-14 -// -// HistoricalStatus.OverCompletedOnVacation, // 2023-11-15 -// HistoricalStatus.Completed, // 2023-11-16 -// HistoricalStatus.CompletedLater, // 2023-11-17 -// HistoricalStatus.SortedOutBacklog, // 2023-11-18 -// HistoricalStatus.OverCompleted, // 2023-11-19 -// HistoricalStatus.AlreadyCompleted, // 2023-11-20 -// HistoricalStatus.Skipped, // 2023-11-21 -// -// HistoricalStatus.Completed, // 2023-11-22 -// PlanningStatus.Backlog, // 2023-11-23 -// PlanningStatus.Backlog, // 2023-11-24 -// PlanningStatus.Planned, // 2023-11-25 -// PlanningStatus.Planned, // 2023-11-26 -// PlanningStatus.Planned, // 2023-11-27 -// PlanningStatus.NotDue, // 2023-11-28 -// -// PlanningStatus.AlreadyCompleted, // 2023-11-29 -// PlanningStatus.OnVacation, // 2023-11-30 -//) -// -//val streakDates = mutableListOf().apply { -// for (dayOfMonth in 1..7) { -// add(LocalDate(2023, 11, dayOfMonth)) -// } -// for (dayOfMonth in 11..30) { -// add(LocalDate(2023, 11, dayOfMonth)) -// } -//} \ No newline at end of file +} \ No newline at end of file diff --git a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/calendar/RoutineCalendarScreen.kt b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/calendar/RoutineCalendarScreen.kt index cb78c55f..3bac7ad4 100644 --- a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/calendar/RoutineCalendarScreen.kt +++ b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/calendar/RoutineCalendarScreen.kt @@ -27,7 +27,7 @@ import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.rendox.routinetracker.core.model.RoutineStatus +import com.rendox.routinetracker.core.model.Habit import com.rendox.routinetracker.core.ui.helpers.LocalLocale import com.rendox.routinetracker.feature.routine_details.R import kotlinx.datetime.LocalDate @@ -39,37 +39,39 @@ internal fun RoutineCalendarRoute( modifier: Modifier = Modifier, viewModel: RoutineCalendarViewModel, ) { - val routineCalendarDates by viewModel.visibleDatesFlow.collectAsStateWithLifecycle() + val habit by viewModel.habitFlow.collectAsStateWithLifecycle() + val routineCalendarDates by viewModel.calendarDatesFlow.collectAsStateWithLifecycle() val currentMonth by viewModel.currentMonthFlow.collectAsStateWithLifecycle() val currentStreakDurationInDays by viewModel.currentStreakDurationInDays.collectAsStateWithLifecycle() val longestStreakDurationInDays by viewModel.longestStreakDurationInDays.collectAsStateWithLifecycle() - RoutineCalendarScreen( - modifier = modifier, - routineCalendarDates = routineCalendarDates, - currentMonth = currentMonth, - currentStreakDurationInDays = currentStreakDurationInDays, - longestStreakDurationInDays = longestStreakDurationInDays, - onCalendarDateClick = { date, routineStatus -> - viewModel.onCalendarDateClick( - date = date, - routineStatusBeforeClick = routineStatus - ) - }, - onScrolledToNewMonth = { newMonth -> - viewModel.onScrolledToNewMonth(newMonth) - } - ) + habit?.let { + RoutineCalendarScreen( + modifier = modifier, + habit = it, + routineCalendarDates = routineCalendarDates, + currentMonth = currentMonth, + currentStreakDurationInDays = currentStreakDurationInDays, + longestStreakDurationInDays = longestStreakDurationInDays, + insertCompletion = { completionRecord -> + viewModel.onHabitComplete(completionRecord) + }, + onScrolledToNewMonth = { newMonth -> + viewModel.onScrolledToNewMonth(newMonth) + }, + ) + } } @Composable fun RoutineCalendarScreen( modifier: Modifier = Modifier, - routineCalendarDates: List, + habit: Habit, + routineCalendarDates: Map, currentMonth: YearMonth, currentStreakDurationInDays: Int, longestStreakDurationInDays: Int, - onCalendarDateClick: (LocalDate, RoutineStatus) -> Unit, + insertCompletion: (Habit.CompletionRecord) -> Unit, onScrolledToNewMonth: (YearMonth) -> Unit, ) { Column( @@ -84,7 +86,20 @@ fun RoutineCalendarScreen( currentMonth = currentMonth, firstDayOfWeek = WeekFields.of(LocalLocale.current).firstDayOfWeek, routineCalendarDates = routineCalendarDates, - onDateClick = onCalendarDateClick, + onDateClick = { date -> + val numOfTimesCompleted = routineCalendarDates[date]?.numOfTimesCompleted + if (numOfTimesCompleted != null) { + when (habit) { + is Habit.YesNoHabit -> { + val completion = Habit.YesNoHabit.CompletionRecord( + date = date, + numOfTimesCompleted = if (numOfTimesCompleted > 0F) 0F else 1F, + ) + insertCompletion(completion) + } + } + } + }, onScrolledToNewMonth = onScrolledToNewMonth, ) @@ -99,8 +114,7 @@ fun RoutineCalendarScreen( count = currentStreakDurationInDays, ) val longestStreakDurationInDaysString = pluralStringResource( - id = com.rendox.routinetracker.core.ui. - R.plurals.num_of_days, + id = com.rendox.routinetracker.core.ui.R.plurals.num_of_days, count = longestStreakDurationInDays, ) diff --git a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/calendar/RoutineCalendarViewModel.kt b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/calendar/RoutineCalendarViewModel.kt index b8dcb59f..4e4f8f77 100644 --- a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/calendar/RoutineCalendarViewModel.kt +++ b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/calendar/RoutineCalendarViewModel.kt @@ -4,30 +4,26 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.kizitonwose.calendar.core.atStartOfMonth import com.kizitonwose.calendar.core.yearMonth -import com.rendox.routinetracker.core.data.routine.RoutineRepository -import com.rendox.routinetracker.core.domain.completion_history.use_cases.GetRoutineStatusUseCase -import com.rendox.routinetracker.core.domain.completion_history.use_cases.InsertRoutineStatusUseCase -import com.rendox.routinetracker.core.domain.completion_history.use_cases.ToggleHistoricalStatusUseCase -import com.rendox.routinetracker.core.domain.streak.GetDisplayStreaksUseCase -import com.rendox.routinetracker.core.domain.streak.checkIfContainDate +import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository +import com.rendox.routinetracker.core.data.habit.HabitRepository +import com.rendox.routinetracker.core.domain.completion_history.HabitComputeStatusUseCase +import com.rendox.routinetracker.core.domain.completion_history.InsertHabitCompletionUseCase import com.rendox.routinetracker.core.domain.streak.getCurrentStreak import com.rendox.routinetracker.core.domain.streak.getDurationInDays import com.rendox.routinetracker.core.domain.streak.getLongestStreak -import com.rendox.routinetracker.core.logic.time.LocalDateRange import com.rendox.routinetracker.core.logic.time.rangeTo import com.rendox.routinetracker.core.model.DisplayStreak -import com.rendox.routinetracker.core.model.PlanningStatus -import com.rendox.routinetracker.core.model.Routine -import com.rendox.routinetracker.core.model.RoutineStatus -import com.rendox.routinetracker.core.model.StatusEntry +import com.rendox.routinetracker.core.model.Habit +import com.rendox.routinetracker.core.model.HabitStatus +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.LocalDate @@ -36,35 +32,30 @@ import kotlinx.datetime.toJavaLocalDate import kotlinx.datetime.toKotlinLocalDate import kotlinx.datetime.todayIn import java.time.YearMonth +import java.time.temporal.ChronoUnit class RoutineCalendarViewModel( - private val today: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), + today: LocalDate = Clock.System.todayIn(TimeZone.currentSystemDefault()), private val routineId: Long, - private val routineRepository: RoutineRepository, - private val getRoutineStatusList: GetRoutineStatusUseCase, - private val insertRoutineStatus: InsertRoutineStatusUseCase, - private val toggleRoutineStatus: ToggleHistoricalStatusUseCase, - private val getAllStreaks: GetDisplayStreaksUseCase + private val habitRepository: HabitRepository, + private val computeHabitStatus: HabitComputeStatusUseCase, + private val completionHistoryRepository: CompletionHistoryRepository, + private val insertHabitCompletion: InsertHabitCompletionUseCase, ) : ViewModel() { - private lateinit var routine: Routine + private val _habitFlow: MutableStateFlow = MutableStateFlow(null) + val habitFlow: StateFlow = _habitFlow.asStateFlow() + + private val todayFlow = MutableStateFlow(today) + + private val _calendarDatesFlow = MutableStateFlow(emptyMap()) + val calendarDatesFlow = _calendarDatesFlow.asStateFlow() - private val cashedDatesFlow: MutableStateFlow>> = - MutableStateFlow(emptyMap()) private val streaksFlow = MutableStateFlow(emptyList()) private val _currentMonthFlow: MutableStateFlow = - MutableStateFlow(today.toJavaLocalDate().yearMonth) + MutableStateFlow(YearMonth.from(todayFlow.value.toJavaLocalDate())) val currentMonthFlow: StateFlow = _currentMonthFlow.asStateFlow() - val visibleDatesFlow: StateFlow> = - combine(_currentMonthFlow, cashedDatesFlow) { currentMonth, cashedDates -> - cashedDates[currentMonth] ?: emptyList() - }.stateIn( - scope = viewModelScope, - initialValue = emptyList(), - started = SharingStarted.WhileSubscribed(5_000), - ) - val currentStreakDurationInDays: StateFlow = streaksFlow.map { streaks -> streaks.getCurrentStreak(today)?.getDurationInDays() ?: 0 }.stateIn( @@ -81,96 +72,177 @@ class RoutineCalendarViewModel( started = SharingStarted.WhileSubscribed(5_000), ) + private val updateMonthRunningJobsFlow = MutableStateFlow(emptyMap>()) + init { + // TODO remove explicit main dispatcher viewModelScope.launch { - routine = routineRepository.getRoutineById(routineId) - streaksFlow.update { - val streaks = getAllStreaks(routineId = routineId, today = today) - println("all streaks = $streaks") - streaks - } + _habitFlow.update { habitRepository.getHabitById(routineId) } } + viewModelScope.launch { - val monthStart = _currentMonthFlow.value.atStartOfMonth().toKotlinLocalDate() - val monthEnd = _currentMonthFlow.value.atEndOfMonth().toKotlinLocalDate() - val initialMonthPeriod = fetchAndDeriveCalendarDates(period = monthStart..monthEnd) - cashedDatesFlow.update { - it.toMutableMap().apply { put(_currentMonthFlow.value, initialMonthPeriod) } + updateMonth(_currentMonthFlow.value) + for (i in 1..NumOfMonthsToLoadInitially) { + updateMonth(_currentMonthFlow.value.plusMonths(i.toLong())) + updateMonth(_currentMonthFlow.value.minusMonths(i.toLong())) } } } - private suspend fun fetchAndDeriveCalendarDates( - period: LocalDateRange - ): List { - val routineStatusesForCurrentMonth: List = getRoutineStatusList( - routineId = routineId, - dates = period, - today = today, + /** + * @param forceUpdate update the data even if it's already loaded + */ + private suspend fun updateMonth(month: YearMonth, forceUpdate: Boolean = false) { + if (!forceUpdate) { + val dataForMonthIsAlreadyLoaded = _calendarDatesFlow.value.keys.find { + it.toJavaLocalDate().yearMonth == month + } != null + if (dataForMonthIsAlreadyLoaded) return + } + + val thisMonthIsAlreadyBeingUpdated = + updateMonthRunningJobsFlow.value.contains(month) + if (thisMonthIsAlreadyBeingUpdated) { + if (forceUpdate) { + updateMonthRunningJobsFlow.value[month]?.forEach { it.cancel() } + } else { + return + } + } + + val monthStart = month.atStartOfMonth().toKotlinLocalDate() + val monthEnd = month.atEndOfMonth().toKotlinLocalDate() + + for (date in monthStart..monthEnd) { + val job = viewModelScope.launch { + updateStatusForDate(date) + } + updateMonthRunningJobsFlow.update { updateMonthJobsRunning -> + updateMonthJobsRunning.toMutableMap().also { + it[month] = updateMonthJobsRunning[month].orEmpty().toMutableList().apply { + add(job) + } + } + } + } + + updateMonthRunningJobsFlow.value[month]?.joinAll() + updateMonthRunningJobsFlow.update { oldValue -> + oldValue.toMutableMap().apply { remove(month) } + } + } + + private suspend fun updateStatusForDate(date: LocalDate) { + val habitStatus = computeHabitStatus( + habitId = routineId, + validationDate = date, + today = todayFlow.value, + ) + val numOfTimesCompleted = completionHistoryRepository.getRecordByDate( + habitId = routineId, + date = date, + )?.numOfTimesCompleted ?: 0F + + val calendarDateData = CalendarDateData( + status = habitStatus, + includedInStreak = false, // TODO Implement streaks + numOfTimesCompleted = numOfTimesCompleted, ) - println("RoutineCalendarViewModelLog routineStatusesForCurrentMonth = $routineStatusesForCurrentMonth") - return routineStatusesForCurrentMonth.map { - val includedInStreak = streaksFlow.value.checkIfContainDate(it.date) - println("${it.date} included in streak = $includedInStreak") - RoutineCalendarDate( - date = it.date, - status = it.status, - includedInStreak = includedInStreak, - ) + + _calendarDatesFlow.update { oldValue -> + oldValue.toMutableMap().also { + it[date] = calendarDateData + } } } fun onScrolledToNewMonth(newMonth: YearMonth) { _currentMonthFlow.update { newMonth } + viewModelScope.launch { + updateMonth(month = newMonth) - if (!cashedDatesFlow.value.contains(newMonth)) { - viewModelScope.launch { - val monthStart = newMonth.atStartOfMonth().toKotlinLocalDate() - val monthEnd = newMonth.atEndOfMonth().toKotlinLocalDate() - val currentMonthDates = fetchAndDeriveCalendarDates(monthStart..monthEnd) - cashedDatesFlow.update { - it.toMutableMap().apply { put(newMonth, currentMonthDates) } + val latestDate = _calendarDatesFlow.value.keys.maxOrNull()?.toJavaLocalDate() + val latestMonth = latestDate?.let { YearMonth.from(it) } + if (latestMonth != null) { + val numOfMonthsUntilLastLoadedMonth = + newMonth.until(latestMonth, ChronoUnit.MONTHS) + if (numOfMonthsUntilLastLoadedMonth < LoadAheadThreshold) { + for (monthNumber in 1..NumOfMonthsToLoadAhead) { + updateMonth(latestMonth.plusMonths(monthNumber.toLong())) + } + } + } + + val earliestDate = _calendarDatesFlow.value.keys.minOrNull()?.toJavaLocalDate() + val earliestMonth = earliestDate?.let { YearMonth.from(it) } + if (earliestMonth != null) { + val numOfMonthsUntilFirstLoadedMonth = + earliestMonth.until(newMonth, ChronoUnit.MONTHS) + if (numOfMonthsUntilFirstLoadedMonth < LoadAheadThreshold) { + for (monthNumber in 1..NumOfMonthsToLoadAhead) { + updateMonth(earliestMonth.minusMonths(monthNumber.toLong())) + } } } } } - fun onCalendarDateClick(date: LocalDate, routineStatusBeforeClick: RoutineStatus) { + fun onHabitComplete(completionRecord: Habit.CompletionRecord) { + updateMonthRunningJobsFlow.value.values.forEach { jobs -> + jobs.forEach { it.cancel() } + } + viewModelScope.launch { - if (routineStatusBeforeClick is PlanningStatus) { - insertRoutineStatus( - routineId = routineId, - currentDate = date, - completedOnCurrentDate = true, - today = today, - ) - } else { - toggleRoutineStatus( - routineId = routineId, - currentDate = date, - today = today, + try { + insertHabitCompletion( + habitId = routineId, + completionRecord = completionRecord, + today = todayFlow.value, ) + } catch (e: InsertHabitCompletionUseCase.IllegalDateException) { + // TODO display a snackbar } - val streaks = getAllStreaks(routineId = routineId, today = today) - streaksFlow.update { - streaks - } + updateMonth(_currentMonthFlow.value, forceUpdate = true) - val monthStart = _currentMonthFlow.value.atStartOfMonth().toKotlinLocalDate() - val monthEnd = _currentMonthFlow.value.atEndOfMonth().toKotlinLocalDate() - val initialMonthPeriod = fetchAndDeriveCalendarDates(period = monthStart..monthEnd) - println("RoutineCalendarViewModelLog current month history: $initialMonthPeriod") - println("RoutineCalendarViewModelLog streaks = $streaks") - cashedDatesFlow.update { - it.toMutableMap().apply { put(_currentMonthFlow.value, initialMonthPeriod) } + // delete other months because they may be outdated + _calendarDatesFlow.update { calendarDates -> + calendarDates.filter { + it.key.toJavaLocalDate().yearMonth == _currentMonthFlow.value + } } } } + +// TODO update todayFlow when the date changes (in case the screen is opened at midnight) + + companion object { + /** + * The number of months that should be loaded in both directions when the list of data is + * empty. It's done for the user to see the pre-loaded data when they scroll to the next + * month. + */ + const val NumOfMonthsToLoadInitially = 6 + + /** + * The number of months that should be loaded ahead or behind the current month (depending + * on the user's scroll direction). It's done for the user to see the pre-loaded data + * when they scroll to the next month. + */ + const val NumOfMonthsToLoadAhead = 3 + + /** + * The maximum number of months between the current month and the last/first loaded month + * that doesn't trigger the pre-loading of future/past months. If this threshold is + * exceeded, the data should be loaded. It's done for the user to see the pre-loaded data + * when they scroll to the next month. + */ + const val LoadAheadThreshold = 3 + } } -data class RoutineCalendarDate( - val date: LocalDate, - val status: RoutineStatus, +data class CalendarDateData( + val status: HabitStatus, val includedInStreak: Boolean, + val numOfTimesCompleted: Float, ) \ No newline at end of file diff --git a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/di/RoutineDetailsModule.kt b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/di/RoutineDetailsModule.kt index 2c68eac2..6cf366c0 100644 --- a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/di/RoutineDetailsModule.kt +++ b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/di/RoutineDetailsModule.kt @@ -9,17 +9,16 @@ val routineDetailsModule = module { viewModel { parameters -> RoutineCalendarViewModel( routineId = parameters.get(), - routineRepository = get(), - getRoutineStatusList = get(), - insertRoutineStatus = get(), - toggleRoutineStatus = get(), - getAllStreaks = get(), + habitRepository = get(), + computeHabitStatus = get(), + completionHistoryRepository = get(), + insertHabitCompletion = get(), ) } viewModel { parameters -> RoutineDetailsViewModel( routineId = parameters.get(), - routineRepository = get(), + habitRepository = get(), ) } } \ No newline at end of file diff --git a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/navigation/RoutineDetailsNavigation.kt b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/navigation/RoutineDetailsNavigation.kt index 989ec7cc..008a7c0c 100644 --- a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/navigation/RoutineDetailsNavigation.kt +++ b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/navigation/RoutineDetailsNavigation.kt @@ -1,11 +1,7 @@ package com.rendox.routinetracker.routine_details.navigation -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.TweenSpec -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder @@ -33,23 +29,11 @@ fun NavGraphBuilder.routineDetailsScreen(popBackStack: () -> Unit) { navArgument(routineIdArg) { type = NavType.LongType } ), enterTransition = { - fadeIn() + scaleIn( - initialScale = 0.75f, - animationSpec = TweenSpec( - durationMillis = 100, - easing = LinearEasing, - ) - ) + EnterTransition.None }, exitTransition = { - fadeOut() + scaleOut( - targetScale = 0.75f, - animationSpec = TweenSpec( - durationMillis = 100, - easing = LinearEasing, - ) - ) - } + ExitTransition.None + }, ) { val routineId = it.arguments!!.getLong(routineIdArg) RoutineDetailsRoute( diff --git a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/statistics/RoutineProperties.kt b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/statistics/RoutineProperties.kt index fafbe790..2029f010 100644 --- a/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/statistics/RoutineProperties.kt +++ b/feature/routine_details/src/main/java/com/rendox/routinetracker/routine_details/statistics/RoutineProperties.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.rendox.routinetracker.core.model.Routine import com.rendox.routinetracker.core.model.Schedule import com.rendox.routinetracker.core.ui.components.WrapTextContent import com.rendox.routinetracker.core.ui.helpers.LocalLocale @@ -46,7 +45,7 @@ fun RoutineProperties( sessionDurationMinutes: Int?, completionTime: LocalTime?, reminderEnabled: Boolean, - numericalValueRoutineUnit: Routine.NumericalValueRoutineUnit?, +// numericalValueHabitUnit: Habit.NumericalValueRoutineUnit?, ) { Column(modifier = modifier) { description?.let { @@ -140,19 +139,19 @@ fun RoutineProperties( ) } - numericalValueRoutineUnit?.let { - RoutineDetailLabel( - modifier = Modifier.weight(weight = 3.7f, fill = false), - icon = painterResource( - id = com.rendox.routinetracker.feature.routine_details.R.drawable.baseline_check_circle_24 - ), - text = deriveAmountOfWorkPerSession( - numOfUnitsPerSession = it.numOfUnitsPerSession, - unitsOfMeasure = it.unitsOfMeasure, - sessionUnit = it.sessionUnit, - ), - ) - } +// numericalValueHabitUnit?.let { +// RoutineDetailLabel( +// modifier = Modifier.weight(weight = 3.7f, fill = false), +// icon = painterResource( +// id = com.rendox.routinetracker.feature.routine_details.R.drawable.baseline_check_circle_24 +// ), +// text = deriveAmountOfWorkPerSession( +// numOfUnitsPerSession = it.numOfUnitsPerSession, +// unitsOfMeasure = it.unitsOfMeasure, +// sessionUnit = it.sessionUnit, +// ), +// ) +// } } } } @@ -179,7 +178,7 @@ private fun deriveRoutineFrequency( "$numOfDays $perWeek" } - Schedule.PeriodicCustomSchedule::class -> { + Schedule.AlternateDaysSchedule::class -> { val numOfActivityDaysString = pluralStringResource( id = R.plurals.num_of_days, count = numOfDueDaysPerPeriod, @@ -298,11 +297,11 @@ private fun RoutinePropertiesPreview() { sessionDurationMinutes = 65, completionTime = LocalTime(23, 15), reminderEnabled = true, - numericalValueRoutineUnit = Routine.NumericalValueRoutineUnit( - numOfUnitsPerSession = 7, - unitsOfMeasure = "books", - DateTimeUnit.YEAR, - ), +// numericalValueHabitUnit = Habit.NumericalValueRoutineUnit( +// numOfUnitsPerSession = 7, +// unitsOfMeasure = "books", +// DateTimeUnit.YEAR, +// ), ) } } \ No newline at end of file diff --git a/feature/routine_details/src/test/java/com/rendox/routinetracker/routine_stats/RoutineCalendarViewModelTest.kt b/feature/routine_details/src/test/java/com/rendox/routinetracker/routine_stats/RoutineCalendarViewModelTest.kt new file mode 100644 index 00000000..efd1a74d --- /dev/null +++ b/feature/routine_details/src/test/java/com/rendox/routinetracker/routine_stats/RoutineCalendarViewModelTest.kt @@ -0,0 +1,296 @@ +package com.rendox.routinetracker.routine_stats + +import com.google.common.truth.Truth.assertThat +import com.kizitonwose.calendar.core.atStartOfMonth +import com.kizitonwose.calendar.core.yearMonth +import com.rendox.routinetracker.core.data.completion_history.CompletionHistoryRepository +import com.rendox.routinetracker.core.data.habit.HabitRepository +import com.rendox.routinetracker.core.domain.completion_history.HabitComputeStatusUseCase +import com.rendox.routinetracker.core.domain.completion_history.InsertHabitCompletionUseCase +import com.rendox.routinetracker.core.logic.time.atEndOfMonth +import com.rendox.routinetracker.core.logic.time.rangeTo +import com.rendox.routinetracker.core.model.Habit +import com.rendox.routinetracker.core.model.Schedule +import com.rendox.routinetracker.core.testcommon.fakes.habit.CompletionHistoryRepositoryFake +import com.rendox.routinetracker.core.testcommon.fakes.habit.HabitData +import com.rendox.routinetracker.core.testcommon.fakes.habit.HabitRepositoryFake +import com.rendox.routinetracker.core.testcommon.fakes.habit.VacationRepositoryFake +import com.rendox.routinetracker.routine_details.calendar.CalendarDateData +import com.rendox.routinetracker.routine_details.calendar.RoutineCalendarViewModel +import com.rendox.routinetracker.routine_details.calendar.RoutineCalendarViewModel.Companion.LoadAheadThreshold +import com.rendox.routinetracker.routine_details.calendar.RoutineCalendarViewModel.Companion.NumOfMonthsToLoadAhead +import com.rendox.routinetracker.routine_details.calendar.RoutineCalendarViewModel.Companion.NumOfMonthsToLoadInitially +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.LocalDate +import kotlinx.datetime.minus +import kotlinx.datetime.plus +import kotlinx.datetime.toJavaLocalDate +import kotlinx.datetime.toKotlinLocalDate +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.koin.core.context.stopKoin + +class RoutineCalendarViewModelTest { + + private lateinit var habitRepository: HabitRepository + private lateinit var completionHistoryRepository: CompletionHistoryRepository + private lateinit var computeHabitStatus: HabitComputeStatusUseCase + private lateinit var insertHabitCompletion: InsertHabitCompletionUseCase + + @OptIn(ExperimentalCoroutinesApi::class) + val coroutineDispatcher = UnconfinedTestDispatcher() + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setUp() = runTest { + Dispatchers.setMain(coroutineDispatcher) + + val habitData = HabitData() + habitRepository = HabitRepositoryFake(habitData) + completionHistoryRepository = CompletionHistoryRepositoryFake(habitData) + computeHabitStatus = HabitComputeStatusUseCase( + habitRepository = habitRepository, + vacationRepository = VacationRepositoryFake(habitData), + completionHistoryRepository = completionHistoryRepository, + dispatcher = coroutineDispatcher, + ) + insertHabitCompletion = InsertHabitCompletionUseCase( + completionHistoryRepository = completionHistoryRepository, + habitRepository = habitRepository, + ) + + habitRepository.insertHabit( + Habit.YesNoHabit( + id = 1L, + name = "RoutineCalendarViewModelTest Habit", + schedule = Schedule.EveryDaySchedule( + startDate = LocalDate(2022, 1, 1) + ), + ) + ) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + stopKoin() + Dispatchers.resetMain() + } + + @Test + fun `assert the first month is initialized with correct dates`() { + val today = LocalDate(2024, 1, 1) + val viewModel = RoutineCalendarViewModel( + routineId = 1L, + today = today, + habitRepository = habitRepository, + computeHabitStatus = computeHabitStatus, + completionHistoryRepository = completionHistoryRepository, + insertHabitCompletion = insertHabitCompletion, + ) + val initialMonth = today.toJavaLocalDate().yearMonth + val monthStart = initialMonth.atStartOfMonth().toKotlinLocalDate() + val monthEnd = initialMonth.atEndOfMonth().toKotlinLocalDate() + val resultingDates = viewModel.calendarDatesFlow.value.keys + assertThat(resultingDates).containsAtLeastElementsIn(monthStart..monthEnd) + } + + @Test + fun `assert months before and after the initial month get preloaded`() { + val today = LocalDate(2024, 1, 1) + val viewModel = RoutineCalendarViewModel( + routineId = 1L, + today = today, + habitRepository = habitRepository, + computeHabitStatus = computeHabitStatus, + completionHistoryRepository = completionHistoryRepository, + insertHabitCompletion = insertHabitCompletion, + ) + val initialMonth = today.toJavaLocalDate().yearMonth + val pastPeriodStart = initialMonth.minusMonths(NumOfMonthsToLoadInitially.toLong()) + .atStartOfMonth().toKotlinLocalDate() + val pastPeriodEnd = initialMonth.minusMonths(1) + .atEndOfMonth().toKotlinLocalDate() + val futurePeriodStart = initialMonth.plusMonths(1) + .atStartOfMonth().toKotlinLocalDate() + val futurePeriodEnd = initialMonth.plusMonths(NumOfMonthsToLoadInitially.toLong()) + .atEndOfMonth().toKotlinLocalDate() + + val resultingDates = viewModel.calendarDatesFlow.value.keys + assertThat(resultingDates).containsAtLeastElementsIn(pastPeriodStart..pastPeriodEnd) + assertThat(resultingDates).containsAtLeastElementsIn(futurePeriodStart..futurePeriodEnd) + } + + @Test + fun `assert already loaded months do not get loaded again upon scrolling`() { + val today = LocalDate(2024, 1, 1) + val viewModel = RoutineCalendarViewModel( + routineId = 1L, + today = today, + habitRepository = habitRepository, + computeHabitStatus = computeHabitStatus, + completionHistoryRepository = completionHistoryRepository, + insertHabitCompletion = insertHabitCompletion, + ) + val initialDates = viewModel.calendarDatesFlow.value + for (i in 1..(NumOfMonthsToLoadInitially - LoadAheadThreshold)) { + viewModel.onScrolledToNewMonth( + newMonth = today.toJavaLocalDate().yearMonth.plusMonths(i.toLong()) + ) + } + assertThat(viewModel.calendarDatesFlow.value).containsExactlyEntriesIn(initialDates) + + for (i in 1..(NumOfMonthsToLoadInitially - LoadAheadThreshold)) { + viewModel.onScrolledToNewMonth( + newMonth = today.toJavaLocalDate().yearMonth.minusMonths(i.toLong()) + ) + } + assertThat(viewModel.calendarDatesFlow.value).containsExactlyEntriesIn(initialDates) + } + + @Test + fun `assert exceeding the threshold in future results in additional future months loading`() { + val today = LocalDate(2024, 1, 1) + val viewModel = RoutineCalendarViewModel( + routineId = 1L, + today = today, + habitRepository = habitRepository, + computeHabitStatus = computeHabitStatus, + completionHistoryRepository = completionHistoryRepository, + insertHabitCompletion = insertHabitCompletion, + ) + + for (i in 1..(LoadAheadThreshold + 1)) { + viewModel.onScrolledToNewMonth( + newMonth = today.toJavaLocalDate().yearMonth.plusMonths(i.toLong()) + ) + } + val resultingDates = viewModel.calendarDatesFlow.value.keys + val firstExpectedDate = + today.minus(DatePeriod(months = NumOfMonthsToLoadInitially)) + val lastExpectedDate = + today.plus(DatePeriod(months = NumOfMonthsToLoadInitially + NumOfMonthsToLoadAhead)) + .atEndOfMonth + assertThat(resultingDates).containsExactlyElementsIn(firstExpectedDate..lastExpectedDate) + } + + @Test + fun `assert exceeding the threshold in past results in additional past months loading`() { + val today = LocalDate(2024, 1, 1) + val viewModel = RoutineCalendarViewModel( + routineId = 1L, + today = today, + habitRepository = habitRepository, + computeHabitStatus = computeHabitStatus, + completionHistoryRepository = completionHistoryRepository, + insertHabitCompletion = insertHabitCompletion, + ) + + for (i in 1..(LoadAheadThreshold + 1)) { + viewModel.onScrolledToNewMonth( + newMonth = today.toJavaLocalDate().yearMonth.minusMonths(i.toLong()) + ) + } + val resultingDates = viewModel.calendarDatesFlow.value.keys + val firstExpectedDate = today.minus( + DatePeriod(months = NumOfMonthsToLoadInitially + NumOfMonthsToLoadAhead) + ) + val lastExpectedDate = today.plus(DatePeriod(months = NumOfMonthsToLoadInitially)) + .atEndOfMonth + assertThat(resultingDates).containsExactlyElementsIn(firstExpectedDate..lastExpectedDate) + } + + @Test + fun `assert the data is updated when a completion is inserted`() = runTest { + val today = LocalDate(2024, 1, 1) + val viewModel = RoutineCalendarViewModel( + routineId = 1L, + today = today, + habitRepository = habitRepository, + computeHabitStatus = computeHabitStatus, + completionHistoryRepository = completionHistoryRepository, + insertHabitCompletion = insertHabitCompletion, + ) + viewModel.onHabitComplete( + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = today, + numOfTimesCompleted = 1F, + ) + ) + val resultingDates = viewModel.calendarDatesFlow.value + val expectedDates = (today..today.atEndOfMonth).associateWith { + CalendarDateData( + status = computeHabitStatus(habitId = 1L, validationDate = it, today = today), + includedInStreak = false, + numOfTimesCompleted = if (it == today) 1F else 0F, + ) + } + assertThat(resultingDates).containsAtLeastEntriesIn(expectedDates) + } + + @Test + fun `assert all other months get deleted when a completion is inserted`() = runTest { + val today = LocalDate(2024, 1, 1) + val viewModel = RoutineCalendarViewModel( + routineId = 1L, + today = today, + habitRepository = habitRepository, + computeHabitStatus = computeHabitStatus, + completionHistoryRepository = completionHistoryRepository, + insertHabitCompletion = insertHabitCompletion, + ) + viewModel.onHabitComplete( + completionRecord = Habit.YesNoHabit.CompletionRecord( + date = today, + numOfTimesCompleted = 1F, + ) + ) + + val resultingDates: Iterable = viewModel.calendarDatesFlow.value.keys + val expectedDates: Iterable = today..today.atEndOfMonth + assertThat(resultingDates).containsExactlyElementsIn(expectedDates) + } + + @Test + fun `assert months get loaded with the margin when scrolled to a not already loaded month`() = runTest { + val today = LocalDate(2024, 1, 1) + val viewModel = RoutineCalendarViewModel( + routineId = 1L, + today = today, + habitRepository = habitRepository, + computeHabitStatus = computeHabitStatus, + completionHistoryRepository = completionHistoryRepository, + insertHabitCompletion = insertHabitCompletion, + ) + val futureMonth = + today.toJavaLocalDate().yearMonth.plusMonths((NumOfMonthsToLoadInitially + 1).toLong()) + viewModel.onScrolledToNewMonth(futureMonth) + + // additional months get loaded only in the future because of the direction of the scroll + val futureMonthStartDate = futureMonth.atStartOfMonth().toKotlinLocalDate() + val lastDateToLoadAhead = futureMonth.plusMonths(NumOfMonthsToLoadAhead.toLong()) + .atEndOfMonth().toKotlinLocalDate() + val expectedFutureDates: Iterable = futureMonthStartDate..lastDateToLoadAhead + + val pastMonth = + today.toJavaLocalDate().yearMonth.minusMonths((NumOfMonthsToLoadInitially + 1).toLong()) + viewModel.onScrolledToNewMonth(pastMonth) + + // additional months get loaded only in the past because of the direction of the scroll + val pastMonthEndDate = pastMonth.atEndOfMonth().toKotlinLocalDate() + val firstDateToLoadBehind = pastMonth.minusMonths(NumOfMonthsToLoadAhead.toLong()) + .atStartOfMonth().toKotlinLocalDate() + val expectedPastDates: Iterable = firstDateToLoadBehind..pastMonthEndDate + + val resultingDates: Iterable = viewModel.calendarDatesFlow.value.keys + assertThat(resultingDates).containsAtLeastElementsIn(expectedFutureDates) + assertThat(resultingDates).containsAtLeastElementsIn(expectedPastDates) + } +} \ No newline at end of file