Skip to content

Commit

Permalink
DST Fixes (#4)
Browse files Browse the repository at this point in the history
* Add test for checking expressions with non-trivial timezone

* Add daylight saving fixes and update tests cases

---------

Co-authored-by: naimul <naimul@thoughtmachine.net>
  • Loading branch information
naimulhaider and naimul authored Feb 14, 2023
1 parent f17a206 commit 24cff38
Show file tree
Hide file tree
Showing 6 changed files with 687 additions and 63 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
154 changes: 106 additions & 48 deletions cronexpr.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ func Parse(cronLine string) (*Expression, error) {
return &expr, nil
}

// roundTimeToNextSec rounds any nanosecond offset to the next second
func roundTimeToNextSec(tm time.Time) time.Time {
diffUntilNext := time.Second - time.Duration(tm.Nanosecond())
return tm.Add(diffUntilNext)
}

/******************************************************************************/

// Next returns the closest time instant immediately following `fromTime` which
Expand All @@ -159,6 +165,10 @@ func (expr *Expression) Next(fromTime time.Time) time.Time {
if fromTime.IsZero() {
return fromTime
}
loc := fromTime.Location()
t := roundTimeToNextSec(fromTime)

WRAP:

// Since expr.nextSecond()-expr.nextMonth() expects that the
// supplied time stamp is a perfect match to the underlying cron
Expand All @@ -169,69 +179,117 @@ func (expr *Expression) Next(fromTime time.Time) time.Time {
// stamp falls in between matching time stamps, thus we move
// to closest future matching immediately upon encountering a mismatching
// time stamp.

// year
v := fromTime.Year()
i := sort.SearchInts(expr.yearList, v)
if i == len(expr.yearList) {

v := t.Year()
if i := sort.SearchInts(expr.yearList, v); i == len(expr.yearList) {
return time.Time{}
} else if v != expr.yearList[i] {
t = time.Date(expr.yearList[i], time.Month(expr.monthList[0]), 1, 0, 0, 0, 0, loc)
}
if v != expr.yearList[i] {
return expr.nextYear(fromTime)
}
// month
v = int(fromTime.Month())
i = sort.SearchInts(expr.monthList, v)
if i == len(expr.monthList) {
return expr.nextYear(fromTime)
}
if v != expr.monthList[i] {
return expr.nextMonth(fromTime)

v = int(t.Month())
if i := sort.SearchInts(expr.monthList, v); i == len(expr.monthList) {
// try again with a new year
t = time.Date(t.Year()+1, time.Month(expr.monthList[0]), 1, 0, 0, 0, 0, loc)
goto WRAP
} else if v != expr.monthList[i] {
t = time.Date(t.Year(), time.Month(expr.monthList[i]), 1, 0, 0, 0, 0, loc)
}

expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(fromTime.Year(), int(fromTime.Month()))
expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(t.Year(), int(t.Month()))
if len(expr.actualDaysOfMonthList) == 0 {
return expr.nextMonth(fromTime)
t = time.Date(t.Year(), t.Month()+1, 1, 0, 0, 0, 0, loc)
goto WRAP
}

// day of month
v = fromTime.Day()
i = sort.SearchInts(expr.actualDaysOfMonthList, v)
if i == len(expr.actualDaysOfMonthList) {
return expr.nextMonth(fromTime)
v = t.Day()
if i := sort.SearchInts(expr.actualDaysOfMonthList, v); i == len(expr.actualDaysOfMonthList) {
t = time.Date(t.Year(), t.Month()+1, 1, 0, 0, 0, 0, loc)
goto WRAP
} else if v != expr.actualDaysOfMonthList[i] {
t = time.Date(t.Year(), t.Month(), expr.actualDaysOfMonthList[i], 0, 0, 0, 0, loc)

// in San Palo, before 2019, there may be no midnight (or multiple midnights)
// due to DST
if t.Hour() != 0 {
if t.Hour() > 12 {
t = t.Add(time.Duration(24-t.Hour()) * time.Hour)
} else {
t = t.Add(time.Duration(-t.Hour()) * time.Hour)
}
}
}
if v != expr.actualDaysOfMonthList[i] {
return expr.nextDayOfMonth(fromTime)

if timeZoneInDay(t) {
goto SLOW_CLOCK
}
// hour
v = fromTime.Hour()
i = sort.SearchInts(expr.hourList, v)
if i == len(expr.hourList) {
return expr.nextDayOfMonth(fromTime)

// Fast path where hours/minutes behave as expected trivially
v = t.Hour()
if i := sort.SearchInts(expr.hourList, v); i == len(expr.hourList) {
t = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, loc)
goto WRAP
} else if v != expr.hourList[i] {
t = time.Date(t.Year(), t.Month(), t.Day(), expr.hourList[i], expr.minuteList[0], expr.secondList[0], 0, loc)
}
if v != expr.hourList[i] {
return expr.nextHour(fromTime)

v = t.Minute()
if i := sort.SearchInts(expr.minuteList, v); i == len(expr.minuteList) {
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour()+1, 0, 0, 0, loc)
goto WRAP
} else if v != expr.minuteList[i] {
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), expr.minuteList[i], expr.secondList[0], 0, loc)
}
// minute
v = fromTime.Minute()
i = sort.SearchInts(expr.minuteList, v)
if i == len(expr.minuteList) {
return expr.nextHour(fromTime)

v = t.Second()
if i := sort.SearchInts(expr.secondList, v); i == len(expr.secondList) {
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()+1, 0, 0, loc)
goto WRAP
} else if v != expr.secondList[i] {
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), expr.secondList[i], 0, loc)
}
if v != expr.minuteList[i] {
return expr.nextMinute(fromTime)

return t

SLOW_CLOCK:
// daylight saving effect is here, where odd things happen:
// An hour may have 60 minutes, 30 minutes or 90 minutes;
// partial hours may "repeat"!
for !sortContains(expr.hourList, t.Hour()) {
hourBefore := t.Hour()
t = t.Add(time.Hour)
if hourBefore == t.Hour() {
t = t.Add(time.Hour)
}
t = t.Truncate(time.Minute)
if t.Minute() != 0 {
t = t.Add(-1 * time.Minute * time.Duration(t.Minute()))
}

if t.Hour() == 0 {
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
goto WRAP
}
}
// second
v = fromTime.Second()
i = sort.SearchInts(expr.secondList, v)
if i == len(expr.secondList) {
return expr.nextMinute(fromTime)

for !sortContains(expr.minuteList, t.Minute()) {
hoursBefore := t.Hour()
t = t.Truncate(time.Minute).Add(time.Minute)
if hoursBefore != t.Hour() {
goto WRAP
}
}

// If we reach this point, there is nothing better to do
// than to move to the next second
v = t.Second()
t = t.Truncate(time.Minute)
if i := sort.SearchInts(expr.secondList, v); i == len(expr.secondList) {
t = t.Add(time.Minute)
goto WRAP
} else {
t = t.Add(time.Duration(expr.secondList[i]) * time.Second)
}

return expr.nextSecond(fromTime)
return t
}

/******************************************************************************/
Expand Down Expand Up @@ -259,7 +317,7 @@ func (expr *Expression) NextN(fromTime time.Time, n uint) []time.Time {
if n == 0 {
break
}
fromTime = expr.nextSecond(fromTime)
fromTime = expr.Next(fromTime)
}
}
return nextTimes
Expand Down
15 changes: 15 additions & 0 deletions cronexpr_next.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,3 +287,18 @@ func workdayOfMonth(targetDom, lastDom time.Time) int {
}
return dom
}

func sortContains(a []int, x int) bool {
i := sort.SearchInts(a, x)
return i < len(a) && a[i] == x
}

func timeZoneInDay(t time.Time) bool {
if t.Location() == time.UTC {
return false
}

_, off := t.AddDate(0, 0, -1).Zone()
_, ndoff := t.AddDate(0, 0, 1).Zone()
return off != ndoff
}
Loading

0 comments on commit 24cff38

Please sign in to comment.