From c99d52ccf5c52e9c75b6fcb661c8e74816b34924 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Tue, 14 Jul 2026 20:17:10 +0000 Subject: 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 Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD --- internal/models/recurrence_test.go | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 internal/models/recurrence_test.go (limited to 'internal/models/recurrence_test.go') 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) + } + }) + } +} -- cgit v1.2.3