Code Monkey home page Code Monkey logo

cronexpr's Introduction

Golang Cron expression parser

Given a cron expression and a time stamp, you can get the next time stamp which satisfies the cron expression.

In another project, I decided to use cron expression syntax to encode scheduling information. Thus this standalone library to parse and apply time stamps to cron expressions.

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/cronexpr/tree/master/cronexpr (which of course uses this library).

Implementation

The reference documentation for this implementation is found at https://en.wikipedia.org/wiki/Cron#CRON_expression, which I copy/pasted here (laziness!) with modifications where this implementation differs:

Field name     Mandatory?   Allowed values    Allowed special characters
----------     ----------   --------------    --------------------------
Seconds        No           0-59              * / , -
Minutes        Yes          0-59              * / , -
Hours          Yes          0-23              * / , -
Day of month   Yes          1-31              * / , - L W
Month          Yes          1-12 or JAN-DEC   * / , -
Day of week    Yes          0-6 or SUN-SAT    * / , - L #
Year           No           1970–2099         * / , -

Asterisk ( * )

The asterisk indicates that the cron expression matches for all values of the field. E.g., using an asterisk in the 4th field (month) indicates every month.

Slash ( / )

Slashes describe increments of ranges. For example 3-59/15 in the minute field indicate the third minute of the hour and every 15 minutes thereafter. The form */... is equivalent to the form "first-last/...", that is, an increment over the largest possible range of the field.

Comma ( , )

Commas are used to separate items of a list. For example, using MON,WED,FRI in the 5th field (day of week) means Mondays, Wednesdays and Fridays.

Hyphen ( - )

Hyphens define ranges. For example, 2000-2010 indicates every year between 2000 and 2010 AD, inclusive.

L

L stands for "last". When used in the day-of-week field, it allows you to specify constructs such as "the last Friday" (5L) of a given month. In the day-of-month field, it specifies the last day of the month.

W

The W character is allowed for the day-of-month field. This character is used to specify the business day (Monday-Friday) nearest the given day. As an example, if you were to specify 15W as the value for the day-of-month field, the meaning is: "the nearest business day to the 15th of the month."

So, if the 15th is a Saturday, the trigger fires on Friday the 14th. If the 15th is a Sunday, the trigger fires on Monday the 16th. If the 15th is a Tuesday, then it fires on Tuesday the 15th. However if you specify 1W as the value for day-of-month, and the 1st is a Saturday, the trigger fires on Monday the 3rd, as it does not 'jump' over the boundary of a month's days.

The W character can be specified only when the day-of-month is a single day, not a range or list of days.

The W character can also be combined with L, i.e. LW to mean "the last business day of the month."

Hash ( # )

# is allowed for the day-of-week field, and must be followed by a number between one and five. It allows you to specify constructs such as "the second Friday" of a given month.

Predefined cron expressions

(Copied from https://en.wikipedia.org/wiki/Cron#Predefined_scheduling_definitions, with text modified according to this implementation)

Entry       Description                                                             Equivalent to
@annually   Run once a year at midnight in the morning of January 1                 0 0 0 1 1 * *
@yearly     Run once a year at midnight in the morning of January 1                 0 0 0 1 1 * *
@monthly    Run once a month at midnight in the morning of the first of the month   0 0 0 1 * * *
@weekly     Run once a week at midnight in the morning of Sunday                    0 0 0 * * 0 *
@daily      Run once a day at midnight                                              0 0 0 * * * *
@hourly     Run once an hour at the beginning of the hour                           0 0 * * * * *
@reboot     Not supported

Other details

  • If only six fields are present, a 0 second field is prepended, that is, * * * * * 2013 internally become 0 * * * * * 2013.
  • 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

Install

go get github.com/gorhill/cronexpr

Usage

Import the library:

import "github.com/gorhill/cronexpr"
import "time"

Simplest way:

nextTime := cronexpr.MustParse("0 0 29 2 *").Next(time.Now())

Assuming time.Now() is "2013-08-29 09:28:00", then nextTime will be "2016-02-29 00:00:00".

You can keep the returned Expression pointer around if you want to reuse it:

expr := cronexpr.MustParse("0 0 29 2 *")
nextTime := expr.Next(time.Now())
...
nextTime = expr.Next(nextTime)

Use time.IsZero() to find out whether a valid time was returned. For example,

cronexpr.MustParse("* * * * * 1980").Next(time.Now()).IsZero()

will return true, whereas

cronexpr.MustParse("* * * * * 2050").Next(time.Now()).IsZero()

will return false (as of 2013-08-29...)

You may also query for n next time stamps:

cronexpr.MustParse("0 0 29 2 *").NextN(time.Now(), 5)

which returns a slice of time.Time objects, containing the following time stamps (as of 2013-08-30):

2016-02-29 00:00:00
2020-02-29 00:00:00
2024-02-29 00:00:00
2028-02-29 00:00:00
2032-02-29 00:00:00

The time zone of time values returned by Next and NextN is always the time zone of the time value passed as argument, unless a zero time value is returned.

API

http://godoc.org/github.com/gorhill/cronexpr

License

License: pick the one which suits you best:

cronexpr's People

Contributors

dadgar avatar gorhill avatar safx avatar takumakanari avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cronexpr's Issues

timezone related error on DST on Next()

step to reproduce:
1.get first schedule time
schedule= 2 02 * * * *
fromTime=2016-03-12 02:03:00 -0800 PST
call Next() return: 2016-03-13T01:02:00-08:00
2. get next schedule time.
use the Next() return time as new fromTime: 2016-03-13T01:02:00-08:00
call Next() again ,and return the same value as new fromTime: 2016-03-13T01:02:00-08:00

  1. repeat step 2 and infinity loop

Wrong calculation on next schedule on cron expression: "*/5 * * * *"

cronExpression= "*/5 * * * *"<-- this is every 5 minutes
prints out from log:
now=2017-03-25 15:38:11.667843072 -0700 PDT next_schedule=[2017-03-25 15:40:00 -0700 PDT] sec_diff=109" <-- 109 secs?

e, err = cronexpr.Parse(aws.StringValue(cronExpression))
base := time.Now()
next := e.NextN(base, 1)
// normalize before subtraction to Unix Time
d := int(next[0].Unix() - base.Unix())
logging.Infof("now=%s next_schedule=%s sec_diff=%d", base, next, d)

multi-goroutine map write when init first time

func makeLayoutRegexp(layout, value string) *regexp.Regexp {
	layout = strings.Replace(layout, `%value%`, value, -1)
	re := layoutRegexp[layout]
	if re == nil {
		re = regexp.MustCompile(layout)
		layoutRegexp[layout] = re
	}
	return re
}

This code will panic when multi-goroutine calls cronexp.Parse(xx) concurrently.

call stack:

fatal error: concurrent map writes

goroutine 33 [running]:
runtime.throw(0xdc8dbf, 0x15)
        /home/work/build_tmp/build/1480301702.549625122/xxxx/go/src/runtime/panic.go:566 +0x95 fp=0xc42005d920 sp=0xc42005d900
runtime.mapassign1(0xca0ae0, 0xc4201c72f0, 0xc42005da60, 0xc42005da58)
        /home/work/build_tmp/build/1480301702.549625122/xxxx/go/src/runtime/hashmap.go:458 +0x8ef fp=0xc42005da08 sp=0xc42005d920
github.com/gorhill/cronexpr.makeLayoutRegexp(0xdbeb61, 0x9, 0xdc5f55, 0x12, 0x0)
        /home/work/build_tmp/build/1480301702.549625122/xxxx/gopath/src/github.com/gorhill/cronexpr/cronexpr_parse.go:486 +0x141 fp=0xc42005da80 sp=0xc42005da08
github.com/gorhill/cronexpr.genericFieldParse(0xc420362c00, 0x1, 0xdbbdd6, 0x6, 0x0, 0x3b, 0x120ac80, 0x3c, 0x3c, 0xdc5f55, ...)
        /home/work/build_tmp/build/1480301702.549625122/xxxx/gopath/src/github.com/gorhill/cronexpr/cronexpr_parse.go:417 +0x22b fp=0xc42005db70 sp=0xc42005da80
github.com/gorhill/cronexpr.genericFieldHandler(0xc420362c00, 0x1, 0xdbbdd6, 0x6, 0x0, 0x3b, 0x120ac80, 0x3c, 0x3c, 0xdc5f55, ...)
        /home/work/build_tmp/build/1480301702.549625122/xxxx/gopath/src/github.com/gorhill/cronexpr/cronexpr_parse.go:268 +0x70 fp=0xc42005dcf8 sp=0xc42005db70
github.com/gorhill/cronexpr.(*Expression).secondFieldHandler(0xc4201380e0, 0xc420362c00, 0x1, 0xf, 0xc42149f3b0)
        /home/work/build_tmp/build/1480301702.549625122/xxxx/gopath/src/github.com/gorhill/cronexpr/cronexpr_parse.go:213 +0x6c fp=0xc42005dda0 sp=0xc42005dcf8
github.com/gorhill/cronexpr.Parse(0xc421315fa0, 0xe, 0x40f3bb, 0xc4202e9f78, 0x0)
        /home/work/build_tmp/build/1480301702.549625122/xxxx/gopath/src/github.com/gorhill/cronexpr/cronexpr.go:90 +0x16b fp=0xc42005de28 sp=0xc42005dda0

I will try to workaround this by call cronexp.Parse() when first start(no other goroutine init). :-) and maybe try to send a pull request.

Add package comment

There's no package-level documentation: http://godoc.org/github.com/gorhill/cronexpression

At a minimum, the package comment should include a first sentence synopsis.

// Package cronexpression parses cron time expressions.
package conexpression

Edit: There's a lot of doc in the readme that should probably pulled into the code so that it's readable in various Go tools.

Improve performance where obvious

Current benchmarking results on Kernel Linux 3.5.0-17-generic i5-3570K:

BenchmarkParse   50000    42706 ns/op
BenchmarkNext   100000    18948 ns/op

For fields which commonly default to "*", it should be worth to avoid constructing array of valid values.

Why using panic?

Why using panic instead of return error? because it will break all application if MustParse function got error.

func MustParse(cronLine string) *Expression {
    expr, err := Parse(cronLine)
    if err != nil {
        panic(err)
    }
    return expr
}

CMIIW, I prefer like this.

func MustParse(cronLine string) (*Expression, error) {
    expr, err := Parse(cronLine)
    if err != nil {
        return nil, err 
    }
    return expr,nil
}

Return Error Rather than Panic

Would it be possible to change the cronexpr.MustParse() function to return an error rather than panic? If so, I'll gladly submit a PR. Thanks!

the DST change on pull 17 cause some new error

on this commit, I found a new error.
seems all the job scheduled on DST day will be scheduled at 3am (fall early). i assume it should be just that missing our jobs fall to early time.
e.g
schedule: 0 14 * * * * timezone: America/Los_Angeles fromTime: 2016-03-12 14:01:00 -0800 PST
reschedule to: 2016-03-13T03:00:00-07:00
in the master version, it is set correct to 2016-03-13T14:00:00-07:00

Previous command

I'd love to be able to step backwards to see the previous time a cron has fired.

Support something like RANDOM_DELAY

anacron supports looking at the RANDOM_DELAY environment variable and randomly adding between 1 and the value of RANDOM_DELAY minutes to the execute time to help stagger the start of jobs. It would be awesome if cronexpr supported something similar either by looking at the RANDOM_DELAY environment variable OR having something that could be set through the library itself.

More here- http://linux.die.net/man/5/anacrontab

I would be more than happy to implement this w/tests if the PR would be welcome.

leading zero are not accepted

most cron implementations authorize the use of leading zeros to improve readbility (and alignment).
When using a task in the form 01 * * * *, cronexpr complains about bad crontab line: 01 * * * *…
If I remove the leading zero, it works as expected.

Note : I'm using cronexpr through aptible/supercronic
i hope supercronic is not the culprit.

Could the `expression` field of `Expression` structs be readable somehow?

e.g. via a GetCronLine() method or similar

Even though the constructor of an Expression struct necessarily knows the raw cron expression, it would be useful when passing things around to have just the one struct rather than the struct + the cron expression string, in cases where this is necessary to do.

Day-of-week parse support for MON-SUN (or `1-7`)

cronexpr.Parse() correctly parses dow (day-of-week) when written as 0-6 or SUN-SAT – but does NOT correctly parse when written as MON-SUN or 1-7

We can see that 0 and 7 both resolve to Sunday, so it should be possible to write expressions as either 0-6 OR 1-7 and have the same result. However- the 1-7 day-of-week does not parse.

Here is an example / reproduction scenario

package main

import (
	"fmt"
	"time"

	"github.com/gorhill/cronexpr"
)

func main() {
	t := time.Date(2017, time.July, 18, 13, 47, 0, 0, time.UTC)

	for _, dow := range []string{"MON-SUN", "SUN-SAT", "0-6", "1-7"} {
		times := cronexpr.MustParse(fmt.Sprintf("* *  * * %s", dow)).NextN(t, 8)

		if len(times) == 0 {
			fmt.Printf("Could NOT parse [%s]\n", dow)
		} else {
			for i := range times {
				fmt.Println(times[i].Format(time.RFC1123))
			}
		}

	}

}

Hyphenated hours

Hello, tracing a bug via Nomad (which uses this library), I've discovered that the following expression causes a nil pointer exception:

1 15-0 * * 1-5

Digging into the code, it's this:

func (expr *Expression) hourFieldHandler(s string) error {
	var err error
	expr.hourList, err = genericFieldHandler(s, hourDescriptor)
        fmt.Println(genericFieldHandler(s, hourDescriptor))
	return err
}

It produces an empty list when trying to parse 15-0:

func (expr *Expression) hourFieldHandler(s string) error {
	var err error
	expr.hourList, err = genericFieldHandler(s, hourDescriptor)
        fmt.Printf("hour: %v\tparsed: %v\n", s, expr.hourList)
	return err
}

hour: 15-0 parsed: [] - this leads to expr.hourList[0] producing a nil pointer over here: https://github.com/gorhill/cronexpr/blob/master/cronexpr_next.go#L115

Is this syntax supposed to be supported? Your doc says hyphens are.

Here's the stacktrace:

panic: runtime error: index out of range [recovered]
        panic: runtime error: index out of range

goroutine 5 [running]:
testing.tRunner.func1(0xc4200bc0f0)
        /usr/local/go/src/testing/testing.go:711 +0x2d2
panic(0x5281e0, 0x5fe0c0)
        /usr/local/go/src/runtime/panic.go:491 +0x283
github.com/hashicorp/nomad/vendor/github.com/gorhill/cronexpr.(*Expression).nextDayOfMonth(0xc4200c8000, 0xbeb0a5b636ed7f9b, 0x592b5, 0x605ae0, 0x0, 0x0, 0x0)
        /home/shanssian/go/src/github.com/hashicorp/nomad/vendor/github.com/gorhill/cronexpr/cronexpr_next.go:118 +0x2be

Add a func to validate whether a cron expression is valid

For convenience, developer might want to check whether an externally supplied cron expression is valid given the currently supported cron syntax.

This func will not be called automatically internally at the entry point CronExpression.NextTime(), the choice will be left to the developer as to whether he/she wants to incur the overhead cost of validation.

Several parse error

I found these parse error.

syntax error in minute field: 'type'
syntax error in minute field: '['
syntax error in minute field: 'if'
syntax error in minute field: 'SOURCES_PREAMBLE="###'
syntax error in minute field: 'cd'
syntax error in minute field: 'egrep'

Unexported fields in `type Expression`

Hi,

I'm new to the world of GoLang, so this may be a unusual request but I wonder whether we can make the fields within type Expression exported.

I actually need to analyse a cron by stepping over the arrays of accepted values this library has generated to determine whether, given a duration, we're still within a time period.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.