Skip to content
Jennifer Programming Language

cron - cron schedules

Import with import "cron.j" as cron;. Parse and evaluate cron expressions - the five-field minute hour day-of-month month day-of-week spec. parse builds a Schedule, matches tests whether a time.Time fires it, and next finds the next fire at or after a time. A pure calculator over time - no clock, no sleeping - so it runs on both binaries; a real scheduler is your own spawn + time.sleep loop over cron.next.

jennifer
import "cron.j" as cron;

def s as cron.Schedule init cron.parse("30 9 * * 1-5");   # 09:30 on weekdays
def fire as time.Time init cron.next($s, time.now());
io.printf("next run: %s\n", time.iso($fire));

Runnable: examples/modules/cron_demo.j.

Functions

CallReturnsNotes
cron.parse(expr)ScheduleParse a five-field expression.
cron.matches(schedule, t)boolDoes the schedule fire at t? (minute granularity - seconds are ignored).
cron.next(schedule, after)time.TimeThe next fire at or after after, keeping its zone offset.

parse returns a Schedule:

jennifer
def struct cron.Schedule {
    minutes as list of int, hours as list of int, daysOfMonth as list of int,
    months as list of int, weekdays as list of int,
    domStar as bool, dowStar as bool, reboot as bool
};

The five list fields hold the matching values for each cron field; domStar / dowStar record whether the day-of-month / day-of-week field was * (driving the either/both rule below), and reboot is true for an @reboot schedule.

Fields

Five whitespace-separated fields, each with the usual operators:

FieldRange
minute0-59
hour0-23
day of month1-31
month1-12
day of week0-70 and 7 are both Sunday

Each field accepts * (every value), a single number, an a-b range, an a,b,c list, and a /n step - on a wildcard (every nth value), a range (a-b/n), or a value (a/n, meaning a to the field maximum). Examples:

ExpressionFires
* * * * *every minute
*/15 * * * *every 15 minutes
0 9 * * 1-509:00 on weekdays (Mon-Fri)
0 0 1 * *midnight on the 1st of each month
30 3 * * 003:30 on Sundays
0 0 13 * 5midnight on Friday the 13th (see below)

Named months and weekdays

The month field also accepts the three-letter names JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC (as 1-12) and the day-of-week field accepts SUN MON TUE WED THU FRI SAT (as 0-6). Names are case-insensitive (jan, Jan, JAN all work) and can appear anywhere a number can - single values, ranges, and lists:

ExpressionSame as
0 9 * * MON-FRI0 9 * * 1-5 (09:00 on weekdays)
0 0 1 JAN,JUL *0 0 1 1,7 * (1st of January and July)
30 3 * * SUN30 3 * * 0 (03:30 on Sundays)

Like the numeric 0, SUN is Sunday (there is no name for the alternate 7).

Nickname macros

A whole expression may instead be one of the standard @ nicknames, which expand to a five-field expression before parsing:

NicknameExpands toFires
@yearly, @annually0 0 1 1 *midnight on January 1st
@monthly0 0 1 * *midnight on the 1st of each month
@weekly0 0 * * 0midnight each Sunday
@daily, @midnight0 0 * * *midnight every day
@hourly0 * * * *the top of every hour

Nicknames are case-insensitive. An unknown @name throws a catchable Error (kind "cron").

@reboot

@reboot means "at startup", so it has no time-based schedule. It parses to a Schedule whose reboot field is true, and that flag changes the two evaluators:

  • cron.matches($s, t) is always false for a @reboot schedule (no clock time ever fires it).
  • cron.next($s, after) throws a catchable Error (kind "cron") - there is no next fire time.

Test for it yourself and run the job once at program start:

jennifer
def s as cron.Schedule init cron.parse("@reboot");
if ($s.reboot) {
    runJob();   # fire once, at startup
}

The day-of-month / day-of-week rule

When both the day-of-month and day-of-week fields are restricted (neither is *), a day matching either one fires - the standard cron behavior. So 0 0 13 * 5 fires on the 13th and on every Friday. When one of the two is *, only the other constrains the day.

next

cron.next(schedule, after) returns the first matching minute at or after after (with its seconds zeroed), preserving the input's zone offset. If after already sits exactly on a matching minute, it is returned. The search skips non-matching days whole (so a yearly schedule is found quickly) and gives up after a five-year horizon - an impossible schedule (e.g. 0 0 31 2 *, February 31st) throws a catchable Error (kind "cron") rather than looping forever.

Zones are fixed-offset (as in the time library), so next does no DST arithmetic.

Scope

  • Standard five fields, plus named months / weekdays and the @ nickname macros (including @reboot) above. No seconds field and no non-standard extensions (L, W, #, ?).
  • A calculator, not a runner. It never touches the clock. Drive it yourself: time.sleep(time.sub(cron.next($s, time.now()), time.now())), then run the job.

See also