changes as suggested at https://groups.google.com/d/msg/golang-nuts/la6mB5xG1L4/5V0L2GqF_1oJ
This commit is contained in:
parent
1998268a1c
commit
718fc4c0ab
|
@ -6,7 +6,7 @@ In another project, I decided to use cron expression syntax to encode scheduling
|
||||||
|
|
||||||
The time-matching algorithm in this implementation is efficient, it avoids as much as possible to guess the next matching time stamp, a common technique seen in a number of implementations out there.
|
The time-matching algorithm in this implementation is efficient, it avoids as much as possible to guess the next matching time stamp, a common technique seen in a number of implementations out there.
|
||||||
|
|
||||||
There is also a companion command-line utility to evaluate cron time expressions: <https://github.com/gorhill/cronexprdo>.
|
There is also a companion command-line utility to evaluate cron time expressions: <https://github.com/gorhill/cronexprdo> (which of course uses this library).
|
||||||
|
|
||||||
Implementation
|
Implementation
|
||||||
--------------
|
--------------
|
||||||
|
|
75
cronexpr.go
75
cronexpr.go
|
@ -45,7 +45,7 @@ type Expression struct {
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
// Returns a new Expression pointer. It expects a well-formed cron expression.
|
// Parse returns a new Expression pointer. It expects a well-formed cron expression.
|
||||||
// If a malformed cron expression is supplied, the result is undefined. See
|
// If a malformed cron expression is supplied, the result is undefined. See
|
||||||
// <https://github.com/gorhill/cronexpr#implementation> for documentation
|
// <https://github.com/gorhill/cronexpr#implementation> for documentation
|
||||||
// about what is a well-formed cron expression from this library point of view.
|
// about what is a well-formed cron expression from this library point of view.
|
||||||
|
@ -98,11 +98,14 @@ func Parse(cronLine string) *Expression {
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
// Given a time stamp `fromTime`, return the closest following time stamp which
|
// Next returns the closest time instant immediately following `fromTime` which
|
||||||
// matches the cron expression `expr`. The `time.Location` of the returned
|
// matches the cron expression `expr`.
|
||||||
// time stamp is the same as `fromTime`.
|
|
||||||
//
|
//
|
||||||
// A nil time.Time object is returned if no matching time stamp exists.
|
// The `time.Location` of the returned time instant is the same as that of
|
||||||
|
// `fromTime`.
|
||||||
|
//
|
||||||
|
// The zero value of time.Time is returned if no matching time instant exists
|
||||||
|
// or if a `fromTime` is itself a zero value.
|
||||||
func (expr *Expression) Next(fromTime time.Time) time.Time {
|
func (expr *Expression) Next(fromTime time.Time) time.Time {
|
||||||
// Special case
|
// Special case
|
||||||
if fromTime.IsZero() {
|
if fromTime.IsZero() {
|
||||||
|
@ -185,29 +188,31 @@ func (expr *Expression) Next(fromTime time.Time) time.Time {
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
// Given a time stamp `fromTime`, return a slice of `n` closest following time
|
// NextN return a slice of `n` closest time instants immediately following
|
||||||
// stamps which match the cron expression `expr`. The time stamps in the
|
// `fromTime` which match the cron expression `expr`.
|
||||||
// returned slice are in chronological ascending order. The `time.Location` of
|
|
||||||
// the returned time stamps is the same as `fromTime`.
|
|
||||||
//
|
//
|
||||||
// A slice with less than `n` entries (up to zero) is returned if not
|
// The time instants in the returned slice are in chronological ascending order.
|
||||||
// enough existing matching time stamps which exist.
|
// The `time.Location` of the returned time instants is the same as that of
|
||||||
func (expr *Expression) NextN(fromTime time.Time, n int) []time.Time {
|
// `fromTime`.
|
||||||
if n <= 0 {
|
//
|
||||||
panic("Expression.NextN(): invalid count")
|
// A slice with len between [0-`n`] is returned, that is, if not enough existing
|
||||||
}
|
// matching time instants exist, the number of returned entries will be less
|
||||||
|
// than `n`.
|
||||||
|
func (expr *Expression) NextN(fromTime time.Time, n uint) []time.Time {
|
||||||
nextTimes := make([]time.Time, 0)
|
nextTimes := make([]time.Time, 0)
|
||||||
fromTime = expr.Next(fromTime)
|
if n > 0 {
|
||||||
for {
|
fromTime = expr.Next(fromTime)
|
||||||
if fromTime.IsZero() {
|
for {
|
||||||
break
|
if fromTime.IsZero() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
nextTimes = append(nextTimes, fromTime)
|
||||||
|
n -= 1
|
||||||
|
if n == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
fromTime = expr.nextSecond(fromTime)
|
||||||
}
|
}
|
||||||
nextTimes = append(nextTimes, fromTime)
|
|
||||||
n -= 1
|
|
||||||
if n == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fromTime = expr.nextSecond(fromTime)
|
|
||||||
}
|
}
|
||||||
return nextTimes
|
return nextTimes
|
||||||
}
|
}
|
||||||
|
@ -297,7 +302,7 @@ func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int {
|
||||||
actualDaysOfMonthMap[lastDayOfMonth] = true
|
actualDaysOfMonthMap[lastDayOfMonth] = true
|
||||||
}
|
}
|
||||||
// Days of month
|
// Days of month
|
||||||
for v, _ := range expr.daysOfMonth {
|
for v := range expr.daysOfMonth {
|
||||||
// Ignore days beyond end of month
|
// Ignore days beyond end of month
|
||||||
if v <= lastDayOfMonth {
|
if v <= lastDayOfMonth {
|
||||||
actualDaysOfMonthMap[v] = true
|
actualDaysOfMonthMap[v] = true
|
||||||
|
@ -305,7 +310,7 @@ func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int {
|
||||||
}
|
}
|
||||||
// Work days of month
|
// Work days of month
|
||||||
// As per Wikipedia: month boundaries are not crossed.
|
// As per Wikipedia: month boundaries are not crossed.
|
||||||
for v, _ := range expr.workdaysOfMonth {
|
for v := range expr.workdaysOfMonth {
|
||||||
// Ignore days beyond end of month
|
// Ignore days beyond end of month
|
||||||
if v <= lastDayOfMonth {
|
if v <= lastDayOfMonth {
|
||||||
// If saturday, then friday
|
// If saturday, then friday
|
||||||
|
@ -335,7 +340,7 @@ func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int {
|
||||||
// offset : (7 - day_of_week_of_1st_day_of_month)
|
// offset : (7 - day_of_week_of_1st_day_of_month)
|
||||||
// target : 1 + (7 * week_of_month) + (offset + day_of_week) % 7
|
// target : 1 + (7 * week_of_month) + (offset + day_of_week) % 7
|
||||||
for w := 0; w <= 4; w += 1 {
|
for w := 0; w <= 4; w += 1 {
|
||||||
for v, _ := range expr.daysOfWeek {
|
for v := range expr.daysOfWeek {
|
||||||
v := 1 + w*7 + (offset+v)%7
|
v := 1 + w*7 + (offset+v)%7
|
||||||
if v <= lastDayOfMonth {
|
if v <= lastDayOfMonth {
|
||||||
actualDaysOfMonthMap[v] = true
|
actualDaysOfMonthMap[v] = true
|
||||||
|
@ -345,7 +350,7 @@ func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int {
|
||||||
// days of week of specific week in the month
|
// days of week of specific week in the month
|
||||||
// offset : (7 - day_of_week_of_1st_day_of_month)
|
// offset : (7 - day_of_week_of_1st_day_of_month)
|
||||||
// target : 1 + (7 * week_of_month) + (offset + day_of_week) % 7
|
// target : 1 + (7 * week_of_month) + (offset + day_of_week) % 7
|
||||||
for v, _ := range expr.specificWeekDaysOfWeek {
|
for v := range expr.specificWeekDaysOfWeek {
|
||||||
v := 1 + 7*(v/7) + (offset+v)%7
|
v := 1 + 7*(v/7) + (offset+v)%7
|
||||||
if v <= lastDayOfMonth {
|
if v <= lastDayOfMonth {
|
||||||
actualDaysOfMonthMap[v] = true
|
actualDaysOfMonthMap[v] = true
|
||||||
|
@ -354,7 +359,7 @@ func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int {
|
||||||
// Last days of week of the month
|
// Last days of week of the month
|
||||||
lastWeekOrigin := timeOrigin.AddDate(0, 1, -7)
|
lastWeekOrigin := timeOrigin.AddDate(0, 1, -7)
|
||||||
offset = 7 - int(lastWeekOrigin.Weekday())
|
offset = 7 - int(lastWeekOrigin.Weekday())
|
||||||
for v, _ := range expr.lastWeekDaysOfWeek {
|
for v := range expr.lastWeekDaysOfWeek {
|
||||||
v := lastWeekOrigin.Day() + (offset+v)%7
|
v := lastWeekOrigin.Day() + (offset+v)%7
|
||||||
if v <= lastDayOfMonth {
|
if v <= lastDayOfMonth {
|
||||||
actualDaysOfMonthMap[v] = true
|
actualDaysOfMonthMap[v] = true
|
||||||
|
@ -510,7 +515,7 @@ func cronNormalize(cronLine string) string {
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
func (expr *Expression) dayofweekFieldParse(cronField string) error {
|
func (expr *Expression) dayofweekFieldParse(cronField string) {
|
||||||
// Defaults
|
// Defaults
|
||||||
expr.daysOfWeekRestricted = true
|
expr.daysOfWeekRestricted = true
|
||||||
expr.lastWeekDaysOfWeek = make(map[int]bool)
|
expr.lastWeekDaysOfWeek = make(map[int]bool)
|
||||||
|
@ -564,13 +569,11 @@ func (expr *Expression) dayofweekFieldParse(cronField string) error {
|
||||||
v := atoi(s) % 7
|
v := atoi(s) % 7
|
||||||
populateOne(expr.daysOfWeek, v)
|
populateOne(expr.daysOfWeek, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
func (expr *Expression) dayofmonthFieldParse(cronField string) error {
|
func (expr *Expression) dayofmonthFieldParse(cronField string) {
|
||||||
// Defaults
|
// Defaults
|
||||||
expr.daysOfMonthRestricted = true
|
expr.daysOfMonthRestricted = true
|
||||||
expr.lastDayOfMonth = false
|
expr.lastDayOfMonth = false
|
||||||
|
@ -615,8 +618,6 @@ func (expr *Expression) dayofmonthFieldParse(cronField string) error {
|
||||||
// single value
|
// single value
|
||||||
populateOne(expr.daysOfMonth, atoi(s))
|
populateOne(expr.daysOfMonth, atoi(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
@ -691,7 +692,7 @@ func populateMany(values map[int]bool, min, max, step int) {
|
||||||
func toList(set map[int]bool) []int {
|
func toList(set map[int]bool) []int {
|
||||||
list := make([]int, len(set))
|
list := make([]int, len(set))
|
||||||
i := 0
|
i := 0
|
||||||
for k, _ := range set {
|
for k := range set {
|
||||||
list[i] = k
|
list[i] = k
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,8 @@ package cronexpr_test
|
||||||
/******************************************************************************/
|
/******************************************************************************/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"cronexpr"
|
"github.com/gorhill/cronexpr"
|
||||||
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
@ -126,11 +127,16 @@ func TestZero(t *testing.T) {
|
||||||
from, _ := time.Parse("2006-01-02", "2013-08-31")
|
from, _ := time.Parse("2006-01-02", "2013-08-31")
|
||||||
next := cronexpr.Parse("* * * * * 1980").Next(from)
|
next := cronexpr.Parse("* * * * * 1980").Next(from)
|
||||||
if next.IsZero() == false {
|
if next.IsZero() == false {
|
||||||
t.Error(`("* * * * * 1980").Next("2013-08-31") returned 'false', expected 'true'`)
|
t.Error(`("* * * * * 1980").Next("2013-08-31").IsZero() returned 'false', expected 'true'`)
|
||||||
}
|
}
|
||||||
|
|
||||||
next = cronexpr.Parse("* * * * * 2050").Next(from)
|
next = cronexpr.Parse("* * * * * 2050").Next(from)
|
||||||
if next.IsZero() == true {
|
if next.IsZero() == true {
|
||||||
t.Error(`("* * * * * 2050").Next("2013-08-31") returned 'true', expected 'false'`)
|
t.Error(`("* * * * * 2050").Next("2013-08-31").IsZero() returned 'true', expected 'false'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
next = cronexpr.Parse("* * * * * 2099").Next(time.Time{})
|
||||||
|
if next.IsZero() == false {
|
||||||
|
t.Error(`("* * * * * 2014").Next(time.Time{}).IsZero() returned 'true', expected 'false'`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*!
|
||||||
|
* Copyright 2013 Raymond Hill
|
||||||
|
*
|
||||||
|
* Project: github.com/gorhill/example_test.go
|
||||||
|
* File: example_test.go
|
||||||
|
* Version: 1.0
|
||||||
|
* License: GPL v3 see <https://www.gnu.org/licenses/gpl.html>
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cronexpr_test
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gorhill/cronexpr"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
// ExampleParse
|
||||||
|
func ExampleParse() {
|
||||||
|
t := time.Date(2013, time.August, 31, 0, 0, 0, 0, time.UTC)
|
||||||
|
nextTimes := cronexpr.Parse("0 0 29 2 *").NextN(t, 5)
|
||||||
|
for i := range nextTimes {
|
||||||
|
fmt.Println(nextTimes[i].Format(time.RFC1123))
|
||||||
|
// Output:
|
||||||
|
// Mon, 29 Feb 2016 00:00:00 UTC
|
||||||
|
// Sat, 29 Feb 2020 00:00:00 UTC
|
||||||
|
// Thu, 29 Feb 2024 00:00:00 UTC
|
||||||
|
// Tue, 29 Feb 2028 00:00:00 UTC
|
||||||
|
// Sun, 29 Feb 2032 00:00:00 UTC
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue