summaryrefslogtreecommitdiff
path: root/internal/models/recurrence.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/models/recurrence.go')
-rw-r--r--internal/models/recurrence.go57
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))
+}