Skip to content

Commit

Permalink
WTA #44: Updated CalculateCurrentStreakUC to handle some base cases a…
Browse files Browse the repository at this point in the history
…nd added tests.
  • Loading branch information
Jacob3075 committed Oct 25, 2022
1 parent 55ae467 commit c4ac24c
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 73 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package com.jacob.wakatimeapp.core.common

import java.time.format.TextStyle.SHORT
import java.util.Locale
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime

val LocalDate.Companion.today
get() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
fun Instant.toDate(timeZone: TimeZone = TimeZone.currentSystemDefault()) =
toLocalDateTime(timeZone).date

fun LocalDate.getDisplayNameForDay(): String =
dayOfWeek.getDisplayName(SHORT, Locale.getDefault())
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,77 @@ package com.jacob.wakatimeapp.home.domain.usecases

import arrow.core.Either
import arrow.core.computations.either
import arrow.core.right
import com.jacob.wakatimeapp.core.common.today
import com.jacob.wakatimeapp.core.common.toDate
import com.jacob.wakatimeapp.core.models.Error
import com.jacob.wakatimeapp.core.models.Time
import com.jacob.wakatimeapp.home.data.local.HomePageCache
import com.jacob.wakatimeapp.home.domain.InstantProvider
import com.jacob.wakatimeapp.home.domain.models.Last7DaysStats
import com.jacob.wakatimeapp.home.domain.models.StreakRange
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.datetime.LocalDate
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.minus

@Singleton
class CalculateCurrentStreakUC @Inject constructor(
dispatcher: CoroutineContext = Dispatchers.IO,
private val homePageCache: HomePageCache,
private val instantProvider: InstantProvider,
) {

suspend operator fun invoke(): Either<Error, StreakRange> {
val currentStreakFlow = homePageCache.getCurrentStreak().first()
val last7DaysStatsFlow = homePageCache.getLast7DaysStats()

last7DaysStatsFlow.collect { last7DaysStatsEither ->
either {
val last7DaysStats = last7DaysStatsEither.bind()
val currentStreak = currentStreakFlow.bind()

val todaysStats = last7DaysStats.weeklyTimeSpent[LocalDate.today] ?: Time.ZERO

if (todaysStats == Time.ZERO) return@either

val updatedStreakRange = if (currentStreak == StreakRange.ZERO) {
StreakRange(
start = LocalDate.today,
end = LocalDate.today,
)
} else {
StreakRange(
start = currentStreak.start,
end = LocalDate.today,
)
}

homePageCache.updateCurrentStreak(updatedStreakRange)
}
suspend operator fun invoke(): Either<Error, StreakRange> = either {
val last7DaysStats = homePageCache.getLast7DaysStats().first().bind()
val currentStreak = homePageCache.getCurrentStreak().first().bind()

val today = instantProvider.now().toDate()
val todaysStats = last7DaysStats.weeklyTimeSpent[today] ?: Time.ZERO

val endOfCurrentStreakIsYesterday = currentStreak.end == today.minus(1, DateTimeUnit.DAY)

when {
isCurrentStreakActive(endOfCurrentStreakIsYesterday, todaysStats) -> currentStreak

isCurrentStreakOngoing(
endOfCurrentStreakIsYesterday,
todaysStats
) -> currentStreak.copy(end = today)

else -> getNewCurrentStreak(currentStreak, last7DaysStats)
}
}

/**
* Streak is considered active if the last date in the streak is the previous day but
* there is no stats for the current day.
*/
private fun isCurrentStreakActive(endOfCurrentStreakIsYesterday: Boolean, todaysStats: Time) =
endOfCurrentStreakIsYesterday && todaysStats == Time.ZERO

/**
* Streak is considered on-going if there is an active streak and there is stats for the current day.
*/
private fun isCurrentStreakOngoing(endOfCurrentStreakIsYesterday: Boolean, todaysStats: Time) =
endOfCurrentStreakIsYesterday && todaysStats != Time.ZERO

private fun getNewCurrentStreak(
currentStreak: StreakRange,
last7DaysStats: Last7DaysStats,
): StreakRange {
if (instantProvider.now().toDate().minus(currentStreak.end).days > 7) {
}

return StreakRange.ZERO.right()
val statsForCurrentStreakRange = last7DaysStats.weeklyTimeSpent
.toSortedMap()
.entries
.reversed()
.takeWhile { it.value != Time.ZERO }

if (statsForCurrentStreakRange.isEmpty()) return StreakRange.ZERO

return StreakRange(
start = statsForCurrentStreakRange.last().key,
end = statsForCurrentStreakRange.first().key,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
package com.jacob.wakatimeapp.home.domain.usecases

import arrow.core.Either
import com.jacob.wakatimeapp.core.common.today
import com.jacob.wakatimeapp.core.models.Error
import com.jacob.wakatimeapp.core.models.Time
import com.jacob.wakatimeapp.home.data.local.HomePageCache
import com.jacob.wakatimeapp.home.domain.InstantProvider
import com.jacob.wakatimeapp.home.domain.models.Last7DaysStats
import com.jacob.wakatimeapp.home.domain.models.StreakRange
import io.kotest.assertions.asClue
import io.kotest.matchers.shouldBe
import io.mockk.clearMocks
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.minutes
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.datetime.LocalDate
import kotlinx.datetime.DateTimeUnit
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.minus
import kotlinx.datetime.toLocalDateTime

@OptIn(ExperimentalCoroutinesApi::class)
internal class CalculateCurrentStreakUCRobot {
private lateinit var useCase: CalculateCurrentStreakUC

Expand All @@ -30,17 +34,23 @@ internal class CalculateCurrentStreakUCRobot {
result = null

useCase = CalculateCurrentStreakUC(
dispatcher = UnconfinedTestDispatcher(),
homePageCache = mockCache,
instantProvider = object : InstantProvider {
override val timeZone = TimeZone.UTC

override fun now() = currentDayInstant
}
)
}

suspend fun callUseCase() = apply {
result = useCase()
}

fun resultsShouldBe(expected: Error?) = apply {
result shouldBe expected
fun resultsShouldBe(expected: Either<Error, StreakRange>) = apply {
result.asClue {
it shouldBe expected
}
}

fun mockGetCurrentStreak(data: Either<Error, StreakRange>) = apply {
Expand All @@ -51,18 +61,51 @@ internal class CalculateCurrentStreakUCRobot {
coEvery { mockCache.getLast7DaysStats() } returns flowOf(data)
}

fun verifyUpdateCurrentStreakCalled(data: StreakRange, count: Int = 1) = apply {
coVerify(exactly = count) { mockCache.updateCurrentStreak(data) }
}

companion object {
internal companion object {
val streakRange = StreakRange.ZERO
val today = LocalDate.today

/**
* Start of a random day
*
* Value:
* - date: 11/10/2022 (dd/mm/yyyy)
* - time: 00:00:00 (hh:mm::ss)
*/
val startOfDay = Instant.parse("2022-10-11T00:00:00Z")

val currentDayInstant = startOfDay + 1.hours + 30.minutes

/**
* Takes [currentDayInstant] and subtracts 1 day from it
*/
val previousDayInstant = currentDayInstant.minus(1.days)

val currentDay = currentDayInstant.toLocalDateTime(TimeZone.UTC).date

val noWeeklyStats = mapOf(
currentDay to Time.ZERO,
currentDay.minus(1, DateTimeUnit.DAY) to Time.ZERO,
currentDay.minus(2, DateTimeUnit.DAY) to Time.ZERO,
currentDay.minus(3, DateTimeUnit.DAY) to Time.ZERO,
currentDay.minus(4, DateTimeUnit.DAY) to Time.ZERO,
currentDay.minus(5, DateTimeUnit.DAY) to Time.ZERO,
currentDay.minus(6, DateTimeUnit.DAY) to Time.ZERO,
)

val continuousWeeklyStats = mutableMapOf(
currentDay to Time.fromDecimal(1f),
currentDay.minus(1, DateTimeUnit.DAY) to Time.fromDecimal(1f),
currentDay.minus(2, DateTimeUnit.DAY) to Time.fromDecimal(1f),
currentDay.minus(3, DateTimeUnit.DAY) to Time.fromDecimal(1f),
currentDay.minus(4, DateTimeUnit.DAY) to Time.fromDecimal(1f),
currentDay.minus(5, DateTimeUnit.DAY) to Time.fromDecimal(1f),
currentDay.minus(6, DateTimeUnit.DAY) to Time.fromDecimal(1f),
)

val last7DaysStats = Last7DaysStats(
timeSpentToday = Time.ZERO,
projectsWorkedOn = listOf(),
weeklyTimeSpent = mutableMapOf(),
weeklyTimeSpent = noWeeklyStats,
mostUsedLanguage = "",
mostUsedEditor = "",
mostUsedOs = ""
Expand Down
Loading

0 comments on commit c4ac24c

Please sign in to comment.