Added support for DST with configurable behavior.

This commit is contained in:
Augusto Becciu 2016-10-28 17:53:31 -03:00
parent f0984319b4
commit 0dc543e5ad
5 changed files with 808 additions and 17 deletions

View File

@ -69,6 +69,7 @@ Other details
* If only five fields are present, a `0` second field is prepended and a wildcard year field is appended, that is, `* * * * Mon` internally become `0 * * * * Mon *`.
* Domain for day-of-week field is [0-7] instead of [0-6], 7 being Sunday (like 0). This to comply with http://linux.die.net/man/5/crontab#.
* As of now, the behavior of the code is undetermined if a malformed cron expression is supplied
* By default, on a DST change, it returns the times that would have been skipped when the clock moves forward and returns only once the times that would have been repeated when the clock moves backwards. For customized behavior, see the godoc documentation.
Install
-------
@ -131,4 +132,3 @@ License: pick the one which suits you best:
- GPL v3 see <https://www.gnu.org/licenses/gpl.html>
- APL v2 see <http://www.apache.org/licenses/LICENSE-2.0>

View File

@ -27,6 +27,7 @@ import (
// <https://github.com/gorhill/cronexpr#implementation>
type Expression struct {
expression string
options Options
secondList []int
minuteList []int
hourList []int
@ -42,6 +43,30 @@ type Expression struct {
lastWeekDaysOfWeek map[int]bool
daysOfWeekRestricted bool
yearList []int
// indicates an instance of a expression used internally
// for rounding a time
rounding bool
}
type DSTFlags uint
const (
// DSTLeapUnskip indicates the parser to not skip times that would have been
// been skipped when the clock moves forward on a DST change.
DSTLeapUnskip = 1 << iota
// DSTFallFireEarly indicates the parser to return the earliest time
// when a time is repeated due to a DST fall.
DSTFallFireEarly
// DSTFallFireLate indicates the parser to return the latest time
// when a time is repeated due to a DST fall.
DSTFallFireLate
)
type Options struct {
DSTFlags DSTFlags
}
/******************************************************************************/
@ -67,6 +92,14 @@ func MustParse(cronLine string) *Expression {
// about what is a well-formed cron expression from this library's point of
// view.
func Parse(cronLine string) (*Expression, error) {
return ParseWithOptions(cronLine, Options{DSTFlags: DSTLeapUnskip | DSTFallFireEarly})
}
// ParseWithOptions is used to build a Expression pointer with custom options.
func ParseWithOptions(cronLine string, options Options) (*Expression, error) {
if options.DSTFlags == 0 {
return nil, fmt.Errorf("missing DST flags")
}
// Maybe one of the built-in aliases is being used
cron := cronNormalizer.Replace(cronLine)
@ -81,7 +114,7 @@ func Parse(cronLine string) (*Expression, error) {
fieldCount = 7
}
var expr = Expression{}
var expr = Expression{options: options}
var field = 0
var err error

View File

@ -53,7 +53,8 @@ func (expr *Expression) nextYear(t time.Time) time.Time {
0,
t.Location()))
}
return time.Date(
next := time.Date(
expr.yearList[i],
time.Month(expr.monthList[0]),
expr.actualDaysOfMonthList[0],
@ -61,7 +62,9 @@ func (expr *Expression) nextYear(t time.Time) time.Time {
expr.minuteList[0],
expr.secondList[0],
0,
t.Location())
time.UTC)
return expr.nextTime(t, next)
}
/******************************************************************************/
@ -87,7 +90,7 @@ func (expr *Expression) nextMonth(t time.Time) time.Time {
t.Location()))
}
return time.Date(
next := time.Date(
t.Year(),
time.Month(expr.monthList[i]),
expr.actualDaysOfMonthList[0],
@ -95,7 +98,9 @@ func (expr *Expression) nextMonth(t time.Time) time.Time {
expr.minuteList[0],
expr.secondList[0],
0,
t.Location())
time.UTC)
return expr.nextTime(t, next)
}
/******************************************************************************/
@ -108,7 +113,7 @@ func (expr *Expression) nextDayOfMonth(t time.Time) time.Time {
return expr.nextMonth(t)
}
return time.Date(
next := time.Date(
t.Year(),
t.Month(),
expr.actualDaysOfMonthList[i],
@ -116,7 +121,9 @@ func (expr *Expression) nextDayOfMonth(t time.Time) time.Time {
expr.minuteList[0],
expr.secondList[0],
0,
t.Location())
time.UTC)
return expr.nextTime(t, next)
}
/******************************************************************************/
@ -129,7 +136,7 @@ func (expr *Expression) nextHour(t time.Time) time.Time {
return expr.nextDayOfMonth(t)
}
return time.Date(
next := time.Date(
t.Year(),
t.Month(),
t.Day(),
@ -137,7 +144,9 @@ func (expr *Expression) nextHour(t time.Time) time.Time {
expr.minuteList[0],
expr.secondList[0],
0,
t.Location())
time.UTC)
return expr.nextTime(t, next)
}
/******************************************************************************/
@ -150,7 +159,7 @@ func (expr *Expression) nextMinute(t time.Time) time.Time {
return expr.nextHour(t)
}
return time.Date(
next := time.Date(
t.Year(),
t.Month(),
t.Day(),
@ -158,7 +167,9 @@ func (expr *Expression) nextMinute(t time.Time) time.Time {
expr.minuteList[i],
expr.secondList[0],
0,
t.Location())
time.UTC)
return expr.nextTime(t, next)
}
/******************************************************************************/
@ -174,7 +185,7 @@ func (expr *Expression) nextSecond(t time.Time) time.Time {
return expr.nextMinute(t)
}
return time.Date(
next := time.Date(
t.Year(),
t.Month(),
t.Day(),
@ -182,7 +193,106 @@ func (expr *Expression) nextSecond(t time.Time) time.Time {
t.Minute(),
expr.secondList[i],
0,
t.Location())
time.UTC)
return expr.nextTime(t, next)
}
func (expr *Expression) roundTime(t time.Time) time.Time {
roundingExpr := new(Expression)
*roundingExpr = *expr
roundingExpr.rounding = true
i := sort.SearchInts(expr.hourList, t.Hour())
if i == len(expr.hourList) || expr.hourList[i] != t.Hour() {
return roundingExpr.nextHour(t)
}
i = sort.SearchInts(expr.minuteList, t.Minute())
if i == len(expr.minuteList) || expr.minuteList[i] != t.Minute() {
return roundingExpr.nextMinute(t)
}
i = sort.SearchInts(expr.secondList, t.Second())
if i == len(expr.secondList) || expr.secondList[i] != t.Second() {
return roundingExpr.nextSecond(t)
}
return t
}
func (expr *Expression) isRounded(t time.Time) bool {
i := sort.SearchInts(expr.hourList, t.Hour())
if i == len(expr.hourList) || expr.hourList[i] != t.Hour() {
return false
}
i = sort.SearchInts(expr.minuteList, t.Minute())
if i == len(expr.minuteList) || expr.minuteList[i] != t.Minute() {
return false
}
i = sort.SearchInts(expr.secondList, t.Second())
if i == len(expr.secondList) || expr.secondList[i] != t.Second() {
return false
}
return true
}
func (expr *Expression) nextTime(prev, next time.Time) time.Time {
dstFlags := expr.options.DSTFlags
t := prev.Add(noTZDiff(prev, next))
offsetDiff := utcOffset(t) - utcOffset(prev)
// a dst leap occurred
if offsetDiff > 0 {
if dstFlags&DSTLeapUnskip != 0 {
return findTimeOfDSTChange(prev, t).Add(1 * time.Second)
}
return expr.roundTime(t)
}
// a dst fall occurred
if offsetDiff < 0 {
twinT := findTwinTime(prev)
if !twinT.IsZero() {
if dstFlags&DSTFallFireLate != 0 {
return twinT
}
if dstFlags&DSTFallFireEarly != 0 {
// skip the twin time
return expr.Next(expr.roundTime(twinT))
}
}
if dstFlags&DSTFallFireEarly != 0 {
return t
}
return expr.roundTime(t)
}
twinT := findTwinTime(t)
if !twinT.IsZero() {
if dstFlags&DSTFallFireEarly != 0 {
return t
}
if dstFlags&DSTFallFireLate != 0 {
return twinT
}
}
if dstFlags&DSTFallFireLate == 0 && !expr.rounding {
twinT = findTwinTime(prev)
if !twinT.IsZero() && twinT.Before(prev) && !expr.isRounded(prev) {
return expr.Next(t)
}
}
return t
}
/******************************************************************************/
@ -290,3 +400,83 @@ func workdayOfMonth(targetDom, lastDom time.Time) int {
}
return dom
}
func utcOffset(t time.Time) int {
_, offset := t.Zone()
return offset
}
func noTZ(t time.Time) time.Time {
return t.UTC().Add(time.Duration(utcOffset(t)) * time.Second)
}
func noTZDiff(t1, t2 time.Time) time.Duration {
t1 = noTZ(t1)
t2 = noTZ(t2)
return t2.Sub(t1)
}
// findTimeOfDSTChange returns the time a second before a DST change occurs,
// and returns zero time in case there's no DST change.
func findTimeOfDSTChange(t1, t2 time.Time) time.Time {
if t1.Location() != t2.Location() || utcOffset(t1) == utcOffset(t2) || t1.Location() == time.UTC {
return time.Time{}
}
// make sure t2 > t1
if t2.Before(t1) {
t := t2
t1 = t2
t2 = t
}
// do a binary search to find the time one second before the dst change
len := t2.Unix() - t1.Unix()
var a int64
b := len
for len > 1 {
len = (b - a + 1) / 2
if utcOffset(t1.Add(time.Duration(a+len)*time.Second)) != utcOffset(t1) {
b = a + len
} else {
a = a + len
}
}
return t1.Add(time.Duration(a) * time.Second)
}
// When a DST fall accurs, a certain interval of time is repeated. Once
// in DST time and once in standard time.
// findTwinTime tries to find the repated "twin" time if one exists.
func findTwinTime(t time.Time) time.Time {
offsetDiff := utcOffset(t.Add(12*time.Hour)) - utcOffset(t)
// a fall occurs within the next 12 hours
if offsetDiff < 0 {
border := findTimeOfDSTChange(t, t.Add(12*time.Hour))
t0 := border.Add(time.Duration(offsetDiff) * time.Second)
if t0.After(t) {
return t
}
dur := t.Sub(t0)
return border.Add(dur)
}
offsetDiff = utcOffset(t) - utcOffset(t.Add(-12*time.Second))
// a fall occurred in the past 12 hours
if offsetDiff < 0 {
border := findTimeOfDSTChange(t.Add(-12*time.Hour), t)
t0 := border.Add(time.Duration(offsetDiff) * time.Second)
if t0.Add(time.Duration(-2*offsetDiff) * time.Second).Before(t) {
return t
}
dur := t.Sub(border)
return t0.Add(dur)
}
return time.Time{}
}

View File

@ -15,6 +15,7 @@ package cronexpr_test
/******************************************************************************/
import (
"fmt"
"testing"
"time"
@ -284,6 +285,495 @@ func TestNextN_every5min(t *testing.T) {
}
}
func TestDST(t *testing.T) {
var locs [3]*time.Location
// 1 hour DST, negative UTC offset
// time.Date(2014, 3, 9, 2, 0, 0, 0, locs[0]) Leap PST -> PDT
// time.Date(2014, 11, 2, 1, 59, 59, 0, locs[0]) Fall PDT -> PST
locs[0], _ = time.LoadLocation("America/Los_Angeles")
// biggest tz leap ever (3 hours), occurred from YAKT to MAGST
// at time.Date(1981, 4, 1, 3, 0, 0, 0, locs[1]),
locs[1], _ = time.LoadLocation("Asia/Ust-Nera")
// 30 mins DST, positive UTC offset
// time.Date(2014, 10, 5, 2, 30, 0, 0, locs[2]) Leap LHST -> LHDT
// time.Date(2015, 4, 5, 1, 30, 0, 0, locs[2]) Fall LHDT -> LHST
locs[2], _ = time.LoadLocation("Australia/LHI")
cases := []struct {
name string
expr string
opts cronexpr.Options
from time.Time
expected []time.Time
}{
{
fmt.Sprintf("%s daily leap skip", locs[0]),
"0 0 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(2014, 3, 9, 1, 0, 0, 0, locs[0]),
[]time.Time{
time.Date(2014, 3, 10, 2, 0, 0, 0, locs[0]),
time.Date(2014, 3, 11, 2, 0, 0, 0, locs[0]),
time.Date(2014, 3, 12, 2, 0, 0, 0, locs[0]),
time.Date(2014, 3, 13, 2, 0, 0, 0, locs[0]),
},
},
{
fmt.Sprintf("%s daily leap unskip", locs[0]),
"0 0 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTLeapUnskip | cronexpr.DSTFallFireEarly},
time.Date(2014, 3, 9, 1, 0, 0, 0, locs[0]),
[]time.Time{
time.Date(2014, 3, 9, 3, 0, 0, 0, locs[0]),
time.Date(2014, 3, 10, 2, 0, 0, 0, locs[0]),
time.Date(2014, 3, 11, 2, 0, 0, 0, locs[0]),
time.Date(2014, 3, 12, 2, 0, 0, 0, locs[0]),
},
},
{
fmt.Sprintf("%s hourly leap skip", locs[0]),
"0 0 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(2014, 3, 9, 1, 0, 0, 0, locs[0]),
[]time.Time{
time.Date(2014, 3, 9, 3, 0, 0, 0, locs[0]),
time.Date(2014, 3, 9, 4, 0, 0, 0, locs[0]),
time.Date(2014, 3, 9, 5, 0, 0, 0, locs[0]),
time.Date(2014, 3, 9, 6, 0, 0, 0, locs[0]),
},
},
{
fmt.Sprintf("%s hourly leap unskip", locs[0]),
"0 0 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTLeapUnskip | cronexpr.DSTFallFireEarly},
time.Date(2014, 3, 9, 1, 0, 0, 0, locs[0]),
[]time.Time{
time.Date(2014, 3, 9, 3, 0, 0, 0, locs[0]),
time.Date(2014, 3, 9, 4, 0, 0, 0, locs[0]),
time.Date(2014, 3, 9, 5, 0, 0, 0, locs[0]),
time.Date(2014, 3, 9, 6, 0, 0, 0, locs[0]),
},
},
{
fmt.Sprintf("%s daily quarter-hourly leap skip", locs[0]),
"0 */15 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(2014, 3, 9, 1, 0, 0, 0, locs[0]),
[]time.Time{
time.Date(2014, 3, 10, 2, 0, 0, 0, locs[0]),
time.Date(2014, 3, 10, 2, 15, 0, 0, locs[0]),
time.Date(2014, 3, 10, 2, 30, 0, 0, locs[0]),
time.Date(2014, 3, 10, 2, 45, 0, 0, locs[0]),
},
},
{
fmt.Sprintf("%s daily quarter-hourly leap unskip", locs[0]),
"0 */15 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTLeapUnskip | cronexpr.DSTFallFireEarly},
time.Date(2014, 3, 9, 1, 0, 0, 0, locs[0]),
[]time.Time{
time.Date(2014, 3, 9, 3, 0, 0, 0, locs[0]),
time.Date(2014, 3, 9, 3, 15, 0, 0, locs[0]),
time.Date(2014, 3, 9, 3, 30, 0, 0, locs[0]),
time.Date(2014, 3, 9, 3, 45, 0, 0, locs[0]),
},
},
{
fmt.Sprintf("%s daily fall fire early", locs[0]),
"0 0 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(2014, 11, 1, 2, 0, 0, 0, locs[0]),
[]time.Time{
time.Date(2014, 11, 2, 1, 0, 0, 0, locs[0]).Add(1 * time.Hour),
time.Date(2014, 11, 3, 2, 0, 0, 0, locs[0]),
time.Date(2014, 11, 4, 2, 0, 0, 0, locs[0]),
time.Date(2014, 11, 5, 2, 0, 0, 0, locs[0]),
},
},
{
fmt.Sprintf("%s daily fall fire late", locs[0]),
"0 0 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireLate},
time.Date(2014, 11, 1, 2, 0, 0, 0, locs[0]),
[]time.Time{
time.Date(2014, 11, 2, 2, 0, 0, 0, locs[0]),
time.Date(2014, 11, 3, 2, 0, 0, 0, locs[0]),
time.Date(2014, 11, 4, 2, 0, 0, 0, locs[0]),
time.Date(2014, 11, 5, 2, 0, 0, 0, locs[0]),
},
},
{
fmt.Sprintf("%s daily fall fire both", locs[0]),
"0 0 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly | cronexpr.DSTFallFireLate},
time.Date(2014, 11, 1, 2, 0, 0, 0, locs[0]),
[]time.Time{
time.Date(2014, 11, 2, 1, 0, 0, 0, locs[0]).Add(1 * time.Hour),
time.Date(2014, 11, 2, 2, 0, 0, 0, locs[0]),
time.Date(2014, 11, 3, 2, 0, 0, 0, locs[0]),
time.Date(2014, 11, 4, 2, 0, 0, 0, locs[0]),
},
},
{
fmt.Sprintf("%s hourly fall fire early", locs[0]),
"0 0 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(2014, 11, 2, 0, 0, 0, 0, locs[0]),
[]time.Time{
time.Date(2014, 11, 2, 1, 0, 0, 0, locs[0]),
time.Date(2014, 11, 2, 2, 0, 0, 0, locs[0]),
time.Date(2014, 11, 2, 3, 0, 0, 0, locs[0]),
time.Date(2014, 11, 2, 4, 0, 0, 0, locs[0]),
},
},
{
fmt.Sprintf("%s hourly fall fire late", locs[0]),
"0 0 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireLate},
time.Date(2014, 11, 2, 0, 0, 0, 0, locs[0]),
[]time.Time{
time.Date(2014, 11, 2, 1, 0, 0, 0, locs[0]).Add(1 * time.Hour),
time.Date(2014, 11, 2, 2, 0, 0, 0, locs[0]),
time.Date(2014, 11, 2, 3, 0, 0, 0, locs[0]),
time.Date(2014, 11, 2, 4, 0, 0, 0, locs[0]),
},
},
{
fmt.Sprintf("%s hourly fall fire twice", locs[0]),
"0 0 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly | cronexpr.DSTFallFireLate},
time.Date(2014, 11, 2, 0, 0, 0, 0, locs[0]),
[]time.Time{
time.Date(2014, 11, 2, 1, 0, 0, 0, locs[0]),
time.Date(2014, 11, 2, 1, 0, 0, 0, locs[0]).Add(1 * time.Hour),
time.Date(2014, 11, 2, 2, 0, 0, 0, locs[0]),
time.Date(2014, 11, 2, 3, 0, 0, 0, locs[0]),
},
},
{
fmt.Sprintf("%s daily leap skip", locs[1]),
"0 0 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(1981, 3, 31, 2, 0, 0, 0, locs[1]),
[]time.Time{
time.Date(1981, 4, 2, 2, 0, 0, 0, locs[1]),
time.Date(1981, 4, 3, 2, 0, 0, 0, locs[1]),
time.Date(1981, 4, 4, 2, 0, 0, 0, locs[1]),
time.Date(1981, 4, 5, 2, 0, 0, 0, locs[1]),
},
},
{
fmt.Sprintf("%s daily leap unskip", locs[1]),
"0 0 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTLeapUnskip | cronexpr.DSTFallFireEarly},
time.Date(1981, 3, 31, 2, 0, 0, 0, locs[1]),
[]time.Time{
time.Date(1981, 4, 1, 3, 0, 0, 0, locs[1]),
time.Date(1981, 4, 2, 2, 0, 0, 0, locs[1]),
time.Date(1981, 4, 3, 2, 0, 0, 0, locs[1]),
time.Date(1981, 4, 4, 2, 0, 0, 0, locs[1]),
},
},
{
fmt.Sprintf("%s hourly leap skip", locs[1]),
"0 0 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(1981, 3, 31, 23, 0, 0, 0, locs[1]),
[]time.Time{
time.Date(1981, 4, 1, 3, 0, 0, 0, locs[1]),
time.Date(1981, 4, 1, 4, 0, 0, 0, locs[1]),
time.Date(1981, 4, 1, 5, 0, 0, 0, locs[1]),
time.Date(1981, 4, 1, 6, 0, 0, 0, locs[1]),
},
},
{
fmt.Sprintf("%s hourly leap unskip", locs[1]),
"0 0 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTLeapUnskip | cronexpr.DSTFallFireEarly},
time.Date(1981, 3, 31, 23, 0, 0, 0, locs[1]),
[]time.Time{
time.Date(1981, 4, 1, 3, 0, 0, 0, locs[1]),
time.Date(1981, 4, 1, 4, 0, 0, 0, locs[1]),
time.Date(1981, 4, 1, 5, 0, 0, 0, locs[1]),
time.Date(1981, 4, 1, 6, 0, 0, 0, locs[1]),
},
},
{
fmt.Sprintf("%s quarter-hourly leap skip", locs[1]),
"0 */15 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(1981, 3, 31, 23, 15, 0, 0, locs[1]),
[]time.Time{
time.Date(1981, 3, 31, 23, 30, 0, 0, locs[1]),
time.Date(1981, 3, 31, 23, 45, 0, 0, locs[1]),
time.Date(1981, 4, 1, 3, 0, 0, 0, locs[1]),
time.Date(1981, 4, 1, 3, 15, 0, 0, locs[1]),
},
},
{
fmt.Sprintf("%s quarter-hourly leap unskip", locs[1]),
"0 */15 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTLeapUnskip | cronexpr.DSTFallFireEarly},
time.Date(1981, 3, 31, 23, 15, 0, 0, locs[1]),
[]time.Time{
time.Date(1981, 3, 31, 23, 30, 0, 0, locs[1]),
time.Date(1981, 3, 31, 23, 45, 0, 0, locs[1]),
time.Date(1981, 4, 1, 3, 0, 0, 0, locs[1]),
time.Date(1981, 4, 1, 3, 15, 0, 0, locs[1]),
},
},
{
fmt.Sprintf("%s daily third-hourly leap skip", locs[1]),
"0 */20 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(1981, 3, 31, 2, 40, 0, 0, locs[1]),
[]time.Time{
time.Date(1981, 4, 2, 2, 0, 0, 0, locs[1]),
time.Date(1981, 4, 2, 2, 20, 0, 0, locs[1]),
time.Date(1981, 4, 2, 2, 40, 0, 0, locs[1]),
time.Date(1981, 4, 3, 2, 0, 0, 0, locs[1]),
},
},
{
fmt.Sprintf("%s daily third-hourly leap unskip", locs[1]),
"0 */20 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTLeapUnskip | cronexpr.DSTFallFireEarly},
time.Date(1981, 3, 31, 2, 40, 0, 0, locs[1]),
[]time.Time{
time.Date(1981, 4, 1, 3, 0, 0, 0, locs[1]),
time.Date(1981, 4, 1, 3, 20, 0, 0, locs[1]),
time.Date(1981, 4, 1, 3, 40, 0, 0, locs[1]),
time.Date(1981, 4, 2, 2, 0, 0, 0, locs[1]),
},
},
{
fmt.Sprintf("%s daily leap skip", locs[2]),
"0 0 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(2014, 10, 4, 2, 0, 0, 0, locs[2]),
[]time.Time{
time.Date(2014, 10, 6, 2, 0, 0, 0, locs[2]),
time.Date(2014, 10, 7, 2, 0, 0, 0, locs[2]),
time.Date(2014, 10, 8, 2, 0, 0, 0, locs[2]),
time.Date(2014, 10, 9, 2, 0, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s daily leap unskip", locs[2]),
"0 0 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTLeapUnskip | cronexpr.DSTFallFireEarly},
time.Date(2014, 10, 4, 2, 0, 0, 0, locs[2]),
[]time.Time{
time.Date(2014, 10, 5, 2, 30, 0, 0, locs[2]),
time.Date(2014, 10, 6, 2, 0, 0, 0, locs[2]),
time.Date(2014, 10, 7, 2, 0, 0, 0, locs[2]),
time.Date(2014, 10, 8, 2, 0, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s hourly leap skip", locs[2]),
"0 0 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(2014, 10, 5, 1, 0, 0, 0, locs[2]),
[]time.Time{
time.Date(2014, 10, 5, 3, 0, 0, 0, locs[2]),
time.Date(2014, 10, 5, 4, 0, 0, 0, locs[2]),
time.Date(2014, 10, 5, 5, 0, 0, 0, locs[2]),
time.Date(2014, 10, 5, 6, 0, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s hourly leap unskip", locs[2]),
"0 0 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTLeapUnskip | cronexpr.DSTFallFireEarly},
time.Date(2014, 10, 5, 1, 0, 0, 0, locs[2]),
[]time.Time{
time.Date(2014, 10, 5, 2, 30, 0, 0, locs[2]),
time.Date(2014, 10, 5, 3, 0, 0, 0, locs[2]),
time.Date(2014, 10, 5, 4, 0, 0, 0, locs[2]),
time.Date(2014, 10, 5, 5, 0, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s quarter-hourly leap skip", locs[2]),
"0 */15 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(2014, 10, 5, 1, 15, 0, 0, locs[2]),
[]time.Time{
time.Date(2014, 10, 5, 1, 30, 0, 0, locs[2]),
time.Date(2014, 10, 5, 1, 45, 0, 0, locs[2]),
time.Date(2014, 10, 5, 2, 30, 0, 0, locs[2]),
time.Date(2014, 10, 5, 2, 45, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s quarter-hourly leap unskip", locs[2]),
"0 */15 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTLeapUnskip | cronexpr.DSTFallFireEarly},
time.Date(2014, 10, 5, 1, 15, 0, 0, locs[2]),
[]time.Time{
time.Date(2014, 10, 5, 1, 30, 0, 0, locs[2]),
time.Date(2014, 10, 5, 1, 45, 0, 0, locs[2]),
time.Date(2014, 10, 5, 2, 30, 0, 0, locs[2]),
time.Date(2014, 10, 5, 2, 45, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s third-hourly leap skip", locs[2]),
"0 */20 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(2014, 10, 4, 2, 40, 0, 0, locs[2]),
[]time.Time{
time.Date(2014, 10, 5, 2, 40, 0, 0, locs[2]),
time.Date(2014, 10, 6, 2, 0, 0, 0, locs[2]),
time.Date(2014, 10, 6, 2, 20, 0, 0, locs[2]),
time.Date(2014, 10, 6, 2, 40, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s third-hourly leap unskip", locs[2]),
"0 */20 2 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTLeapUnskip | cronexpr.DSTFallFireEarly},
time.Date(2014, 10, 4, 2, 40, 0, 0, locs[2]),
[]time.Time{
time.Date(2014, 10, 5, 2, 30, 0, 0, locs[2]),
time.Date(2014, 10, 5, 2, 40, 0, 0, locs[2]),
time.Date(2014, 10, 6, 2, 0, 0, 0, locs[2]),
time.Date(2014, 10, 6, 2, 20, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s daily fall fire early", locs[2]),
"0 45 1 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(2015, 4, 5, 1, 0, 0, 0, locs[2]).Add(45 * time.Minute),
[]time.Time{
time.Date(2015, 4, 6, 1, 45, 0, 0, locs[2]),
time.Date(2015, 4, 7, 1, 45, 0, 0, locs[2]),
time.Date(2015, 4, 8, 1, 45, 0, 0, locs[2]),
time.Date(2015, 4, 9, 1, 45, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s daily fall fire late", locs[2]),
"0 45 1 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireLate},
time.Date(2015, 4, 5, 1, 45, 0, 0, locs[2]),
[]time.Time{
time.Date(2015, 4, 6, 1, 45, 0, 0, locs[2]),
time.Date(2015, 4, 7, 1, 45, 0, 0, locs[2]),
time.Date(2015, 4, 8, 1, 45, 0, 0, locs[2]),
time.Date(2015, 4, 9, 1, 45, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s daily fall fire twice", locs[2]),
"0 45 1 * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly | cronexpr.DSTFallFireLate},
time.Date(2015, 4, 5, 1, 0, 0, 0, locs[2]),
[]time.Time{
time.Date(2015, 4, 5, 1, 0, 0, 0, locs[2]).Add(45 * time.Minute),
time.Date(2015, 4, 5, 1, 45, 0, 0, locs[2]),
time.Date(2015, 4, 6, 1, 45, 0, 0, locs[2]),
time.Date(2015, 4, 7, 1, 45, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s hourly fall fire early", locs[2]),
"0 30 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(2015, 4, 5, 0, 30, 0, 0, locs[2]),
[]time.Time{
time.Date(2015, 4, 5, 0, 30, 0, 0, locs[2]).Add(1 * time.Hour),
time.Date(2015, 4, 5, 2, 30, 0, 0, locs[2]),
time.Date(2015, 4, 5, 3, 30, 0, 0, locs[2]),
time.Date(2015, 4, 5, 4, 30, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s hourly fall fire late", locs[2]),
"0 30 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireLate},
time.Date(2015, 4, 5, 0, 30, 0, 0, locs[2]),
[]time.Time{
time.Date(2015, 4, 5, 1, 30, 0, 0, locs[2]),
time.Date(2015, 4, 5, 2, 30, 0, 0, locs[2]),
time.Date(2015, 4, 5, 3, 30, 0, 0, locs[2]),
time.Date(2015, 4, 5, 4, 30, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s hourly fall fire twice", locs[2]),
"0 30 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly | cronexpr.DSTFallFireLate},
time.Date(2015, 4, 5, 0, 30, 0, 0, locs[2]),
[]time.Time{
time.Date(2015, 4, 5, 0, 30, 0, 0, locs[2]).Add(1 * time.Hour),
time.Date(2015, 4, 5, 1, 30, 0, 0, locs[2]),
time.Date(2015, 4, 5, 2, 30, 0, 0, locs[2]),
time.Date(2015, 4, 5, 3, 30, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s half-hourly fall fire early", locs[2]),
"0 */30 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly},
time.Date(2015, 4, 5, 1, 0, 0, 0, locs[2]),
[]time.Time{
time.Date(2015, 4, 5, 1, 0, 0, 0, locs[2]).Add(30 * time.Minute),
time.Date(2015, 4, 5, 2, 0, 0, 0, locs[2]),
time.Date(2015, 4, 5, 2, 30, 0, 0, locs[2]),
time.Date(2015, 4, 5, 3, 0, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s half-hourly fall fire late", locs[2]),
"0 */30 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireLate},
time.Date(2015, 4, 5, 1, 0, 0, 0, locs[2]),
[]time.Time{
time.Date(2015, 4, 5, 1, 30, 0, 0, locs[2]),
time.Date(2015, 4, 5, 2, 0, 0, 0, locs[2]),
time.Date(2015, 4, 5, 2, 30, 0, 0, locs[2]),
time.Date(2015, 4, 5, 3, 0, 0, 0, locs[2]),
},
},
{
fmt.Sprintf("%s half-hourly fall fire twice", locs[2]),
"0 */30 * * * * *",
cronexpr.Options{DSTFlags: cronexpr.DSTFallFireEarly | cronexpr.DSTFallFireLate},
time.Date(2015, 4, 5, 1, 0, 0, 0, locs[2]),
[]time.Time{
time.Date(2015, 4, 5, 1, 0, 0, 0, locs[2]).Add(30 * time.Minute),
time.Date(2015, 4, 5, 1, 30, 0, 0, locs[2]),
time.Date(2015, 4, 5, 2, 0, 0, 0, locs[2]),
time.Date(2015, 4, 5, 2, 30, 0, 0, locs[2]),
},
},
}
for _, tc := range cases {
s, err := cronexpr.ParseWithOptions(tc.expr, tc.opts)
if err != nil {
t.Fatalf("parser error: %s", err)
}
runs := s.NextN(tc.from, 4)
if len(runs) != 4 {
t.Errorf("Case %s: Expected 4 runs, got %d", tc.name, len(runs))
}
for i := 0; i < len(runs); i++ {
if !runs[i].Equal(tc.expected[i]) {
t.Errorf("Case %s: Expected %v, got %v", tc.name, tc.expected[i], runs[i])
}
}
}
}
/******************************************************************************/
var benchmarkExpressions = []string{

View File

@ -35,3 +35,81 @@ func ExampleMustParse() {
// Sun, 29 Feb 2032 00:00:00 UTC
}
}
// Configure the parser to skip times in DST leaps and
// repeat times in DST falls
func ExampleParseWithOptions_dst1() {
loc, _ := time.LoadLocation("America/Los_Angeles")
t := time.Date(2014, 3, 8, 1, 0, 0, 0, loc)
expr, _ := cronexpr.ParseWithOptions("0 0 2 * * * *", cronexpr.Options{
DSTFlags: cronexpr.DSTFallFireEarly | cronexpr.DSTFallFireLate,
})
fmt.Println("DST leap times:")
nextTimes := expr.NextN(t, 4)
for i := range nextTimes {
fmt.Println(nextTimes[i].Format(time.RFC1123))
}
t = time.Date(2014, 10, 31, 1, 0, 0, 0, loc)
expr, _ = cronexpr.ParseWithOptions("0 0 1 * * * *", cronexpr.Options{
DSTFlags: cronexpr.DSTFallFireEarly | cronexpr.DSTFallFireLate,
})
fmt.Println("DST fall times:")
nextTimes = expr.NextN(t, 4)
for i := range nextTimes {
fmt.Println(nextTimes[i].Format(time.RFC1123))
}
// Output:
// DST leap times:
// Sat, 08 Mar 2014 02:00:00 PST
// Mon, 10 Mar 2014 02:00:00 PDT
// Tue, 11 Mar 2014 02:00:00 PDT
// Wed, 12 Mar 2014 02:00:00 PDT
// DST fall times:
// Sat, 01 Nov 2014 01:00:00 PDT
// Sun, 02 Nov 2014 01:00:00 PDT
// Sun, 02 Nov 2014 01:00:00 PST
// Mon, 03 Nov 2014 01:00:00 PST
}
// Configure the parser to unskip times in DST leaps and
// fire late in DST falls
func ExampleParseWithOptions_dst2() {
loc, _ := time.LoadLocation("America/Los_Angeles")
t := time.Date(2014, 3, 8, 1, 0, 0, 0, loc)
expr, _ := cronexpr.ParseWithOptions("0 0 2 * * * *", cronexpr.Options{
DSTFlags: cronexpr.DSTLeapUnskip | cronexpr.DSTFallFireLate,
})
fmt.Println("DST leap times:")
nextTimes := expr.NextN(t, 4)
for i := range nextTimes {
fmt.Println(nextTimes[i].Format(time.RFC1123))
}
t = time.Date(2014, 10, 31, 1, 0, 0, 0, loc)
expr, _ = cronexpr.ParseWithOptions("0 0 1 * * * *", cronexpr.Options{
DSTFlags: cronexpr.DSTLeapUnskip | cronexpr.DSTFallFireLate,
})
fmt.Println("DST fall times:")
nextTimes = expr.NextN(t, 4)
for i := range nextTimes {
fmt.Println(nextTimes[i].Format(time.RFC1123))
}
// Output:
// DST leap times:
// Sat, 08 Mar 2014 02:00:00 PST
// Sun, 09 Mar 2014 03:00:00 PDT
// Mon, 10 Mar 2014 02:00:00 PDT
// Tue, 11 Mar 2014 02:00:00 PDT
// DST fall times:
// Sat, 01 Nov 2014 01:00:00 PDT
// Sun, 02 Nov 2014 01:00:00 PST
// Mon, 03 Nov 2014 01:00:00 PST
// Tue, 04 Nov 2014 01:00:00 PST
}