From ca4c9a5e64f155e51653fd251ba8ef0aef941cb9 Mon Sep 17 00:00:00 2001 From: walle250ai Date: Tue, 28 Apr 2026 20:00:38 +0800 Subject: [PATCH] feat: add NextN method to SpecSchedule --- spec.go | 21 +++++++++++ spec_test.go | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/spec.go b/spec.go index fa1e241e..6d3fbb31 100644 --- a/spec.go +++ b/spec.go @@ -174,6 +174,27 @@ WRAP: return t.In(origLocation) } +// NextN returns the next n times this schedule is activated, greater than the given +// time. If no time can be found to satisfy the schedule within 5 years, returns +// the partial results collected so far. The results are returned in ascending order. +// If n <= 0, returns an empty slice. +func (s *SpecSchedule) NextN(t time.Time, n int) []time.Time { + if n <= 0 { + return []time.Time{} + } + result := make([]time.Time, 0, n) + current := t + for i := 0; i < n; i++ { + next := s.Next(current) + if next.IsZero() { + break + } + result = append(result, next) + current = next + } + return result +} + // dayMatches returns true if the schedule's day-of-week and day-of-month // restrictions are satisfied by the given time. func dayMatches(s *SpecSchedule, t time.Time) bool { diff --git a/spec_test.go b/spec_test.go index 1b8a503e..e79e57e0 100644 --- a/spec_test.go +++ b/spec_test.go @@ -298,3 +298,108 @@ func TestSlash0NoHang(t *testing.T) { t.Error("expected an error on 0 increment") } } + +func TestSpecSchedule_NextN(t *testing.T) { + t.Parallel() + + baseTime := getTime("Mon Jul 9 14:45:00 2012") + + t.Run("n=0 returns empty slice", func(t *testing.T) { + t.Parallel() + sched, err := secondParser.Parse("0 0/15 * * * *") + if err != nil { + t.Fatal(err) + } + specSched := sched.(*SpecSchedule) + result := specSched.NextN(baseTime, 0) + if len(result) != 0 { + t.Errorf("expected empty slice, got %d elements", len(result)) + } + }) + + t.Run("n=0 with negative n returns empty slice", func(t *testing.T) { + t.Parallel() + sched, err := secondParser.Parse("0 0/15 * * * *") + if err != nil { + t.Fatal(err) + } + specSched := sched.(*SpecSchedule) + result := specSched.NextN(baseTime, -5) + if len(result) != 0 { + t.Errorf("expected empty slice for n=-5, got %d elements", len(result)) + } + }) + + t.Run("n=1 returns single time", func(t *testing.T) { + t.Parallel() + sched, err := secondParser.Parse("0 0/15 * * * *") + if err != nil { + t.Fatal(err) + } + specSched := sched.(*SpecSchedule) + result := specSched.NextN(baseTime, 1) + if len(result) != 1 { + t.Fatalf("expected 1 element, got %d", len(result)) + } + expected := getTime("Mon Jul 9 15:00 2012") + if !result[0].Equal(expected) { + t.Errorf("expected %v, got %v", expected, result[0]) + } + }) + + t.Run("n=3 with every second returns 3 consecutive times", func(t *testing.T) { + t.Parallel() + sched, err := secondParser.Parse("*/1 * * * * *") + if err != nil { + t.Fatal(err) + } + specSched := sched.(*SpecSchedule) + result := specSched.NextN(baseTime, 3) + if len(result) != 3 { + t.Fatalf("expected 3 elements, got %d", len(result)) + } + expected1 := baseTime.Add(1 * time.Second) + expected2 := baseTime.Add(2 * time.Second) + expected3 := baseTime.Add(3 * time.Second) + if !result[0].Equal(expected1) { + t.Errorf("expected first element %v, got %v", expected1, result[0]) + } + if !result[1].Equal(expected2) { + t.Errorf("expected second element %v, got %v", expected2, result[1]) + } + if !result[2].Equal(expected3) { + t.Errorf("expected third element %v, got %v", expected3, result[2]) + } + }) + + t.Run("unsatisfiable schedule returns fewer results than n", func(t *testing.T) { + t.Parallel() + sched, err := secondParser.Parse("0 0 0 30 Feb ?") + if err != nil { + t.Fatal(err) + } + specSched := sched.(*SpecSchedule) + result := specSched.NextN(baseTime, 5) + if len(result) >= 5 { + t.Errorf("expected fewer than 5 elements for unsatisfiable schedule, got %d", len(result)) + } + }) + + t.Run("results are strictly increasing", func(t *testing.T) { + t.Parallel() + sched, err := secondParser.Parse("0 0/15 * * * *") + if err != nil { + t.Fatal(err) + } + specSched := sched.(*SpecSchedule) + result := specSched.NextN(baseTime, 5) + if len(result) != 5 { + t.Fatalf("expected 5 elements, got %d", len(result)) + } + for i := 1; i < len(result); i++ { + if !result[i].After(result[i-1]) { + t.Errorf("result[%d] = %v is not after result[%d] = %v", i, result[i], i-1, result[i-1]) + } + } + }) +}