-
-
Notifications
You must be signed in to change notification settings - Fork 25
/
next.go
189 lines (165 loc) · 5.1 KB
/
next.go
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
178
179
180
181
182
183
184
185
186
187
188
189
package gronx
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"
)
// CronDateFormat is Y-m-d H:i (seconds are not significant)
const CronDateFormat = "2006-01-02 15:04"
// FullDateFormat is Y-m-d H:i:s (with seconds)
const FullDateFormat = "2006-01-02 15:04:05"
// NextTick gives next run time from now
func NextTick(expr string, inclRefTime bool) (time.Time, error) {
return NextTickAfter(expr, time.Now(), inclRefTime)
}
// NextTickAfter gives next run time from the provided time.Time
func NextTickAfter(expr string, start time.Time, inclRefTime bool) (time.Time, error) {
gron, next := New(), start.Truncate(time.Second)
due, err := gron.IsDue(expr, start)
if err != nil || (due && inclRefTime) {
return start, err
}
segments, _ := Segments(expr)
if len(segments) > 6 && isUnreachableYear(segments[6], next, false) {
return next, fmt.Errorf("unreachable year segment: %s", segments[6])
}
next, err = loop(gron, segments, next, inclRefTime, false)
// Ignore superfluous err
if err != nil && gron.isDue(expr, next) {
err = nil
}
return next, err
}
func loop(gron *Gronx, segments []string, start time.Time, incl bool, reverse bool) (next time.Time, err error) {
iter, next, bumped := 500, start, false
over:
for iter > 0 {
iter--
skipMonthDayForIter := false
for i := 0; i < len(segments); i++ {
pos := len(segments) - 1 - i
seg := segments[pos]
isMonthDay, isWeekday := pos == 3, pos == 5
if seg == "*" || seg == "?" {
continue
}
if !isWeekday {
if isMonthDay && skipMonthDayForIter {
continue
}
if next, bumped, err = bumpUntilDue(gron.C, seg, pos, next, reverse); bumped {
goto over
}
continue
}
// From here we process the weekday segment in case it is neither * nor ?
monthDaySeg := segments[3]
intersect := strings.Index(seg, "*/") == 0 || strings.Index(monthDaySeg, "*") == 0 || monthDaySeg == "?"
nextForWeekDay := next
nextForWeekDay, bumped, err = bumpUntilDue(gron.C, seg, pos, nextForWeekDay, reverse)
if !bumped {
// Weekday seg is specific and next is already at right weekday, so no need to process month day if union case
next = nextForWeekDay
if !intersect {
skipMonthDayForIter = true
}
continue
}
// Weekday was bumped, so we need to check for month day
if intersect {
// We need intersection so we keep bumped weekday and go over
next = nextForWeekDay
goto over
}
// Month day seg is specific and a number/list/range, so we need to check and keep the closest to next
nextForMonthDay := next
nextForMonthDay, bumped, err = bumpUntilDue(gron.C, monthDaySeg, 3, nextForMonthDay, reverse)
monthDayIsClosestToNextThanWeekDay := reverse && nextForMonthDay.After(nextForWeekDay) ||
!reverse && nextForMonthDay.Before(nextForWeekDay)
if monthDayIsClosestToNextThanWeekDay {
next = nextForMonthDay
if !bumped {
// Month day seg is specific and next is already at right month day, we can continue
skipMonthDayForIter = true
continue
}
} else {
next = nextForWeekDay
}
goto over
}
if !incl && next.Format(FullDateFormat) == start.Format(FullDateFormat) {
delta := time.Second
if reverse {
delta = -time.Second
}
next = next.Add(delta)
continue
}
return
}
return start, errors.New("tried so hard")
}
var dashRe = regexp.MustCompile(`/.*$`)
func isUnreachableYear(year string, ref time.Time, reverse bool) bool {
if year == "*" || year == "?" {
return false
}
edge := ref.Year()
for _, offset := range strings.Split(year, ",") {
if strings.Index(offset, "*/") == 0 || strings.Index(offset, "0/") == 0 {
return false
}
for _, part := range strings.Split(dashRe.ReplaceAllString(offset, ""), "-") {
val, err := strconv.Atoi(part)
if err != nil || (!reverse && val >= edge) || (reverse && val <= edge) {
return false
}
}
}
return true
}
var limit = map[int]int{0: 60, 1: 60, 2: 24, 3: 31, 4: 12, 5: 366, 6: 100}
func bumpUntilDue(c Checker, segment string, pos int, ref time.Time, reverse bool) (time.Time, bool, error) {
// <second> <minute> <hour> <day> <month> <weekday> <year>
iter := limit[pos]
for iter > 0 {
c.SetRef(ref)
if ok, _ := c.CheckDue(segment, pos); ok {
return ref, iter != limit[pos], nil
}
if reverse {
ref = bumpReverse(ref, pos)
} else {
ref = bump(ref, pos)
}
iter--
}
return ref, false, errors.New("tried so hard")
}
func bump(ref time.Time, pos int) time.Time {
loc := ref.Location()
switch pos {
case 0:
ref = ref.Add(time.Second)
case 1:
minTime := ref.Add(time.Minute)
ref = time.Date(minTime.Year(), minTime.Month(), minTime.Day(), minTime.Hour(), minTime.Minute(), 0, 0, loc)
case 2:
hTime := ref.Add(time.Hour)
ref = time.Date(hTime.Year(), hTime.Month(), hTime.Day(), hTime.Hour(), 0, 0, 0, loc)
case 3, 5:
dTime := ref.AddDate(0, 0, 1)
ref = time.Date(dTime.Year(), dTime.Month(), dTime.Day(), 0, 0, 0, 0, loc)
case 4:
ref = time.Date(ref.Year(), ref.Month(), 1, 0, 0, 0, 0, loc)
ref = ref.AddDate(0, 1, 0)
case 6:
yTime := ref.AddDate(1, 0, 0)
ref = time.Date(yTime.Year(), 1, 1, 0, 0, 0, 0, loc)
}
return ref
}