mirror_cron.go RawHistoryFolder
1// SPDX-License-Identifier: AGPL-3.0-or-later
2package domain
3
4import (
5 "sync"
6 "time"
7)
8
9// cronGapCache memoizes CronMinGap per parsed entry: a scan covers two years of minutes.
10const cronGapCacheMax = 256
11
12var cronGapCache = struct {
13 sync.Mutex
14 m map[Cron]time.Duration
15}{m: make(map[Cron]time.Duration)}
16
17// CronMinGap is the smallest gap between two firings, 0 when the entry fires fewer than twice.
18// It scans a leap year, the following non-leap year and the January after, so that Feb 29,
19// year-end and Feb 28/Mar 1 neighbours are all compared.
20func CronMinGap(c Cron) time.Duration {
21 cronGapCache.Lock()
22 gap, ok := cronGapCache.m[c]
23 cronGapCache.Unlock()
24 if ok {
25 return gap
26 }
27 gap = scanCronMinGap(c)
28 cronGapCache.Lock()
29 if len(cronGapCache.m) >= cronGapCacheMax {
30 clear(cronGapCache.m)
31 }
32 cronGapCache.m[c] = gap
33 cronGapCache.Unlock()
34 return gap
35}
36
37func scanCronMinGap(c Cron) time.Duration {
38 start := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) // leap
39 end := time.Date(2026, 2, 1, 0, 0, 0, 0, time.UTC) // 2025 is not leap; include Jan 2026
40 var prev time.Time
41 var gap time.Duration
42 for t := start; t.Before(end); t = t.Add(time.Minute) {
43 if !c.dayMatches(t) {
44 t = t.Truncate(24 * time.Hour).Add(24*time.Hour - time.Minute)
45 continue
46 }
47 if c.Matches(t) {
48 if !prev.IsZero() {
49 d := t.Sub(prev)
50 if gap == 0 || d < gap {
51 gap = d
52 }
53 }
54 prev = t
55 }
56 }
57 return gap
58}
59
60// dayMatches is the day-level part of Matches (month, day of month, day of week), UTC.
61func (c Cron) dayMatches(t time.Time) bool {
62 t = t.UTC()
63 if c.month&(1<<uint(t.Month())) == 0 {
64 return false
65 }
66 domOK := c.dom&(1<<uint(t.Day())) != 0
67 dowOK := c.dow&(1<<uint(t.Weekday())) != 0
68 if !c.domStar && !c.dowStar {
69 return domOK || dowOK
70 }
71 return domOK && dowOK
72}
73
74// nextCronHorizon bounds NextCron: a year of minutes.
75const nextCronHorizon = 366 * 24 * time.Hour
76
77// NextCron is the first firing after t, within a year. UTC schedules skip the days that cannot match.
78func NextCron(s CronSchedule, t time.Time) (time.Time, bool) {
79 end := t.Add(nextCronHorizon)
80 utc := s.Loc == nil || s.Loc == time.UTC
81 for n := t.Truncate(time.Minute).Add(time.Minute); !n.After(end); n = n.Add(time.Minute) {
82 if utc && !s.Cron.dayMatches(n) {
83 n = n.Truncate(24 * time.Hour).Add(24*time.Hour - time.Minute)
84 continue
85 }
86 if s.Due(n) {
87 return n, true
88 }
89 }
90 return time.Time{}, false
91}