diff --git a/examples/go-quartz.go b/examples/go-quartz.go index df99839..d7c4a20 100644 --- a/examples/go-quartz.go +++ b/examples/go-quartz.go @@ -23,11 +23,13 @@ func (pj PrintJob) Execute() { //demo main func main() { sched := quartz.NewStdScheduler() + cronTrigger, _ := quartz.NewCronTrigger("1/3 * * * * *") sched.Start() sched.ScheduleJob(&PrintJob{"Ad hoc Job"}, quartz.NewRunOnceTrigger(time.Second*5)) sched.ScheduleJob(&PrintJob{"First job"}, quartz.NewSimpleTrigger(time.Second*12)) sched.ScheduleJob(&PrintJob{"Second job"}, quartz.NewSimpleTrigger(time.Second*6)) sched.ScheduleJob(&PrintJob{"Third job"}, quartz.NewSimpleTrigger(time.Second*3)) + sched.ScheduleJob(&PrintJob{"Cron job"}, cronTrigger) time.Sleep(time.Second * 15) sched.Stop() } diff --git a/quartz/cron.go b/quartz/cron.go index 3c55dff..68940ed 100644 --- a/quartz/cron.go +++ b/quartz/cron.go @@ -10,6 +10,7 @@ import ( ) type CronTrigger struct { + expression string fields []*CronField lastDefined int } @@ -29,7 +30,16 @@ func NewCronTrigger(expr string) (*CronTrigger, error) { if lastDefined == -1 { fields[0].values, _ = fillRange(0, 59) } - return &CronTrigger{fields, lastDefined}, nil + return &CronTrigger{expr, fields, lastDefined}, nil +} + +func (ct *CronTrigger) NextFireTime(prev int64) (int64, error) { + parser := NewCronExpressionParser(ct.lastDefined) + return parser.nextTime(prev, ct.fields) +} + +func (st *CronTrigger) Description() string { + return fmt.Sprintf("CronTrigger %s", st.expression) } type CronExpressionParser struct { @@ -62,9 +72,21 @@ func (cf *CronField) toString() string { } var ( - months = []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} + months = []string{"0", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} days = []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} - daysInMonth = []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} + daysInMonth = []int{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} + + // pre-defined cron expressions + special = map[string]string{ + "@yearly": "0 0 0 1 1 *", + "@monthly": "0 0 0 1 * *", + "@weekly": "0 0 0 * * 0", + "@daily": "0 0 0 * * *", + "@hourly": "0 0 * * * *", + } + + readDateLayout = "Mon Jan 2 15:04:05 2006" + writeDateLayout = "Jan 2 15:04:05 2006" ) // @@ -79,11 +101,6 @@ const ( yearIndex ) -func (ct *CronTrigger) NextFireTime(prev int64) (int64, error) { - parser := NewCronExpressionParser(ct.lastDefined) - return parser.nextTime(prev, ct.fields) -} - func (parser *CronExpressionParser) nextTime(prev int64, fields []*CronField) (nextTime int64, err error) { defer func() { if r := recover(); r != nil { @@ -97,31 +114,33 @@ func (parser *CronExpressionParser) nextTime(prev int64, fields []*CronField) (n } } }() - // UnixDate = "Mon Jan _2 15:04:05 MST 2006" - tfmt := time.Unix(prev, 0).Format(time.UnixDate) + tfmt := time.Unix(prev/int64(time.Second), 0).UTC().Format(readDateLayout) ttok := strings.Split(strings.Replace(tfmt, " ", " ", 1), " ") hms := strings.Split(ttok[3], ":") - parser.maxDays = maxDays(intVal(months, ttok[1]), atoi(ttok[5])) + parser.maxDays = maxDays(intVal(months, ttok[1]), atoi(ttok[4])) second := parser.nextSeconds(atoi(hms[2]), fields[0]) minute := parser.nextMinutes(atoi(hms[1]), fields[1]) hour := parser.nextHours(atoi(hms[0]), fields[2]) - dayOfWeek, dayOfMonth := parser.nextDay(intVal(days, ttok[0]), fields[5], - atoi(strings.Replace(ttok[2], "_", "", 1)), fields[3]) + dayOfMonth := parser.nextDay(intVal(days, ttok[0]), fields[5], atoi(ttok[2]), fields[3]) month := parser.nextMonth(ttok[1], fields[4]) - year := parser.nextYear(ttok[5], fields[6]) + year := parser.nextYear(ttok[4], fields[6]) - nstr := fmt.Sprintf("%s %s %s %s:%s:%s %s %s", dayOfWeek, month, dayOfMonth, - hour, minute, second, ttok[4], year) - ntime, err := time.Parse(time.UnixDate, nstr) - nextTime = ntime.Unix() + nstr := fmt.Sprintf("%s %s %s:%s:%s %s", month, strconv.Itoa(dayOfMonth), + hour, minute, second, year) + ntime, err := time.Parse(writeDateLayout, nstr) + nextTime = ntime.UnixNano() return } //the ? wildcard is only used in the day of month and day of week fields func validateCronExpression(expression string) ([]*CronField, error) { var tokens []string - tokens = strings.Split(expression, " ") + if value, ok := special[expression]; ok { + tokens = strings.Split(value, " ") + } else { + tokens = strings.Split(expression, " ") + } length := len(tokens) if length < 6 || length > 7 { return nil, cronError("length") @@ -129,7 +148,7 @@ func validateCronExpression(expression string) ([]*CronField, error) { if length == 6 { tokens = append(tokens, "*") } - if (tokens[3] != "?" && tokens[3] != "*") && tokens[5] != "?" { + if (tokens[3] != "?" && tokens[3] != "*") && (tokens[5] != "?" && tokens[5] != "*") { return nil, cronError("day field set twice") } if tokens[6] != "*" { @@ -276,21 +295,19 @@ func (parser *CronExpressionParser) nextHours(prev int, field *CronField) string } func (parser *CronExpressionParser) nextDay(prevWeek int, weekField *CronField, - prevMonth int, monthField *CronField) (string, string) { - var nextMonth, nextWeek int + prevMonth int, monthField *CronField) int { + var nextMonth int if weekField.isEmpty() && monthField.isEmpty() && parser.lastSet(dayOfWeekIndex) { if parser.dayBump { nextMonth, parser.monthBump = bumpValue(prevMonth, parser.maxDays, 1) - nextWeek, _ = bumpLiteral(prevWeek, 6, 1) - return days[nextWeek], alignDigit(nextMonth, " ") + return nextMonth } - return days[prevWeek], alignDigit(prevMonth, " ") + return prevMonth } if len(monthField.values) > 0 { nextMonth, parser.monthBump = parser.findNextValue(prevMonth, monthField.values) parser.setDone(dayOfMonthIndex) - nextWeek, _ = bumpLiteral(prevWeek, 6, step(prevMonth, nextMonth, parser.maxDays)) - return days[nextWeek], alignDigit(nextMonth, " ") + return nextMonth } else if len(weekField.values) > 0 { nextWeek, bumpDayOfMonth := parser.findNextValue(prevWeek, weekField.values) parser.setDone(dayOfWeekIndex) @@ -301,16 +318,16 @@ func (parser *CronExpressionParser) nextDay(prevWeek int, weekField *CronField, _step = step(prevWeek, nextWeek, 7) } nextMonth, parser.monthBump = bumpValue(prevMonth, parser.maxDays, _step) - return days[nextWeek], alignDigit(nextMonth, " ") + return nextMonth } - return days[prevWeek], alignDigit(prevMonth, " ") + return prevMonth } func (parser *CronExpressionParser) nextMonth(prev string, field *CronField) string { var next int if field.isEmpty() && parser.lastSet(dayOfWeekIndex) { if parser.monthBump { - next, parser.yearBump = bumpLiteral(intVal(months, prev), 11, 1) + next, parser.yearBump = bumpLiteral(intVal(months, prev), 12, 1) return months[next] } return prev @@ -342,7 +359,7 @@ func bumpLiteral(iprev int, max int, step int) (int, bool) { if bumped%max == 0 { return iprev, true } - return (bumped % max) - 1, true + return (bumped % max), true } return bumped, false } diff --git a/quartz/cron_test.go b/quartz/cron_test.go index f44af93..f5299aa 100644 --- a/quartz/cron_test.go +++ b/quartz/cron_test.go @@ -6,7 +6,7 @@ import ( ) func TestCronExpression1(t *testing.T) { - prev := int64(1554120000) + prev := int64(1555351200000000000) result := "" cronTrigger, err := NewCronTrigger("10/20 15 14 5-10 * ? *") if err != nil { @@ -14,11 +14,11 @@ func TestCronExpression1(t *testing.T) { } else { result, _ = iterate(prev, cronTrigger, 1000) } - assertEqual(t, result, "Wed Nov 8 14:15:10 IST 2023") + assertEqual(t, result, "Fri Dec 8 14:15:10 2023") } func TestCronExpression2(t *testing.T) { - prev := int64(1554120000) + prev := int64(1555351200000000000) result := "" cronTrigger, err := NewCronTrigger("* 5,7,9 14-16 * * ? *") if err != nil { @@ -26,11 +26,11 @@ func TestCronExpression2(t *testing.T) { } else { result, _ = iterate(prev, cronTrigger, 1000) } - assertEqual(t, result, "Sun Jul 21 15:05:00 IDT 2019") + assertEqual(t, result, "Mon Aug 5 14:05:00 2019") } func TestCronExpression3(t *testing.T) { - prev := int64(1554120000) + prev := int64(1555351200000000000) result := "" cronTrigger, err := NewCronTrigger("* 5,7,9 14/2 * * Wed,Sat *") if err != nil { @@ -38,7 +38,7 @@ func TestCronExpression3(t *testing.T) { } else { result, _ = iterate(prev, cronTrigger, 1000) } - assertEqual(t, result, "Wed Nov 20 22:05:00 IST 2019") + assertEqual(t, result, "Sat Dec 7 14:05:00 2019") } func TestCronExpression4(t *testing.T) { @@ -50,7 +50,7 @@ func TestCronExpression4(t *testing.T) { } func TestCronExpression5(t *testing.T) { - prev := int64(1554120000) + prev := int64(1555351200000000000) result := "" cronTrigger, err := NewCronTrigger("* * * * * ? *") if err != nil { @@ -58,11 +58,11 @@ func TestCronExpression5(t *testing.T) { } else { result, _ = iterate(prev, cronTrigger, 1000) } - assertEqual(t, result, "Mon Apr 1 15:16:40 IDT 2019") + assertEqual(t, result, "Mon Apr 15 18:16:40 2019") } func TestCronExpression6(t *testing.T) { - prev := int64(1554120000) + prev := int64(1555351200000000000) result := "" cronTrigger, err := NewCronTrigger("* * 14/2 * * Mon/3 *") if err != nil { @@ -70,11 +70,11 @@ func TestCronExpression6(t *testing.T) { } else { result, _ = iterate(prev, cronTrigger, 1000) } - assertEqual(t, result, "Thu Mar 4 20:00:00 IST 2021") + assertEqual(t, result, "Mon Mar 15 18:00:00 2021") } func TestCronExpression7(t *testing.T) { - prev := int64(1554120000) + prev := int64(1555351200000000000) result := "" cronTrigger, err := NewCronTrigger("* 5-9 14/2 * * 0-2 *") if err != nil { @@ -82,7 +82,67 @@ func TestCronExpression7(t *testing.T) { } else { result, _ = iterate(prev, cronTrigger, 1000) } - assertEqual(t, result, "Tue Jul 2 14:09:00 IDT 2019") + assertEqual(t, result, "Tue Jul 16 16:09:00 2019") +} + +func TestCronYearly(t *testing.T) { + prev := int64(1555351200000000000) + result := "" + cronTrigger, err := NewCronTrigger("@yearly") + if err != nil { + t.Fatal(err) + } else { + result, _ = iterate(prev, cronTrigger, 100) + } + assertEqual(t, result, "Sun Jan 1 00:00:00 2119") +} + +func TestCronMonthly(t *testing.T) { + prev := int64(1555351200000000000) + result := "" + cronTrigger, err := NewCronTrigger("@monthly") + if err != nil { + t.Fatal(err) + } else { + result, _ = iterate(prev, cronTrigger, 100) + } + assertEqual(t, result, "Sun Aug 1 00:00:00 2027") +} + +func TestCronWeekly(t *testing.T) { + prev := int64(1555351200000000000) + result := "" + cronTrigger, err := NewCronTrigger("@weekly") + if err != nil { + t.Fatal(err) + } else { + result, _ = iterate(prev, cronTrigger, 100) + } + assertEqual(t, result, "Mon Mar 15 00:00:00 2021") +} + +func TestCronDaily(t *testing.T) { + prev := int64(1555351200000000000) + result := "" + cronTrigger, err := NewCronTrigger("@daily") + if err != nil { + t.Fatal(err) + } else { + result, _ = iterate(prev, cronTrigger, 1000) + } + assertEqual(t, result, "Sun Jan 9 00:00:00 2022") +} + +func TestCronHourly(t *testing.T) { + prev := int64(1555351200000000000) + result := "" + cronTrigger, err := NewCronTrigger("@hourly") + if err != nil { + t.Fatal(err) + } else { + result, _ = iterate(prev, cronTrigger, 1000) + } + assertEqual(t, result, "Wed May 29 06:00:00 2019") } func assertEqual(t *testing.T, a interface{}, b interface{}) { @@ -95,10 +155,10 @@ func iterate(prev int64, cronTrigger *CronTrigger, iterations int) (string, erro var err error for i := 0; i < iterations; i++ { prev, err = cronTrigger.NextFireTime(prev) + // fmt.Println(time.Unix(prev/int64(time.Second), 0).UTC().Format(readDateLayout)) if err != nil { return "", err } - // fmt.Println(time.Unix(prev, 0).Format(time.UnixDate)) } - return time.Unix(prev, 0).Format(time.UnixDate), nil + return time.Unix(prev/int64(time.Second), 0).UTC().Format(readDateLayout), nil } diff --git a/quartz/scheduler.go b/quartz/scheduler.go index 2a38306..7f6030a 100644 --- a/quartz/scheduler.go +++ b/quartz/scheduler.go @@ -36,7 +36,7 @@ func NewStdScheduler() *StdScheduler { } func (sched *StdScheduler) ScheduleJob(job Job, trigger Trigger) error { - nextRunTime, err := trigger.NextFireTime(time.Now().UnixNano()) + nextRunTime, err := trigger.NextFireTime(time.Now().UTC().UnixNano()) if err == nil { sched.feeder <- &Item{ job, @@ -132,7 +132,7 @@ func (sched *StdScheduler) startFeedReader() { } func parkTime(ts int64) int64 { - now := time.Now().UnixNano() + now := time.Now().UTC().UnixNano() if ts > now { return ts - now } diff --git a/quartz/util.go b/quartz/util.go index e7018a3..ec8be8e 100644 --- a/quartz/util.go +++ b/quartz/util.go @@ -116,3 +116,11 @@ func isLeapYear(year int) bool { } return true } + +func dayOfTheWeek(y int, m int, d int) string { + t := []int{0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4} + if m < 3 { + y-- + } + return days[((y + y/4 - y/100 + y/400 + t[m-1] + d) % 7)] +}