File: next.go

package info (click to toggle)
golang-github-adhocore-gronx 1.19.6-2
  • links: PTS, VCS
  • area: main
  • in suites: experimental, forky, sid
  • size: 248 kB
  • sloc: makefile: 6
file content (189 lines) | stat: -rw-r--r-- 5,221 bytes parent folder | download
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
}