Repository navigation
Expand file tree
/
Copy pathspec.go
More file actions
177 lines (158 loc) · 4.92 KB
/
Copy pathspec.go
File metadata and controls
177 lines (158 loc) · 4.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package cron
import (
"iter"
"log/slog"
"time"
"github.com/libtnb/cron/internal/bitmask"
)
// This file evaluates a compiled cron expression. Next walks the calendar
// from the largest unit down: it skips to a matching month, then day, hour,
// minute and second, restarting the walk whenever a jump carries into a larger
// unit, and gives up after nextYearLimit years so an impossible date such as
// "0 0 30 2 *" terminates. Day matching implements the classic coupling rule:
// when either day-of-month or day-of-week was a literal "*", both must match;
// otherwise either may.
// nextYearLimit caps SpecSchedule.Next search; matches robfig/cron.
const nextYearLimit = 5
// SpecSchedule is a compiled cron expression as produced by StandardParser.
// It is immutable and safe for concurrent use; one instance may back several
// entries. The zero value never fires.
type SpecSchedule struct {
second uint64
minute uint64
hour uint64
dom uint64
month uint64
dow uint64
loc *time.Location
}
// Location returns the time zone the expression is evaluated in: the TZ=
// prefix if present, otherwise the parser's default. AnalyzeSpec reports it.
func (s *SpecSchedule) Location() *time.Location {
return s.loc
}
// Next returns the first firing strictly after t, evaluated in the schedule's
// Location and returned in t's location, or zero when nothing matches within
// the next five years.
//
// The hour branch reconstructs the wall clock at the target hour instead of
// adding an absolute (h-hour)*time.Hour, which would overshoot a DST
// spring-forward gap and skip the day. The minute/second branches keep their
// absolute jumps: they never straddle a whole-hour DST boundary, and stepping
// through the repeated hour on fall-back keeps both firings observable.
func (s *SpecSchedule) Next(t time.Time) time.Time {
origLoc := t.Location()
loc := s.loc
if loc == nil { // zero-value SpecSchedule
loc = time.Local
}
if loc != origLoc {
t = t.In(loc)
}
t = t.Add(time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
yearLimit := t.Year() + nextYearLimit
for {
year, month, day := t.Date()
if year > yearLimit {
return time.Time{}
}
if 1<<uint(month)&s.month == 0 {
nextYear := year
m := bitmask.NextInRange(s.month, uint(month)+1, 12)
if m < 0 {
m = bitmask.NextInRange(s.month, 1, 12)
if m < 0 {
return time.Time{} // zero-value SpecSchedule: empty month set
}
nextYear++
}
t = time.Date(nextYear, time.Month(m), 1, 0, 0, 0, 0, loc)
continue
}
if !dayMatches(s, t, day) {
t = time.Date(year, month, day+1, 0, 0, 0, 0, loc)
continue
}
hour, minute, sec := t.Clock()
if 1<<uint(hour)&s.hour == 0 {
h := bitmask.NextInRange(s.hour, uint(hour)+1, 23)
if h < 0 {
t = time.Date(year, month, day+1, 0, 0, 0, 0, loc)
continue
}
// time.Date resolves a nonexistent spring-forward hour to the pre-gap
// wall time (02:00 -> 01:00), which wouldn't advance; step one wall
// hour in that case and let the loop re-validate.
candidate := time.Date(year, month, day, h, 0, 0, 0, loc)
if candidate.Hour() == h && candidate.After(t) {
t = candidate
continue
}
t = t.Add(time.Hour -
time.Duration(minute)*time.Minute -
time.Duration(sec)*time.Second)
continue
}
if 1<<uint(minute)&s.minute == 0 {
m := bitmask.NextInRange(s.minute, uint(minute)+1, 59)
if m < 0 {
t = t.Add(time.Hour -
time.Duration(minute)*time.Minute -
time.Duration(sec)*time.Second)
continue
}
t = t.Add(time.Duration(m-minute)*time.Minute -
time.Duration(sec)*time.Second)
continue
}
if 1<<uint(sec)&s.second == 0 {
secN := bitmask.NextInRange(s.second, uint(sec)+1, 59)
if secN < 0 {
t = t.Add(time.Minute - time.Duration(sec)*time.Second)
continue
}
t = t.Add(time.Duration(secN-sec) * time.Second)
continue
}
return t.In(origLoc)
}
}
// Upcoming lazily yields firings strictly after from, in order, until the
// schedule exhausts. NextN and Between use it.
func (s *SpecSchedule) Upcoming(from time.Time) iter.Seq[time.Time] {
return func(yield func(time.Time) bool) {
cur := from
for {
next := s.Next(cur)
if next.IsZero() {
return
}
if !yield(next) {
return
}
cur = next
}
}
}
// LogValue renders the schedule's kind and location for slog.
func (s *SpecSchedule) LogValue() slog.Value {
loc := "Local"
if s.loc != nil {
loc = s.loc.String()
}
return slog.GroupValue(
slog.String("kind", "spec"),
slog.String("loc", loc),
)
}
// dayMatches implements the DOM/DOW coupling: AND when either field was "*",
// OR otherwise. day is the already-computed t.Date() day, reused to avoid a
// second civil-time conversion.
func dayMatches(s *SpecSchedule, t time.Time, day int) bool {
domMatch := 1<<uint(day)&s.dom != 0
dowMatch := 1<<uint(t.Weekday())&s.dow != 0
if s.dom&starBit != 0 || s.dow&starBit != 0 {
return domMatch && dowMatch
}
return domMatch || dowMatch
}