forked from adhocore/gronx
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnext.go
135 lines (120 loc) · 3.28 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
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, inclRefTime, 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--
for pos, seg := range segments {
if seg == "*" || seg == "?" {
continue
}
if next, bumped, err = bumpUntilDue(gron.C, seg, pos, next, reverse); bumped {
goto over
}
}
if !incl && next.Format(FullDateFormat) == start.Format(FullDateFormat) {
delta := time.Second
if reverse {
delta = -time.Second
}
next, _, err = bumpUntilDue(gron.C, segments[0], 0, next.Add(delta), reverse)
continue
}
return
}
return start, errors.New("tried so hard")
}
var dashRe = regexp.MustCompile(`/.*$`)
func isUnreachableYear(year string, ref time.Time, incl bool, reverse bool) bool {
if year == "*" || year == "?" {
return false
}
edge, inc := ref.Year(), 1
if !incl {
if reverse {
inc = -1
}
edge += inc
}
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
}
ref = bump(ref, pos, reverse)
iter--
}
return ref, false, errors.New("tried so hard")
}
func bump(ref time.Time, pos int, reverse bool) time.Time {
factor := 1
if reverse {
factor = -1
}
switch pos {
case 0:
ref = ref.Add(time.Duration(factor) * time.Second)
case 1:
ref = ref.Add(time.Duration(factor) * time.Minute)
case 2:
ref = ref.Add(time.Duration(factor) * time.Hour)
case 3, 5:
ref = ref.AddDate(0, 0, factor)
case 4:
ref = ref.AddDate(0, factor, 0)
case 6:
ref = ref.AddDate(factor, 0, 0)
}
return ref
}