Skip to content

A javascript library for defining recurring schedules and calculating future (or past) occurrences for them. Includes support for using English phrases and Cron schedules. Works in Node and in the browser.

xqliu/later

 
 

Repository files navigation

Later is a simple library for describing recurring schedules and calculating their future occurrences. It supports a very flexible schedule definition including support for composite schedules and schedule exceptions. Later also supports executing a callback on a provided schedule.

There are four ways that schedules can be defined: using the chainable Recur api, using an English expression, using a Cron expression, or they can also be manually defined. Later works in both the browser and node and the core engine for calculating schedules is only 1.3k minified and compressed.

Primary Goal

The primary goal of Later is to produce deterministic schedules. These are schedules that can be calculated at any time and will always produce the same results provided the same start time. This is important because it means that the schedule can be stored and instances dynamically calculated as needed instead of having to store previous instances to calculate future occurrences.

For example, a schedule such as every 10 mins on Friday is deterministic. A schedule such as after 5 mins is not deterministic. You would have to know the previous occurrence to figure out the next occurrence. While these types of schedules are supported, it is not the primary focus of Later.

Example uses of Later schedules:

  • Run a report on the last day of every month at 12 AM except in December
  • Install patches on the 2nd Tuesday of every month at 4 AM
  • Gather CPU metrics every 10 mins Mon - Fri and every 30 mins Sat - Sun
  • Send out a scary e-mail at 13:13:13 every Friday the 13th

Schedules that Later supports but for which it is probably overkill:

  • Run a task after 5 minutes from the start time

Node Example

var recur = require('later').recur
  , cron = require('later').cronParser
  , text = require('later').enParser
  , later = require('later').later
  , rSched, cSched, tSched, mSched, aSched, results;

// equivalent schedules for every 5 minutes on the hour
rSched = recur().every(5).minute();
cSched = cron().parse('* */5 * * * *', true);
tSched = text().parse('every 5 minutes');
mSched = {schedules: [ {m: [0,5,10,15,20,25,30,35,40,45,50,55]}]};

// schedule for every 5 minutes from the start time
aSched = text().parse('after 5 minutes');
aSched = recur().after(5).minute();

// calculate the next occurrence, using a minimum resolution of 60 seconds
// otherwise every second of every minute would be valid occurrences
results = later(60).getNext(rSched);

// calculates the next 10 occurrences starting on Jan 1st 2013
results = later(60).get(rSched, 10, new Date('1/1/2013'));

// executes fn every 5 minutes
var fn = function() {
  console.log(new Date().toLocaleString());
}
var l = later(60);
l.exec(cSched, (new Date()), fn);

// stops execution
l.stopExec();

Browser Example

<script src="later.min.js" type="text/javascript"></script>
<script type="text/javascript">

// create the desired schedule
var schedule = enParser().parse('every 5 minutes');

// calculate the next 5 occurrences with a minimum resolution of 60 seconds, using local time
var results = later(60, true).get(schedule, 5);

</script>

Installation

Using npm:

$ npm install later

Using bower:

$ bower install later

Important: A note about underspecified schedules

Later works as a very primitive constraints solver. A Later schedule is simply a set of constraints indicating the valid values for a set of time periods. Later then finds the date and time (or any number of occurrences) that meets all of the constraints.

This has the side effect that Later assumes that all time periods are valid unless specific valid values have been indicated. In other words, if no value for seconds is specified then Later will assume that any value for seconds is valid (in reality, it won't even bother looking at the seconds value). This can be confusing when you are looking at occurrences generated by Later.

For example:

rSched = recur().every(5).minute();
later().get(rSched, 5, new Date('2012-01-31T10:04:00Z'));

2012-01-31T10:05:00Z
2012-01-31T10:05:01Z
2012-01-31T10:05:02Z
2012-01-31T10:05:03Z
2012-01-31T10:05:04Z

Probably not what you were expecting! This happened becase the schedule was underspecified. You probably meant to specify a schedule that occured once every five minutes, but you actually specified a schedule that means check that the minutes value is divisible by 5. Since the resolution (or minimum time between valid occurrences) is set to 1 second by default, Later kept bumping the previous value by 1 second and checking against the constraints. Since they were all met, the occurrence was considered valid.

Fixing underspecified schedules

There are two fixes for underspecified schedules. First, modify the schedule resolution to a higher value such that occurrences happen at the desired frequency. In this case, we bump the resolution up to 60 seconds to ensure that valid occurrences are at least 1 minute apart.

rSched = recur().every(5).minute();
later(60).get(rSched, 5, new Date('2012-01-31T10:05:13Z'));

2012-01-31T10:05:13Z
2012-01-31T10:10:00Z
2012-01-31T10:15:00Z
2012-01-31T10:20:00Z
2012-01-31T10:25:00Z

Note that the schedule will now occur at most once every five minutes as desired. However, keep in mind that every second is considered valid and so the first occurrence will depend on your start time. The other fix is to fully specify the schedule.

rSched = recur().every(5).minute().on(0).second();
later().get(rSched, 5, new Date('2012-01-31T10:05:13Z'));

2012-01-31T10:10:00Z
2012-01-31T10:15:00Z
2012-01-31T10:20:00Z
2012-01-31T10:25:00Z
2012-01-31T10:30:00Z

Now every occurrence will occur on minutes divisible by 5 and when seconds is 0. This is probably closer to what you expected.

Time Periods

Later supports constraints using following time periods (Note: Not all of these are supported when using Cron expressions):

Seconds (s)

Denotes seconds within each minute.
Minimum value is 0, maximum value is 59. Specify 59 for last.

Minutes (m)

Denotes minutes within each hour.
Minimum value is 0, maximum value is 59. Specify 59 for last.

Hours (h)

Denotes hours within each day.
Minimum value is 0, maximum value is 23. Specify 23 for last.

Days Of Month (D)

Denotes number of days within a month.
Minimum value is 1, maximum value is 31. Specify 0 for last.

Days Of Week (dw)

Denotes the days within a week.
Minimum value is 1, maximum value is 7. Specify 0 for last.

1 - Sunday
2 - Monday
3 - Tuesday
4 - Wednesday
5 - Thursday
6 - Friday
7 - Saturday  

Day of Week Count (dc)

Denotes the number of times a particular day has occurred within a month. Used to specify things like second Tuesday, or third Friday in a month.
Minimum value is 1, maximum value is 5. Specify 0 for last.

1 - First occurrence
2 - Second occurrence
3 - Third occurrence
4 - Fourth occurrence
5 - Fifth occurrence
0 - Last occurrence  

Day of Year (dy)

Denotes number of days within a year.
Minimum value is 1, maximum value is 366. Specify 0 for last.

Week of Month (wm)

Denotes number of weeks within a month. The first week is the week that includes the 1st of the month. Subsequent weeks start on Sunday.
Minimum value is 1, maximum value is 5. Specify 0 for last.

For example, February of 2012:

Week 1 - February 2nd,  2012
Week 2 - February 5th,  2012
Week 3 - February 12th, 2012 
Week 4 - February 19th, 2012 
Week 5 - February 26th, 2012

Week of Year (wy)

Denotes the ISO 8601 week date. For more information see: http://en.wikipedia.org/wiki/ISO_week_date.
Minimum value is 1, maximum value is 53. Specify 0 for last.

Month of Year (M)

Denotes the months within a year.
Minimum value is 1, maximum value is 12. Specify 0 for last.

1 - January
2 - February
3 - March
4 - April
5 - May
6 - June
7 - July
8 - August
9 - September
10 - October
11 - November
12 - December  

Year (Y)

Denotes the four digit year.
Minimum value is 1970, maximum value is 2450 (arbitrary).

After constraints

Other than Cron expressions, all other types of schedules support after constraints. Use after constraints when you want a schedule to first occur after a certain amount of time instead of at a specific date and time. For example to specify a schedule that continually occurs after 5 minutes:

var s = enParser().parse('after 5 mins');

After constraints can be chained together with the resultant after constraint being the sum of all of the constraints. For example, to specify a schedule that occurs after 1 day and 15 minutes:

var s = recur().after(1).dayOfYear().after(2).minute();

The first valid occurrence will be 24 hours and 2 minutes from the start date.

Composite Schedules

Other than Cron expressions, all other types of schedules support composite schedules. A composite schedule can include multiple sets of constraints. An occurrence is considered valid if it meets all of the constraints within any one set of the constraints defined.

var s = enParser().parse('every 5 mins also at 11:07 am');

This schedule will produce occurrences on the five minute boundaries (11:00 am, 11:05 am, etc) but will also have a valid occurrence at 11:07 am.

Schedule Exceptions

Other than Cron expressions, all other types of schedules support exception schedules (which can be composite shedules). An occurrence is considered invalid if it meets all of the constraints within any exception schedule that has been defined.

var s = recur().every(1).hour().except().onWeekends().and().at('13:00:00');

This schedule will produce occurrences on the hour (make sure to set the minimum resolution when calculating schedules to 3600 seconds or every second of every minute would also be valid). No valid occurrences will ever occur on weekends or at 1:00 pm.

Creating Schedules using Recur

Recur provides a simple, chainable API for creating schedules. All valid schedules can be produced using this API. See the example folder and the test folder for lots of examples of valid schedules.

Time periods

Recur uses the following:

second();
minute();
hour();
dayOfWeek();
dayOfWeekCount();
dayOfMonth();
dayOfYear();
weekOfMonth();
weekOfYear();
month();
year();

on(args)

Specifies one or more specific occurrences of a time period.

recur().on(2).minute();
recur().on(4,6).dayOfWeek();

onWeekend()

Shorthand for on(1,7).dayOfWeek().

onWeekday()

Shorthand for on(2,3,4,5,6).dayOfWeek().

every(x)

Specifies an interval x of occurrences of a time period. By default, intervals start at the minimum value of the time period and go until the maximum value of the time period.

For example:

recur().every(2).month();

Will include months 1,3,5,7,9,11.

after(x)

Specifies the minimum interval x of a time period that must pass between valid instances of the schedule.

For example:

recur().after(2).month();

Will cause the first valid occurrence to be two months after the start date.

startingOn(x)

Specifies the starting occurrence x of a time period. Must be chained after an every call.

recur().every(4).weeksOfYear().startingOn(2);

between(x, y)

Specifies the starting occurrence x and ending occurrence y of a time period. Must be chained after an every call.

recur().every(6).dayOfYear().between(10,200);

at(time)

Specifies a specific time for the schedule. The time must be in 24 hour time and time zone agnostic.

recur().at('11:00:00');

and()

Creates a composite schedule.

recur().every(2).hour().onWeekend().and().every(5).minute().every(2).hour().onWeekday();

except()

Creates an exception schedule.

recur().every(2).hour().except().onWeekday().and().on(25).dayOfMonth().on(12).month();

Creating Schedules using Text Expression

Schedules can also be created using an English text expression syntax. All valid schedules can be produced in this manner. See the example folder and the test folder for lots of examples of valid schedules.

var s = enParser().parse('every 5 minutes');

If the text expression could not be parsed, s.error will contain the position in the string where parsing failed or -1 if no errors were found.

timePeriod

The valid time period expressions are:

  • (s|sec(ond)?(s)?),
  • (m|min(ute)?(s)?),
  • (h|hour(s)?),
  • (day(s)?( of the month)?),
  • day instance,
  • day(s)? of the week,
  • day(s)? of the year,
  • week(s)?( of the year)?,
  • week(s)? of the month,
  • month(s)?,
  • year

num

((\d\d\d\d)|([2-5]?1(st)?|[2-5]?2(nd)?|[2-5]?3(rd)?|(0|[1-5]?[4-9]|[1-5]0|1[1-3])(th)?))

time

((([0]?[1-9]|1[0-2]):[0-5]\d(\s)?(am|pm))|(([0]?\d|1\d|2[0-3]):[0-5]\d)),

monthName

(jan(uary)?|feb(ruary)?|ma((r(ch)?)?|y)|apr(il)?|ju(ly|ne)|aug(ust)?|oct(ober)?|(sept|nov|dec)(ember)?)

dayName

((sun|mon|tue(s)?|wed(nes)?|thu(r(s)?)?|fri|sat(ur)?)(day)?)

numRange

num((-|through)num)?((,|and)_numRange)*

monthRange

monthName((-|through)monthName)?((,|and)monthName)*

dayRange

dayName((-|through)dayName)?((,|and)dayName)*

specificTime

on the ( first | last | numRange timePeriod )

startingOn

(start(ing)? (at|on( the)?)?) num timePeriod

between

between (the)? num and num

recurringTime

every ( weekend | weekday | num timePeriod ( startingOn | between ))

after

after num timePeriod

onDayOfWeek

on dayRange

ofMonth

of monthRange

inYear

in numRange

schedule

( specificTime | recurringTime | after | onDayOfWeek | ofMonth | inYear )*

compositeSchedule

( schedule )( also schedule )*( except )( schedule )( also schedule )*

Creating Schedules using Cron

A valid schedule can be generated from any valid Cron expression. For more information on the Cron expression format, see: http://en.wikipedia.org/wiki/Cron. Currently Cron expressions are the most compact way to describe a schedule, but are slightly less flexible (no direct support for composite or exception schedules) and can be harder to read.

parse(expr [,hasSeconds])

Parses the Cron expression expr and returns a valid schedule that can be used with Later. If expr contains the seconds component (optionally appears before the minutes component), then hasSeconds must be set to true.

var s = cronParser().parse('* */5 * * * *', true);

Creating Schedules Manually

Schedules are basic json objects that can be constructed directly if desired. The schedule object has the following form:

{
  schedules: [
    {
      // constraints
    },
    {
      // constraints
    },

  ],
  exceptions: [
    {
      // constraints
    },
    {
      // constraints
    },
  ]
}

where constraints are of the form:

constraint_id: [
  //valid values
],

The constraint_ids can be found in the Time Periods section above following the constraint name along with the valid values. To specify an after constraint, prefix the desired constraint_id with a.

For example, the schedule every hour on weekdays and every other hour on weekends would be defined as:

{schedules: [ 
  {
    h: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23], 
    d: [2,3,4,5,6]
  },
  {
    h: [0,2,4,6,8,10,12,14,16,18,20,22], 
    d: [1,7]}
  ]
};

Calculating Occurrences

later([resolution[, useLocalTime]])

Configures later to calculate future occurrences. Resolution is the minimum amount of time in seconds between valid occurrences. The default is 1 second which may produce undesirable results when calcuating multiple occurrences into the future.

To calculate occurrences for a schedule that occurs every five minutes, either of the following would produce the expected results:

var s = recur().every(5).minute();
var r = later(60).get(s,10);

var s = recur().every(5).minute().first().second();
var r = later().get(s,10);

By default, all schedules are calculated using UTC time. Set useLocalTime to true to do calculations using local time instead. This makes hour, minute, and time constraints fall on the expected values on a local machine. Schedule definitions are always time zone agnostic.

getNext(recur, [startDate[, endDate]])

Returns the next valid occurrence of the schedule definition, recur, that is passed in or null if no occurrences exist. Pass in Date objects to startDate and endDate to define the time range to find the next valid occurrence. By default startDate is the current date and time and there is no endDate.

var s = cronParser().parse(* */5 * * * *);
later().getNext(s, new Date('1/1/2012'), new Date('1/1/2013'));

getPrevious(recur, [startDate[, endDate]])

Returns the previous valid occurrence of the schedule definition, recur, that is passed in or null if no occurrences exist. Pass in Date objects to startDate and endDate to define the time range to find the next valid occurrence. For previous occurrences, the startDate must be greater than the endDate. By default startDate is the current date and time and there is no endDate.

var s = cronParser().parse(* */5 * * * *);
later().getPrevious(s, new Date('1/1/2013'), new Date('1/1/2012'));

get(recur, count, [startDate[, endDate][, reverse]])

Returns the next count occurrences of the schedule definition, recur, that is passed in or null if no occurrences exist. Pass in Date objects to startDate and endDate to define the time range to find the next valid occurrences. By default startDate is the current date and time and there is no endDate. Setting reverse to true will return the previous occurrences starting from startDate and working backwards.

var s = cronParser().parse(* */5 * * * *);
later().get(sched, 10, new Date('1/1/2012'), new Date('1/1/2013'));

isValid(recur, date)

Returns true if date is a valid occurrence of the schedule defined by recur.

exec(recur, startDate, callback, arg)

Executes callback on the schedule defined by recur starting on startDate. The callback will be called with whatever is passed in as arg. The callback will continue to be called until either stopExec is called or there are no more valid occurrences of the schedule. Only one schedule should be executed per later object to make stoping execution simpler.

Do this:

var s1 = cronParser().parse('* */5 * * * *');  
var every5 = later();  
ever5.exec(s1, new Date(), cb);    

var s2 = cronParser().parse('* */6 * * * *');
var every6 = later();
every6.exec(s2, new Date(), cb);

Not this:

var s1 = cronParser().parse('* */5 * * * *');
var s2 = cronParser().parse('* */6 * * * *');
var l = later();
l.exec(s1, new Date(), cb);  
l.exec(s2, new Date(), cb);

stopExec()

Immediately stops the execution of any schedule execution created using exec.

Building

To build the minified javascript files for later:

$ make build

There are 5 different javascript files that are built.

  • later.min.js contains all of the library files
  • later-core.min.js contains only the core engine for calculating occurrences
  • later-recur.min.js contains only the files needed to use Recur based scheduling
  • later-cron.min.js contains only the files needed to use Cron based scheduling
  • later-en.min.js contains only the files need to use English text based scheduling

Running tests

To run the tests for later, run npm install to install dependencies and then:

$ make test

Performance

Some basic performance tests are available on jsperf:

License

(The MIT License)

Copyright (c) 2011 BunKat LLC <bill@bunkat.com>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WIT

About

A javascript library for defining recurring schedules and calculating future (or past) occurrences for them. Includes support for using English phrases and Cron schedules. Works in Node and in the browser.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published