Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 46 additions & 15 deletions app/src/main/java/com/mensinator/app/business/CalculationsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import java.time.LocalDate
import java.time.temporal.ChronoUnit
import kotlinx.collections.immutable.PersistentSet
import kotlinx.collections.immutable.persistentSetOf

class CalculationsHelper(
private val dbHelper: IPeriodDatabaseHelper,
Expand All @@ -34,25 +36,19 @@ class CalculationsHelper(
// Basic calculation using latest period start dates
Log.d("TAG", "Basic calculation")
//do basic calculation here
//Use X latest periodstartdates (will return list of X+1)
//Use averageCycleLength function
val listPeriodDates = dbHelper.getLatestXPeriodStart(periodHistory)

if (listPeriodDates.isEmpty()) {
null
} else {
// Calculate the cycle lengths between consecutive periods
val cycleLengths = mutableListOf<Long>()
for (i in 0 until listPeriodDates.size - 1) {
val cycleLength = ChronoUnit.DAYS.between(
listPeriodDates[i],
listPeriodDates[i + 1]
)
cycleLengths.add(cycleLength)
val averageCycleLength: Double? = averageCycleLength();
if (averageCycleLength == null) {
null
} else {
listPeriodDates.last().plusDays(averageCycleLength.toLong())
}
// Calculate the average cycle length
val averageLength = cycleLengths.average()

listPeriodDates.last().plusDays(averageLength.toLong())
}.also {
Log.d("TAG", "Expected period date Basic: $it")
}
Expand Down Expand Up @@ -144,13 +140,13 @@ class CalculationsHelper(

// Takes the X (periodHistory) latest period start dates and calculates the average cycle length
// X comes from app_settings in the database
override fun averageCycleLength(): Double {
override fun averageCycleLength(): Double? {
val periodDates = dbHelper.getLatestXPeriodStart(periodHistory)

// Check if there are enough dates to calculate cycle lengths
if (periodDates.size < 2) {
// Not enough data to calculate cycle lengths
return 0.0
return null
}

// List to store cycle lengths
Expand Down Expand Up @@ -252,4 +248,39 @@ class CalculationsHelper(
override fun cycleDay(date: LocalDate): Flow<Int?> = dbHelper.dbWriteTrigger.map {
getCycleDay(date)
}
}

override fun calculateNextPeriodDates(): PersistentSet<LocalDate> {

var datesSet: PersistentSet<LocalDate> = persistentSetOf()

val firstNextPeriodDate: LocalDate? = if (lutealCalculation == 1) {
//go to advanced calculation using X latest ovulations (set by user in settings)
Log.d("TAG", "Advanced calculation")
advancedNextPeriod()
} else {
calculateNextPeriod()
}

if (firstNextPeriodDate == null) {
return datesSet
} else {
datesSet = datesSet.add(firstNextPeriodDate)
}

// Basic calculation for far-away periods
Log.d("TAG", "Basic calculation")
val listPeriodDates = dbHelper.getLatestXPeriodStart(periodHistory)

if (!listPeriodDates.isEmpty()) {
// Calculate the cycle lengths between consecutive periods
val averageCycleLength: Double? = averageCycleLength();
if (averageCycleLength != null) {
for (numberOfCycles in 2..14) {
val daysShift = averageCycleLength * numberOfCycles
datesSet = datesSet.add(listPeriodDates.last().plusDays(daysShift.toLong()))
}
}
}
return datesSet
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.mensinator.app.business

import kotlinx.coroutines.flow.Flow
import java.time.LocalDate
import kotlinx.collections.immutable.PersistentSet

/**
* This helper provides methods to calculate menstrual cycle related data
Expand Down Expand Up @@ -29,7 +30,7 @@ interface ICalculationsHelper {
* X comes from app_settings in the database
* @return The average cycle length as a double.
*/
fun averageCycleLength(): Double
fun averageCycleLength(): Double?

/**
* Calculates the average period length using the latest period start dates.
Expand Down Expand Up @@ -63,4 +64,6 @@ interface ICalculationsHelper {
fun getCycleDay(date: LocalDate): Int?

fun cycleDay(date: LocalDate): Flow<Int?>

fun calculateNextPeriodDates(): PersistentSet<LocalDate>
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.mensinator.app.business

import java.time.LocalDate
import kotlinx.collections.immutable.PersistentSet

interface IPeriodPrediction {
fun getPredictedPeriodDate(): LocalDate?
fun getPredictedPeriodDates(): PersistentSet<LocalDate>

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class OvulationPrediction(
override fun getPredictedOvulationDate(): LocalDate? {
val periodCount = dbHelper.getPeriodCount()
val ovulationCount = dbHelper.getOvulationCount()
val periodPredictionDate = periodPrediction.getPredictedPeriodDate()
val periodPredictionDate = periodPrediction.getPredictedPeriodDates().firstOrNull()
val lastOvulationDate = dbHelper.getNewestOvulationDate()
val firstDayOfNextMonth = LocalDate.now().withDayOfMonth(1).plusMonths(1)
val previousFirstPeriodDate = dbHelper.getFirstPreviousPeriodDate(firstDayOfNextMonth)
Expand Down Expand Up @@ -55,4 +55,4 @@ class OvulationPrediction(
null
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package com.mensinator.app.business

import java.time.LocalDate
import kotlinx.collections.immutable.PersistentSet
import kotlinx.collections.immutable.persistentSetOf

class PeriodPrediction(
private val dbHelper: IPeriodDatabaseHelper,
private val calcHelper: ICalculationsHelper,
) : IPeriodPrediction {

override fun getPredictedPeriodDate(): LocalDate? {
override fun getPredictedPeriodDates(): PersistentSet<LocalDate> {
val periodCount = dbHelper.getPeriodCount()
if (periodCount < 2) {
return null
return persistentSetOf()
}

return calcHelper.calculateNextPeriod()
return calcHelper.calculateNextPeriodDates()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class NotificationScheduler(
withContext(dispatcherProvider.IO) {
val periodReminderDays =
dbHelper.getSettingByKey(IntSetting.REMINDER_DAYS.settingDbKey)?.value?.toIntOrNull() ?: defaultReminderDays
val nextPeriodDate = periodPrediction.getPredictedPeriodDate()
val nextPeriodDate = periodPrediction.getPredictedPeriodDates().firstOrNull()
val initPeriodKeyOrCustomMessage =
dbHelper.getStringSettingByKey(StringSetting.PERIOD_NOTIFICATION_MESSAGE.settingDbKey)
val periodMessageText =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ fun Day(
val dayColors = when {
day.date in state.selectedDays -> settingColors[ColorSetting.SELECTION]
day.date in state.periodDates.keys -> settingColors[ColorSetting.PERIOD]
state.periodPredictionDate?.isEqual(day.date) == true -> settingColors[ColorSetting.EXPECTED_PERIOD]
day.date in state.periodPredictionDates -> settingColors[ColorSetting.EXPECTED_PERIOD]
day.date in state.ovulationDates -> settingColors[ColorSetting.OVULATION]
state.ovulationPredictionDate?.isEqual(day.date) == true -> settingColors[ColorSetting.EXPECTED_OVULATION]
else -> null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class CalendarViewModel(
_viewState.update {
it.copy(
showCycleNumbers = showCycleNumbersSetting,
periodPredictionDate = periodPrediction.getPredictedPeriodDate(),
periodPredictionDates = periodPrediction.getPredictedPeriodDates(),
ovulationPredictionDate = ovulationPrediction.getPredictedOvulationDate(),
periodDates = dbHelper.getPeriodDatesForMonthNew(
it.focusedYearMonth.year,
Expand Down Expand Up @@ -181,7 +181,7 @@ class CalendarViewModel(

val showCycleNumbers: Boolean = false,
val focusedYearMonth: YearMonth = YearMonth.now(),
val periodPredictionDate: LocalDate? = null,
val periodPredictionDates: PersistentSet<LocalDate> = persistentSetOf(),
val ovulationPredictionDate: LocalDate? = null,
val periodDates: PersistentMap<LocalDate, Int> = persistentMapOf(),
val symptomDates: PersistentMap<LocalDate, Set<Symptom>> = persistentMapOf(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,21 @@ class StatisticsViewModel(
_viewState.update {
it.copy(
trackedPeriods = periodDatabaseHelper.getPeriodCount().toString(),
averageCycleLength = formatDays(calcHelper.averageCycleLength().formatToOneDecimalPoint()),
averageCycleLength = formatDays(calcHelper.averageCycleLength()?.formatToOneDecimalPoint()),
averagePeriodLength = formatDays(calcHelper.averagePeriodLength().formatToOneDecimalPoint()),
averageLutealLength =formatDays(calcHelper.averageLutealLength().formatToOneDecimalPoint()),
follicleGrowthDays = calcHelper.averageFollicalGrowthInDays().formatToOneDecimalPoint(),
ovulationPredictionDate = ovulationPrediction.getPredictedOvulationDate()?.format(dateFormatter) ?: dash,
periodPredictionDate = periodPrediction.getPredictedPeriodDate()?.format(dateFormatter) ?: dash,
periodPredictionDate = periodPrediction.getPredictedPeriodDates().firstOrNull()?.format(dateFormatter) ?: dash,
ovulationCount = periodDatabaseHelper.getOvulationCount().toString()
)
}
}

private fun formatDays(text: String): String {
private fun formatDays(text: String?): String? {
if (text == null) {
return null;
}
val days = appContext.getString(R.string.days)
return "$text $days"
}
Expand Down
4 changes: 2 additions & 2 deletions app/src/test/java/com/mensinator/app/PeriodPredictionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ class PeriodPredictionTest {

periodPrediction = PeriodPrediction(dbHelper, calcHelper)

assertEquals(null, periodPrediction.getPredictedPeriodDate())
assertEquals(null, periodPrediction.getPredictedPeriodDates().firstOrNull())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import io.mockk.impl.annotations.RelaxedMockK
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
import kotlinx.collections.immutable.persistentSetOf
import org.junit.After
import org.junit.Before
import org.junit.Test
Expand Down Expand Up @@ -76,14 +77,15 @@ class NotificationSchedulerTest {
val reminderDays = 2
val today = LocalDate.now()
val predictedDate = today.plusDays(5)
val predictedDates = persistentSetOf(predictedDate)
val expectedNotificationDate = predictedDate.minusDays(reminderDays.toLong())
val customMessage = "Custom period message"
val mappedMessage = "Your period is coming in 2 days"

coEvery { dbHelper.getSettingByKey(IntSetting.REMINDER_DAYS.settingDbKey) } returns mockk<Setting> {
every { value } returns reminderDays.toString()
}
every { periodPrediction.getPredictedPeriodDate() } returns predictedDate
every { periodPrediction.getPredictedPeriodDates() } returns predictedDates
coEvery { dbHelper.getStringSettingByKey(StringSetting.PERIOD_NOTIFICATION_MESSAGE.settingDbKey) } returns customMessage
every {
ResourceMapper.getPeriodReminderMessage(customMessage, any())
Expand All @@ -104,12 +106,12 @@ class NotificationSchedulerTest {
fun `do not schedule notification when reminder days is zero`() = runTest(testDispatcher) {
// Given
val reminderDays = 0
val predictedDate = LocalDate.now().plusDays(5)
val predictedDates = persistentSetOf(LocalDate.now().plusDays(5))

coEvery { dbHelper.getSettingByKey(IntSetting.REMINDER_DAYS.settingDbKey) } returns mockk<Setting> {
every { value } returns reminderDays.toString()
}
every { periodPrediction.getPredictedPeriodDate() } returns predictedDate
every { periodPrediction.getPredictedPeriodDates() } returns predictedDates
coEvery { dbHelper.getStringSettingByKey(StringSetting.PERIOD_NOTIFICATION_MESSAGE.settingDbKey) } returns "message"
every { ResourceMapper.getPeriodReminderMessage(any(), any()) } returns "mapped message"

Expand All @@ -126,11 +128,12 @@ class NotificationSchedulerTest {
val reminderDays = 3
val today = LocalDate.now()
val predictedDate = today.plusDays(2) // This makes the reminder date in the past
val predictedDates = persistentSetOf(predictedDate)

coEvery { dbHelper.getSettingByKey(IntSetting.REMINDER_DAYS.settingDbKey) } returns mockk<Setting> {
every { value } returns reminderDays.toString()
}
every { periodPrediction.getPredictedPeriodDate() } returns predictedDate
every { periodPrediction.getPredictedPeriodDates() } returns predictedDates
coEvery { dbHelper.getStringSettingByKey(StringSetting.PERIOD_NOTIFICATION_MESSAGE.settingDbKey) } returns "message"
every {
ResourceMapper.getPeriodReminderMessage(any(), any())
Expand All @@ -151,7 +154,7 @@ class NotificationSchedulerTest {
coEvery { dbHelper.getSettingByKey(IntSetting.REMINDER_DAYS.settingDbKey) } returns mockk<Setting> {
every { value } returns reminderDays.toString()
}
every { periodPrediction.getPredictedPeriodDate() } returns null
every { periodPrediction.getPredictedPeriodDates() } returns persistentSetOf()
coEvery { dbHelper.getStringSettingByKey(StringSetting.PERIOD_NOTIFICATION_MESSAGE.settingDbKey) } returns "message"
every { ResourceMapper.getPeriodReminderMessage(any(), any()) } returns "mapped message"

Expand All @@ -168,11 +171,12 @@ class NotificationSchedulerTest {
val defaultReminderDays = 2
val today = LocalDate.now()
val predictedDate = today.plusDays(5)
val predictedDates = persistentSetOf(predictedDate)
val expectedNotificationDate = predictedDate.minusDays(defaultReminderDays.toLong())
val message = "message"

coEvery { dbHelper.getSettingByKey(IntSetting.REMINDER_DAYS.settingDbKey) } returns null
every { periodPrediction.getPredictedPeriodDate() } returns predictedDate
every { periodPrediction.getPredictedPeriodDates() } returns predictedDates
coEvery { dbHelper.getStringSettingByKey(StringSetting.PERIOD_NOTIFICATION_MESSAGE.settingDbKey) } returns message
every { ResourceMapper.getPeriodReminderMessage(any(), any()) } returns message

Expand All @@ -191,13 +195,14 @@ class NotificationSchedulerTest {
val defaultReminderDays = 2
val today = LocalDate.now()
val predictedDate = today.plusDays(5)
val predictedDates = persistentSetOf(predictedDate)
val expectedNotificationDate = predictedDate.minusDays(defaultReminderDays.toLong())
val message = "message"

coEvery { dbHelper.getSettingByKey(IntSetting.REMINDER_DAYS.settingDbKey) } returns mockk<Setting> {
every { value } returns "not a number"
}
every { periodPrediction.getPredictedPeriodDate() } returns predictedDate
every { periodPrediction.getPredictedPeriodDates() } returns predictedDates
coEvery { dbHelper.getStringSettingByKey(StringSetting.PERIOD_NOTIFICATION_MESSAGE.settingDbKey) } returns message
every { ResourceMapper.getPeriodReminderMessage(any(), any()) } returns message

Expand Down