summaryrefslogtreecommitdiff
path: root/internal/models/recurrence_test.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-14 20:17:10 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-16 02:44:02 +0000
commitc99d52ccf5c52e9c75b6fcb661c8e74816b34924 (patch)
treec14a8f89cc7c7d76147d12a998479432cde43782 /internal/models/recurrence_test.go
parent0baf1134ea2d415aa55079ff98476afd0acf5811 (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_test.go')
-rw-r--r--internal/models/recurrence_test.go51
1 files changed, 51 insertions, 0 deletions
diff --git a/internal/models/recurrence_test.go b/internal/models/recurrence_test.go
new file mode 100644
index 0000000..8d18996
--- /dev/null
+++ b/internal/models/recurrence_test.go
@@ -0,0 +1,51 @@
+package models
+
+import (
+ "testing"
+ "time"
+)
+
+func TestComputeNextOccurrence(t *testing.T) {
+ mustParse := func(t *testing.T, s string) time.Time {
+ t.Helper()
+ d, err := time.Parse("2006-01-02", s)
+ if err != nil {
+ t.Fatalf("bad fixture date %q: %v", s, err)
+ }
+ return d
+ }
+
+ tests := []struct {
+ name string
+ due string
+ freq string
+ interval int
+ weekdays []int
+ want string
+ }{
+ {"daily interval 1", "2026-07-13", "daily", 1, nil, "2026-07-14"},
+ {"daily interval 3", "2026-07-13", "daily", 3, nil, "2026-07-16"},
+ {"weekly no weekdays interval 1", "2026-07-13", "weekly", 1, nil, "2026-07-20"},
+ {"weekly no weekdays interval 2", "2026-07-13", "weekly", 2, nil, "2026-07-27"},
+ // 2026-07-13 is a Monday (weekday=1).
+ {"weekly with weekdays same week", "2026-07-13", "weekly", 1, []int{1, 3, 5}, "2026-07-15"},
+ {"weekly with weekdays wraps to next week", "2026-07-17", "weekly", 1, []int{1, 3, 5}, "2026-07-20"}, // due=Fri(5), wraps to Mon
+ {"weekly with weekdays interval 2 wraps", "2026-07-13", "weekly", 2, []int{1}, "2026-07-27"}, // due=Mon, only Mon active, skip a week
+ {"monthly interval 1", "2026-06-13", "monthly", 1, nil, "2026-07-13"},
+ {"monthly rollover", "2026-01-31", "monthly", 1, nil, "2026-03-03"},
+ {"monthly rollover locks in on the drifted day", "2026-03-03", "monthly", 1, nil, "2026-04-03"},
+ {"yearly interval 1", "2026-07-13", "yearly", 1, nil, "2027-07-13"},
+ {"unknown freq returns due unchanged", "2026-07-13", "bogus", 1, nil, "2026-07-13"},
+ {"interval below 1 is treated as 1", "2026-07-13", "daily", 0, nil, "2026-07-14"},
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ got := ComputeNextOccurrence(mustParse(t, tc.due), tc.freq, tc.interval, tc.weekdays)
+ want := mustParse(t, tc.want)
+ if !got.Equal(want) {
+ t.Errorf("ComputeNextOccurrence(%s, %s, %d, %v) = %s, want %s", tc.due, tc.freq, tc.interval, tc.weekdays, got.Format("2006-01-02"), tc.want)
+ }
+ })
+ }
+}