Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bump segments iteration order and day of month/week intersection/union handling #35

Merged
merged 9 commits into from
Mar 12, 2024
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,8 @@ Following modifiers supported
- `L` stands for last day of month (eg: `L` could mean 29th for February in leap year)
- `W` stands for closest week day (eg: `10W` is closest week days (MON-FRI) to 10th date)
- *Day of Week / 5th of 5 segments / 6th of 6+ segments:*
- `L` stands for last weekday of month (eg: `2L` is last monday)
- `#` stands for nth day of week in the month (eg: `1#2` is second sunday)
- `L` stands for last weekday of month (eg: `2L` is last tuesday)
Copy link
Owner

@adhocore adhocore Mar 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

@codenem codenem Mar 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes both exists depending what tool you use, e.g. linux crontab uses 0 or 7 for Sunday (man 5 crontab).
Since the gronx code has been using 0 or 7 for Sunday (because it checks with go's time Weekday() method), I fixed the readme accordingly.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alright then (im also thinking for a feature in yet another release to make configurable for whether to use 0 based or 1 based for ordinal modifiers - but it could be overkill given the gronx is intended to be a lean/minimal lib)

- `#` stands for nth day of week in the month (eg: `1#2` is second monday)

---
## License
Expand Down
10 changes: 5 additions & 5 deletions checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,20 @@ func (c *SegmentChecker) SetRef(ref time.Time) {
func (c *SegmentChecker) CheckDue(segment string, pos int) (due bool, err error) {
ref, last := c.GetRef(), -1
val, loc := valueByPos(ref, pos), ref.Location()
isMonth, isWeekDay := pos == 3, pos == 5
isMonthDay, isWeekDay := pos == 3, pos == 5

for _, offset := range strings.Split(segment, ",") {
mod := (isMonth || isWeekDay) && strings.ContainsAny(offset, "LW#")
mod := (isMonthDay || isWeekDay) && strings.ContainsAny(offset, "LW#")
if due, err = c.isOffsetDue(offset, val, pos); due || (!mod && err != nil) {
return
}
if !mod {
continue
}
if last == -1 {
last = time.Date(ref.Year(), ref.Month(), 1, 0, 0, 0, 0, loc).AddDate(0, 1, 0).Add(-time.Nanosecond).Day()
last = time.Date(ref.Year(), ref.Month(), 1, 0, 0, 0, 0, loc).AddDate(0, 1, 0).Add(-time.Second).Day()
}
if isMonth {
if isMonthDay {
due, err = isValidMonthDay(offset, last, ref)
} else if isWeekDay {
due, err = isValidWeekDay(offset, last, ref)
Expand Down Expand Up @@ -126,7 +126,7 @@ func boundsByPos(pos int) (bounds []int) {
case 5:
bounds = []int{0, 7}
case 6:
bounds = []int{1, 9999}
bounds = []int{0, 9999}
}
return
}
35 changes: 34 additions & 1 deletion gronx.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,44 @@ func Segments(expr string) ([]string, error) {
// SegmentsDue checks if all cron parts are due.
// It returns bool. You should use IsDue(expr) instead.
func (g *Gronx) SegmentsDue(segs []string) (bool, error) {
for pos, seg := range segs {
skipMonthDayCheck := false
for i := 0; i < len(segs); i++ {
pos := len(segs) - 1 - i
seg := segs[pos]
isMonthDay, isWeekday := pos == 3, pos == 5

if seg == "*" || seg == "?" {
continue
}

if isMonthDay && skipMonthDayCheck {
continue
}

if isWeekday {
segIsIntersecting := strings.Index(seg, "*/") == 0
monthDaySeg := segs[3]
monthDaySegIsIntersecting := strings.Index(monthDaySeg, "*") == 0 || monthDaySeg == "?"
intersectCase := segIsIntersecting || monthDaySegIsIntersecting

if !intersectCase {
due, err := g.C.CheckDue(seg, pos)
if err != nil {
return false, err
}

monthDayDue, err := g.C.CheckDue(monthDaySeg, 3)
if due || monthDayDue {
skipMonthDayCheck = true
continue
}

if err != nil {
return false, err
}
}
}

if due, err := g.C.CheckDue(seg, pos); !due {
return due, err
}
Expand Down
42 changes: 25 additions & 17 deletions gronx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,25 +148,26 @@ func testcases() []Case {
{"* * * * * 2018", "2022-01-02 15:04:00", false, "err"},
{"* * * * * 2018", "2021-04-19 12:54:00", false, "err"},
{"@5minutes", "2017-05-10 02:30:00", true, "2017-05-10 02:35:00"},
{"* * 7W * *", "2017-10-15 20:00:00", false, "2017-11-07 20:00:00"},
{"*/2 */2 * * *", "2015-08-10 21:47:00", false, "2015-08-10 22:48:00"},
{"* * 7W * *", "2017-10-15 20:00:00", false, "2017-11-07 00:00:00"},
{"*/2 */2 * * *", "2015-08-10 21:47:00", false, "2015-08-10 22:00:00"},
{"* * * * *", "2015-08-10 21:50:00", true, "2015-08-10 21:51:00"},
{"* * * * * ", "2015-08-10 21:50:00", true, "2015-08-10 21:51:00"},
{"* * * * *", "2015-08-10 21:50:00", true, "2015-08-10 21:51:00"},
{"* * * * *", "2015-08-10 21:50:00", true, "2015-08-10 21:51:00"},
{"* * * * *", "2015-08-10 21:50:00", true, "2015-08-10 21:51:00"},
{"* 20,21,22 * * *", "2015-08-10 21:50:00", true, "2015-08-10 21:51:00"},
{"* 20,22 * * *", "2015-08-10 21:50:00", false, "2015-08-10 22:50:00"},
{"* 20,22 * * *", "2015-08-10 21:50:00", false, "2015-08-10 22:00:00"},
{"* 5,21-22 * * *", "2015-08-10 21:50:00", true, "2015-08-10 21:51:00"},
{"7-9 * */9 * *", "2015-08-10 22:02:00", false, "2015-08-18 22:07:00"},
{"1 * * * 7", "2015-08-10 21:47:00", false, "2015-08-16 22:01:00"},
{"7-9 * */9 * *", "2015-08-10 22:02:00", false, "2015-08-10 22:07:00"},
{"7-9 * */9 * *", "2015-08-11 22:02:00", false, "2015-08-19 00:07:00"},
{"1 * * * 7", "2015-08-10 21:47:00", false, "2015-08-16 00:01:00"},
{"47 21 * * *", "2015-08-10 21:47:00", true, "2015-08-11 21:47:00"},
{"00 * * * *", "2023-07-21 12:30:00", false, "2023-07-21 13:00:00"},
{"0 00 * * *", "2023-07-21 12:30:00", false, "2023-07-22 00:00:00"},
{"0 000 * * *", "2023-07-21 12:30:00", false, "2023-07-22 00:00:00"},
{"* * * * 0", "2011-06-15 23:09:00", false, "2011-06-19 23:09:00"},
{"* * * * 7", "2011-06-15 23:09:00", false, "2011-06-19 23:09:00"},
{"* * * * 1", "2011-06-15 23:09:00", false, "2011-06-20 23:09:00"},
{"* * * * 0", "2011-06-15 23:09:00", false, "2011-06-19 00:00:00"},
{"* * * * 7", "2011-06-15 23:09:00", false, "2011-06-19 00:00:00"},
{"* * * * 1", "2011-06-15 23:09:00", false, "2011-06-20 00:00:00"},
{"0 0 * * MON,SUN", "2011-06-15 23:09:00", false, "2011-06-19 00:00:00"},
{"0 0 * * 1,7", "2011-06-15 23:09:00", false, "2011-06-19 00:00:00"},
{"0 0 * * 0-4", "2011-06-15 23:09:00", false, "2011-06-16 00:00:00"},
Expand All @@ -187,7 +188,7 @@ func testcases() []Case {
{"0 0 * * 0,2-6", "2011-06-20 23:09:00", false, "2011-06-21 00:00:00"},
{"0 0 1 1 0", "2011-06-15 23:09:00", false, "2012-01-01 00:00:00"},
{"0 0 1 JAN 0", "2011-06-15 23:09:00", false, "2012-01-01 00:00:00"},
{"0 0 1 * 0", "2011-06-15 23:09:00", false, "2012-01-01 00:00:00"},
{"0 0 1 * 0", "2011-06-15 23:09:00", false, "2011-06-19 00:00:00"},
{"0 0 L * *", "2011-07-15 00:00:00", false, "2011-07-31 00:00:00"},
{"0 0 2W * *", "2011-07-01 00:00:00", true, "2011-08-02 00:00:00"},
{"0 0 1W * *", "2011-05-01 00:00:00", false, "2011-05-02 00:00:00"},
Expand All @@ -197,10 +198,10 @@ func testcases() []Case {
{"0 0 28W * *", "2011-07-01 00:00:00", false, "2011-07-28 00:00:00"},
{"0 0 30W * *", "2011-07-01 00:00:00", false, "2011-07-29 00:00:00"},
// {"0 0 31W * *", "2011-07-01 00:00:00", false, "2011-07-29 00:00:00"},
{"* * * * * 2012", "2011-05-01 00:00:00", false, "2012-05-01 00:00:00"}, // 1
{"* * * * * 2012", "2011-05-01 00:00:00", false, "2012-01-01 00:00:00"},
{"* * * * 5L", "2011-07-01 00:00:00", false, "2011-07-29 00:00:00"},
{"* * * * 6L", "2011-07-01 00:00:00", false, "2011-07-30 00:00:00"},
{"* * * * 7L", "2011-07-01 00:00:00", false, "2011-10-09 00:00:00"},
{"* * * * 7L", "2011-07-01 00:00:00", false, "2011-07-31 00:00:00"},
{"* * * * 1L", "2011-07-24 00:00:00", false, "2011-07-25 00:00:00"},
{"* * * * TUEL", "2011-07-24 00:00:00", false, "2011-07-26 00:00:00"},
{"* * * 1 5L", "2011-12-25 00:00:00", false, "2012-01-27 00:00:00"},
Expand All @@ -213,21 +214,26 @@ func testcases() []Case {
{"5/20 * * * *", "2018-08-13 00:24:00", false, "2018-08-13 00:25:00"},
{"5/20 * * * *", "2018-08-13 00:45:00", true, "2018-08-13 01:05:00"},
{"5-11/4 * * * *", "2018-08-13 00:03:00", false, "2018-08-13 00:05:00"},
{"0 0 L * 0", "2011-06-15 23:09:00", false, "2011-07-31 00:00:00"},
{"3-59/15 6-12 */15 1 2-5", "2017-01-08 00:00:00", false, "2018-01-30 06:03:00"},
{"0 0 L * 0", "2011-06-15 23:09:00", false, "2011-06-19 00:00:00"},
{"3-59/15 6-12 */15 1 2-5", "2017-01-08 00:00:00", false, "2017-01-31 06:03:00"},
{"* * * * MON-FRI", "2017-01-08 00:00:00", false, "2017-01-09 00:00:00"},
{"* * * * TUE", "2017-01-08 00:00:00", false, "2017-01-10 00:00:00"},
{"0 1 15 JUL mon,Wed,FRi", "2019-11-14 00:00:00", false, "2020-07-15 01:00:00"},
{"0 1 15 jul mon,Wed,FRi", "2019-11-14 00:00:00", false, "2020-07-15 01:00:00"},
{"0 1 15 JUL mon,Wed,FRi", "2019-11-14 00:00:00", false, "2020-07-01 01:00:00"},
{"0 1 15 jul mon,Wed,FRi", "2019-11-14 00:00:00", false, "2020-07-01 01:00:00"},
{"1 * 2 7 5-7", "2020-07-02 00:00:00", false, "2020-07-02 00:01:00"},
{"@weekly", "2019-11-14 00:00:00", false, "2019-11-17 00:00:00"},
{"@weekly", "2019-11-14 00:00:00", false, "2019-11-17 00:00:00"},
{"@weekly", "2019-11-14 00:00:00", false, "2019-11-17 00:00:00"},
{"0 12 * * ?", "2020-08-20 00:00:00", false, "2020-08-20 12:00:00"},
{"0 12 ? * *", "2020-08-20 00:00:00", false, "2020-08-20 12:00:00"},
{"* ? * ? * *", "2020-08-20 00:00:00", true, "2020-08-20 00:00:01"},
{"* * ? * * * */2", "2021-08-20 00:00:00", false, "2022-08-20 00:00:00"},
{"* * ? * * * */2", "2021-08-20 00:00:00", false, "2022-01-01 00:00:00"},
{"* * * * * * *", "2021-08-20 00:00:00", true, "2021-08-20 00:00:01"},
{"* * * * * * 2023-2099", "2021-08-20 00:00:00", false, "2023-08-20 00:00:00"},
{"* * * * * * 2023-2099", "2021-08-20 00:00:00", false, "2023-01-01 00:00:00"},
{"30 9 L */3 *", "2023-04-23 09:30:00", false, "2023-04-30 09:30:00"},
{"30 9 L */3 *", "2023-05-01 09:30:00", false, "2023-07-31 09:30:00"},
{"0 * * * * * */2", "2019-05-01 09:30:00", false, "2020-01-01 00:00:00"},
{"0/4 * * * *", "2019-05-01 09:31:00", false, "2019-05-01 09:32:00"},
}
}

Expand All @@ -251,6 +257,8 @@ func errcases() []Case {
{"* * * * * ZL", "", false, ""},
{"* * * * * Z#", "", false, ""},
{"* * * * * 1#Z", "", false, ""},
{"* * W * 3", "", false, ""},
{"* * 15 * 1#Z", "", false, ""},
}
}

Expand Down
99 changes: 91 additions & 8 deletions next.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,77 @@ func loop(gron Gronx, segments []string, start time.Time, incl bool, reverse boo
over:
for iter > 0 {
iter--
for pos, seg := range segments {
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 next, bumped, err = bumpUntilDue(gron.C, seg, pos, next, reverse); bumped {

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 ?

segIsIntersecting := strings.Index(seg, "*/") == 0
monthDaySeg := segments[3]
monthDaySegIsIntersecting := strings.Index(monthDaySeg, "*") == 0 || monthDaySeg == "?"

intersectCase := segIsIntersecting || monthDaySegIsIntersecting

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 !intersectCase {
skipMonthDayForIter = true
}
continue
}
// Weekday was bumped, so we need to check for month day

if intersectCase {
// 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, _, err = bumpUntilDue(gron.C, segments[0], 0, next.Add(delta), reverse)
next = next.Add(delta)
continue
}
return
Expand Down Expand Up @@ -116,20 +173,46 @@ func bump(ref time.Time, pos int, reverse bool) time.Time {
if reverse {
factor = -1
}
loc := ref.Location()

switch pos {
case 0:
ref = ref.Add(time.Duration(factor) * time.Second)
case 1:
ref = ref.Add(time.Duration(factor) * time.Minute)
minTime := ref.Add(time.Duration(factor) * time.Minute)
if reverse {
ref = time.Date(minTime.Year(), minTime.Month(), minTime.Day(), minTime.Hour(), minTime.Minute(), 59, 0, loc)
} else {
ref = time.Date(minTime.Year(), minTime.Month(), minTime.Day(), minTime.Hour(), minTime.Minute(), 0, 0, loc)
}
case 2:
ref = ref.Add(time.Duration(factor) * time.Hour)
hTime := ref.Add(time.Duration(factor) * time.Hour)
if reverse {
ref = time.Date(hTime.Year(), hTime.Month(), hTime.Day(), hTime.Hour(), 59, 59, 0, loc)
} else {
ref = time.Date(hTime.Year(), hTime.Month(), hTime.Day(), hTime.Hour(), 0, 0, 0, loc)
}
case 3, 5:
ref = ref.AddDate(0, 0, factor)
dTime := ref.AddDate(0, 0, factor)
if reverse {
ref = time.Date(dTime.Year(), dTime.Month(), dTime.Day(), 23, 59, 59, 0, loc)
} else {
ref = time.Date(dTime.Year(), dTime.Month(), dTime.Day(), 0, 0, 0, 0, loc)
}
case 4:
ref = ref.AddDate(0, factor, 0)
ref = time.Date(ref.Year(), ref.Month(), 1, 0, 0, 0, 0, loc)
if reverse {
ref = ref.Add(time.Duration(factor) * time.Second)
} else {
ref = ref.AddDate(0, factor, 0)
}
case 6:
ref = ref.AddDate(factor, 0, 0)
yTime := ref.AddDate(factor, 0, 0)
if reverse {
ref = time.Date(yTime.Year(), 12, 31, 23, 59, 59, 0, loc)
} else {
ref = time.Date(yTime.Year(), 1, 1, 0, 0, 0, 0, loc)
}
}
return ref
}
19 changes: 10 additions & 9 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ func inStep(val int, s string, bounds []int) (bool, error) {
return false, errors.New("step can't be 0")
}

if strings.Index(s, "*/") == 0 || strings.Index(s, "0/") == 0 {
if strings.Index(s, "*/") == 0 {
return (val-bounds[0])%step == 0, nil
}
if strings.Index(s, "0/") == 0 {
return val%step == 0, nil
}

Expand Down Expand Up @@ -104,22 +107,20 @@ func isValidMonthDay(val string, last int, ref time.Time) (valid bool, err error

func isValidWeekDay(val string, last int, ref time.Time) (bool, error) {
loc := ref.Location()
if pos := strings.Index(strings.ReplaceAll(val, "7L", "0L"), "L"); pos > 0 {

if pos := strings.Index(val, "L"); pos > 0 {
nval, err := strconv.Atoi(val[0:pos])
if err != nil {
return false, err
}

for i := 0; i < 7; i++ {
decr := last - i
dref := time.Date(ref.Year(), ref.Month(), decr, ref.Hour(), ref.Minute(), ref.Second(), ref.Nanosecond(), loc)

if int(dref.Weekday()) == nval {
return ref.Day() == decr, nil
day := last - i
dref := time.Date(ref.Year(), ref.Month(), day, ref.Hour(), ref.Minute(), ref.Second(), 0, loc)
if int(dref.Weekday()) == nval%7 {
return ref.Day() == day, nil
}
}

return false, nil
}

pos := strings.Index(val, "#")
Expand Down
Loading