diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-07-14 20:17:10 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-07-16 02:44:02 +0000 |
| commit | c99d52ccf5c52e9c75b6fcb661c8e74816b34924 (patch) | |
| tree | c14a8f89cc7c7d76147d12a998479432cde43782 /internal/models/recurrence.go | |
| parent | 0baf1134ea2d415aa55079ff98476afd0acf5811 (diff) | |
feat(tasks): add ComputeNextOccurrence for recurring task scheduling
Implement pure function to compute next occurrence of recurring tasks given
frequency, interval, and optional weekday constraints. Supports daily, weekly,
monthly, and yearly recurrence patterns. Weekly recurrence with weekday
constraints wraps to next applicable week when needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
Diffstat (limited to 'internal/models/recurrence.go')
| -rw-r--r-- | internal/models/recurrence.go | 57 |
1 files changed, 57 insertions, 0 deletions
diff --git a/internal/models/recurrence.go b/internal/models/recurrence.go new file mode 100644 index 0000000..f42183c --- /dev/null +++ b/internal/models/recurrence.go @@ -0,0 +1,57 @@ +package models + +import ( + "sort" + "time" +) + +// ComputeNextOccurrence returns the next occurrence date after due, given a +// recurrence pattern. weekdays is only consulted when freq == "weekly"; nil +// or empty means "same weekday as due, every interval weeks." Unknown freq +// values return due unchanged. interval < 1 is treated as 1. +// +// Monthly/yearly rollover uses Go's standard AddDate overflow behavior (a +// due date of Jan 31 + 1 month becomes Mar 3, not clamped to Feb's last +// day) -- this is an accepted simplification, and the drift is permanent: +// each call computes from the previous call's actual result, not an +// original anchor day, so a drifted date locks onto its new day-of-month +// going forward. Only anchor days 29-31 are ever affected; every month has +// at least 28 days, so any anchor day <= 28 never drifts. +func ComputeNextOccurrence(due time.Time, freq string, interval int, weekdays []int) time.Time { + if interval < 1 { + interval = 1 + } + switch freq { + case "daily": + return due.AddDate(0, 0, interval) + case "weekly": + return nextWeeklyOccurrence(due, interval, weekdays) + case "monthly": + return due.AddDate(0, interval, 0) + case "yearly": + return due.AddDate(interval, 0, 0) + default: + return due + } +} + +func nextWeeklyOccurrence(due time.Time, interval int, weekdays []int) time.Time { + if len(weekdays) == 0 { + return due.AddDate(0, 0, 7*interval) + } + sorted := append([]int(nil), weekdays...) + sort.Ints(sorted) + dueWeekday := int(due.Weekday()) + + for _, wd := range sorted { + if wd > dueWeekday { + return due.AddDate(0, 0, wd-dueWeekday) + } + } + // Wrapped past the last active weekday this week: land on the first + // active weekday, (interval-1) whole weeks further out than the + // immediate next week (interval=1 means "next week", interval=2 means + // "skip a week", etc). + daysToNextWeekStart := 7 - dueWeekday + return due.AddDate(0, 0, daysToNextWeekStart+sorted[0]+7*(interval-1)) +} |
