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.
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
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();
<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>
Using npm:
$ npm install later
Using bower:
$ bower install later
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.
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.
Later supports constraints using following time periods (Note: Not all of these are supported when using Cron expressions):
Denotes seconds within each minute.
Minimum value is 0, maximum value is 59. Specify 59 for last.
Denotes minutes within each hour.
Minimum value is 0, maximum value is 59. Specify 59 for last.
Denotes hours within each day.
Minimum value is 0, maximum value is 23. Specify 23 for last.
Denotes number of days within a month.
Minimum value is 1, maximum value is 31. Specify 0 for last.
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
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
Denotes number of days within a year.
Minimum value is 1, maximum value is 366. Specify 0 for last.
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
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.
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
Denotes the four digit year.
Minimum value is 1970, maximum value is 2450 (arbitrary).
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.
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.
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.
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.
Recur uses the following:
second();
minute();
hour();
dayOfWeek();
dayOfWeekCount();
dayOfMonth();
dayOfYear();
weekOfMonth();
weekOfYear();
month();
year();
Specifies one or more specific occurrences of a time period.
recur().on(2).minute();
recur().on(4,6).dayOfWeek();
Shorthand for on(1,7).dayOfWeek()
.
Shorthand for on(2,3,4,5,6).dayOfWeek()
.
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.
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.
Specifies the starting occurrence x
of a time period. Must be chained after an every
call.
recur().every(4).weeksOfYear().startingOn(2);
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);
Specifies a specific time for the schedule. The time must be in 24 hour time and time zone agnostic.
recur().at('11:00:00');
Creates a composite schedule.
recur().every(2).hour().onWeekend().and().every(5).minute().every(2).hour().onWeekday();
Creates an exception schedule.
recur().every(2).hour().except().onWeekday().and().on(25).dayOfMonth().on(12).month();
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.
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
((\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)?))
((([0]?[1-9]|1[0-2]):[0-5]\d(\s)?(am|pm))|(([0]?\d|1\d|2[0-3]):[0-5]\d)),
(jan(uary)?|feb(ruary)?|ma((r(ch)?)?|y)|apr(il)?|ju(ly|ne)|aug(ust)?|oct(ober)?|(sept|nov|dec)(ember)?)
((sun|mon|tue(s)?|wed(nes)?|thu(r(s)?)?|fri|sat(ur)?)(day)?)
num((-|through)num)?((,|and)_numRange)*
monthName((-|through)monthName)?((,|and)monthName)*
dayName((-|through)dayName)?((,|and)dayName)*
on the ( first | last | numRange timePeriod )
(start(ing)? (at|on( the)?)?) num timePeriod
between (the)? num and num
every ( weekend | weekday | num timePeriod ( startingOn | between ))
after num timePeriod
on dayRange
of monthRange
in numRange
( specificTime | recurringTime | after | onDayOfWeek | ofMonth | inYear )*
( schedule )( also schedule )*( except )( schedule )( also schedule )*
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.
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);
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_id
s 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]}
]
};
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.
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'));
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'));
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'));
Returns true if date
is a valid occurrence of the schedule defined by recur
.
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);
Immediately stops the execution of any schedule execution created using exec
.
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
To run the tests for later, run npm install
to install dependencies and then:
$ make test
Some basic performance tests are available on jsperf:
(The MIT License)
Copyright (c) 2011 BunKat LLC <[email protected]>
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