Project period predictions across calendar ranges
All checks were successful
Build Org Backend / build (push) Successful in 15s
All checks were successful
Build Org Backend / build (push) Successful in 15s
This commit is contained in:
@@ -93,7 +93,13 @@ public class PeriodCompanionService {
|
||||
validateRange(from, to);
|
||||
List<PeriodCycle> cycles = cycleRepository.findAllByOrderByStartDateAsc();
|
||||
PeriodSettings settings = getOrCreateSettings();
|
||||
PredictionDTO prediction = latestPrediction(cycles, LocalDate.now(), settings);
|
||||
List<PredictionEstimate> projections = predictionService.project(
|
||||
cycles,
|
||||
LocalDate.now(),
|
||||
from,
|
||||
to,
|
||||
settings.getAverageCycleLength(),
|
||||
settings.getAveragePeriodLength());
|
||||
Map<LocalDate, PeriodDailyLog> logs = dailyLogRepository.findByDateBetweenOrderByDateAsc(from, to)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(PeriodDailyLog::getDate, Function.identity()));
|
||||
@@ -106,9 +112,9 @@ public class PeriodCompanionService {
|
||||
days.add(new CalendarDayDTO(
|
||||
date,
|
||||
periodDay,
|
||||
isPredictedPeriodDay(prediction, date),
|
||||
!date.isBefore(prediction.fertileWindowStart()) && !date.isAfter(prediction.fertileWindowEnd()),
|
||||
date.equals(prediction.predictedOvulationDate()),
|
||||
projections.stream().anyMatch(projection -> isPredictedPeriodDay(projection, currentDate)),
|
||||
projections.stream().anyMatch(projection -> isFertileWindowDay(projection, currentDate)),
|
||||
projections.stream().anyMatch(projection -> currentDate.equals(projection.predictedOvulationDate())),
|
||||
log == null ? List.of() : log.getSymptoms().stream().map(PeriodSymptom::getName).sorted().toList(),
|
||||
log == null ? null : toDailyLogDto(log)));
|
||||
}
|
||||
@@ -291,11 +297,15 @@ public class PeriodCompanionService {
|
||||
return !date.isBefore(cycle.getStartDate()) && date.isBefore(cycle.getStartDate().plusDays(periodLength));
|
||||
}
|
||||
|
||||
private boolean isPredictedPeriodDay(PredictionDTO prediction, LocalDate date) {
|
||||
private boolean isPredictedPeriodDay(PredictionEstimate prediction, LocalDate date) {
|
||||
return !date.isBefore(prediction.predictedPeriodDate())
|
||||
&& date.isBefore(prediction.predictedPeriodDate().plusDays(prediction.averagePeriodLength()));
|
||||
}
|
||||
|
||||
private boolean isFertileWindowDay(PredictionEstimate prediction, LocalDate date) {
|
||||
return !date.isBefore(prediction.fertileWindowStart()) && !date.isAfter(prediction.fertileWindowEnd());
|
||||
}
|
||||
|
||||
private List<String> contextualSupportMessages(PredictionDTO prediction, LocalDate today) {
|
||||
long daysUntilPeriod = ChronoUnit.DAYS.between(today, prediction.predictedPeriodDate());
|
||||
long daysUntilOvulation = ChronoUnit.DAYS.between(today, prediction.predictedOvulationDate());
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.zaine.app.model.PeriodPredictionSnapshot;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
@@ -45,6 +46,36 @@ public class PeriodPredictionService {
|
||||
averagePeriodLength);
|
||||
}
|
||||
|
||||
public List<PredictionEstimate> project(
|
||||
List<PeriodCycle> cycles,
|
||||
LocalDate today,
|
||||
LocalDate from,
|
||||
LocalDate to,
|
||||
int fallbackCycleLength,
|
||||
int fallbackPeriodLength) {
|
||||
PredictionEstimate first = estimate(cycles, today, fallbackCycleLength, fallbackPeriodLength);
|
||||
List<PredictionEstimate> projections = new ArrayList<>();
|
||||
LocalDate predictedPeriodDate = first.predictedPeriodDate();
|
||||
|
||||
while (!predictedPeriodDate.minusDays(19).isAfter(to)) {
|
||||
LocalDate predictedOvulationDate = predictedPeriodDate.minusDays(14);
|
||||
PredictionEstimate projection = new PredictionEstimate(
|
||||
predictedPeriodDate,
|
||||
predictedOvulationDate,
|
||||
predictedOvulationDate.minusDays(5),
|
||||
predictedOvulationDate.plusDays(1),
|
||||
first.averageCycleLength(),
|
||||
first.averagePeriodLength());
|
||||
|
||||
if (intersectsRange(projection, from, to)) {
|
||||
projections.add(projection);
|
||||
}
|
||||
predictedPeriodDate = predictedPeriodDate.plusDays(first.averageCycleLength());
|
||||
}
|
||||
|
||||
return projections;
|
||||
}
|
||||
|
||||
public CurrentCycleDTO currentCycle(List<PeriodCycle> cycles, LocalDate today) {
|
||||
return currentCycle(cycles, today, DEFAULT_CYCLE_LENGTH, DEFAULT_PERIOD_LENGTH);
|
||||
}
|
||||
@@ -125,6 +156,16 @@ public class PeriodPredictionService {
|
||||
return (int) Math.round(average);
|
||||
}
|
||||
|
||||
private boolean intersectsRange(PredictionEstimate projection, LocalDate from, LocalDate to) {
|
||||
LocalDate periodEnd = projection.predictedPeriodDate().plusDays(projection.averagePeriodLength() - 1L);
|
||||
return rangesOverlap(projection.predictedPeriodDate(), periodEnd, from, to)
|
||||
|| rangesOverlap(projection.fertileWindowStart(), projection.fertileWindowEnd(), from, to);
|
||||
}
|
||||
|
||||
private boolean rangesOverlap(LocalDate firstStart, LocalDate firstEnd, LocalDate secondStart, LocalDate secondEnd) {
|
||||
return !firstStart.isAfter(secondEnd) && !secondStart.isAfter(firstEnd);
|
||||
}
|
||||
|
||||
public record PredictionEstimate(
|
||||
LocalDate predictedPeriodDate,
|
||||
LocalDate predictedOvulationDate,
|
||||
|
||||
@@ -52,6 +52,25 @@ public class PeriodPredictionServiceTest {
|
||||
assertEquals(LocalDate.of(2026, 6, 15), estimate.predictedOvulationDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void projectReturnsRepeatedPredictionsAcrossLongDateRange() {
|
||||
LocalDate today = LocalDate.of(2026, 6, 22);
|
||||
|
||||
List<PredictionEstimate> projections = service.project(
|
||||
List.of(),
|
||||
today,
|
||||
LocalDate.of(2026, 7, 1),
|
||||
LocalDate.of(2026, 9, 30),
|
||||
28,
|
||||
5);
|
||||
|
||||
assertEquals(4, projections.size());
|
||||
assertEquals(LocalDate.of(2026, 7, 20), projections.get(0).predictedPeriodDate());
|
||||
assertEquals(LocalDate.of(2026, 8, 17), projections.get(1).predictedPeriodDate());
|
||||
assertEquals(LocalDate.of(2026, 9, 14), projections.get(2).predictedPeriodDate());
|
||||
assertEquals(LocalDate.of(2026, 10, 12), projections.get(3).predictedPeriodDate());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void currentCycleReturnsMenstrualPhaseDuringPeriodLength() {
|
||||
PeriodCycle cycle = cycle(LocalDate.of(2026, 6, 20), LocalDate.of(2026, 6, 24), 28, 5);
|
||||
|
||||
Reference in New Issue
Block a user