- Add Rooms, Tasks, TaskCompletions Drift tables with schema v2 migration - Create RoomsDao with CRUD, watchAll, watchWithStats, cascade delete, reorder - Create TasksDao with CRUD, watchInRoom (sorted by due), completeTask, overdue detection - Implement calculateNextDueDate and catchUpToPresent pure scheduling functions - Define IntervalType enum (8 types), EffortLevel enum, FrequencyInterval model - Add formatRelativeDate German formatter and curatedRoomIcons icon list - Enable PRAGMA foreign_keys in beforeOpen migration strategy - All 30 unit tests passing (17 scheduling + 6 rooms DAO + 7 tasks DAO) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
67 lines
2.4 KiB
Dart
67 lines
2.4 KiB
Dart
import 'frequency.dart';
|
|
|
|
/// Calculate the next due date from the current due date and interval config.
|
|
///
|
|
/// For calendar-anchored intervals, [anchorDay] is the original day-of-month.
|
|
/// Day-count intervals use pure arithmetic (Duration).
|
|
/// Calendar-anchored intervals use month arithmetic with clamping.
|
|
DateTime calculateNextDueDate({
|
|
required DateTime currentDueDate,
|
|
required IntervalType intervalType,
|
|
required int intervalDays,
|
|
int? anchorDay,
|
|
}) {
|
|
switch (intervalType) {
|
|
// Day-count: pure arithmetic
|
|
case IntervalType.daily:
|
|
return currentDueDate.add(const Duration(days: 1));
|
|
case IntervalType.everyNDays:
|
|
return currentDueDate.add(Duration(days: intervalDays));
|
|
case IntervalType.weekly:
|
|
return currentDueDate.add(const Duration(days: 7));
|
|
case IntervalType.biweekly:
|
|
return currentDueDate.add(const Duration(days: 14));
|
|
// Calendar-anchored: month arithmetic with clamping
|
|
case IntervalType.monthly:
|
|
return _addMonths(currentDueDate, 1, anchorDay);
|
|
case IntervalType.everyNMonths:
|
|
return _addMonths(currentDueDate, intervalDays, anchorDay);
|
|
case IntervalType.quarterly:
|
|
return _addMonths(currentDueDate, 3, anchorDay);
|
|
case IntervalType.yearly:
|
|
return _addMonths(currentDueDate, 12, anchorDay);
|
|
}
|
|
}
|
|
|
|
/// Add months with day-of-month clamping.
|
|
/// [anchorDay] remembers the original day for correct clamping.
|
|
DateTime _addMonths(DateTime date, int months, int? anchorDay) {
|
|
final targetMonth = date.month + months;
|
|
final targetYear = date.year + (targetMonth - 1) ~/ 12;
|
|
final normalizedMonth = ((targetMonth - 1) % 12) + 1;
|
|
final day = anchorDay ?? date.day;
|
|
// Last day of target month: day 0 of next month
|
|
final lastDay = DateTime(targetYear, normalizedMonth + 1, 0).day;
|
|
final clampedDay = day > lastDay ? lastDay : day;
|
|
return DateTime(targetYear, normalizedMonth, clampedDay);
|
|
}
|
|
|
|
/// Catch-up: if next due is in the past, keep adding intervals until future/today.
|
|
DateTime catchUpToPresent({
|
|
required DateTime nextDue,
|
|
required DateTime today,
|
|
required IntervalType intervalType,
|
|
required int intervalDays,
|
|
int? anchorDay,
|
|
}) {
|
|
while (nextDue.isBefore(today)) {
|
|
nextDue = calculateNextDueDate(
|
|
currentDueDate: nextDue,
|
|
intervalType: intervalType,
|
|
intervalDays: intervalDays,
|
|
anchorDay: anchorDay,
|
|
);
|
|
}
|
|
return nextDue;
|
|
}
|