You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1003 lines
26 KiB
1003 lines
26 KiB
1 month ago
|
'use strict';
|
||
|
|
||
|
// Load Date class extensions
|
||
|
var CronDate = require('./date');
|
||
|
|
||
|
var stringifyField = require('./field_stringify');
|
||
|
|
||
|
/**
|
||
|
* Cron iteration loop safety limit
|
||
|
*/
|
||
|
var LOOP_LIMIT = 10000;
|
||
|
|
||
|
/**
|
||
|
* Construct a new expression parser
|
||
|
*
|
||
|
* Options:
|
||
|
* currentDate: iterator start date
|
||
|
* endDate: iterator end date
|
||
|
*
|
||
|
* @constructor
|
||
|
* @private
|
||
|
* @param {Object} fields Expression fields parsed values
|
||
|
* @param {Object} options Parser options
|
||
|
*/
|
||
|
function CronExpression (fields, options) {
|
||
|
this._options = options;
|
||
|
this._utc = options.utc || false;
|
||
|
this._tz = this._utc ? 'UTC' : options.tz;
|
||
|
this._currentDate = new CronDate(options.currentDate, this._tz);
|
||
|
this._startDate = options.startDate ? new CronDate(options.startDate, this._tz) : null;
|
||
|
this._endDate = options.endDate ? new CronDate(options.endDate, this._tz) : null;
|
||
|
this._isIterator = options.iterator || false;
|
||
|
this._hasIterated = false;
|
||
|
this._nthDayOfWeek = options.nthDayOfWeek || 0;
|
||
|
this.fields = CronExpression._freezeFields(fields);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Field mappings
|
||
|
* @type {Array}
|
||
|
*/
|
||
|
CronExpression.map = [ 'second', 'minute', 'hour', 'dayOfMonth', 'month', 'dayOfWeek' ];
|
||
|
|
||
|
/**
|
||
|
* Prefined intervals
|
||
|
* @type {Object}
|
||
|
*/
|
||
|
CronExpression.predefined = {
|
||
|
'@yearly': '0 0 1 1 *',
|
||
|
'@monthly': '0 0 1 * *',
|
||
|
'@weekly': '0 0 * * 0',
|
||
|
'@daily': '0 0 * * *',
|
||
|
'@hourly': '0 * * * *'
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Fields constraints
|
||
|
* @type {Array}
|
||
|
*/
|
||
|
CronExpression.constraints = [
|
||
|
{ min: 0, max: 59, chars: [] }, // Second
|
||
|
{ min: 0, max: 59, chars: [] }, // Minute
|
||
|
{ min: 0, max: 23, chars: [] }, // Hour
|
||
|
{ min: 1, max: 31, chars: ['L'] }, // Day of month
|
||
|
{ min: 1, max: 12, chars: [] }, // Month
|
||
|
{ min: 0, max: 7, chars: ['L'] }, // Day of week
|
||
|
];
|
||
|
|
||
|
/**
|
||
|
* Days in month
|
||
|
* @type {number[]}
|
||
|
*/
|
||
|
CronExpression.daysInMonth = [
|
||
|
31,
|
||
|
29,
|
||
|
31,
|
||
|
30,
|
||
|
31,
|
||
|
30,
|
||
|
31,
|
||
|
31,
|
||
|
30,
|
||
|
31,
|
||
|
30,
|
||
|
31
|
||
|
];
|
||
|
|
||
|
/**
|
||
|
* Field aliases
|
||
|
* @type {Object}
|
||
|
*/
|
||
|
CronExpression.aliases = {
|
||
|
month: {
|
||
|
jan: 1,
|
||
|
feb: 2,
|
||
|
mar: 3,
|
||
|
apr: 4,
|
||
|
may: 5,
|
||
|
jun: 6,
|
||
|
jul: 7,
|
||
|
aug: 8,
|
||
|
sep: 9,
|
||
|
oct: 10,
|
||
|
nov: 11,
|
||
|
dec: 12
|
||
|
},
|
||
|
|
||
|
dayOfWeek: {
|
||
|
sun: 0,
|
||
|
mon: 1,
|
||
|
tue: 2,
|
||
|
wed: 3,
|
||
|
thu: 4,
|
||
|
fri: 5,
|
||
|
sat: 6
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Field defaults
|
||
|
* @type {Array}
|
||
|
*/
|
||
|
CronExpression.parseDefaults = [ '0', '*', '*', '*', '*', '*' ];
|
||
|
|
||
|
CronExpression.standardValidCharacters = /^[,*\d/-]+$/;
|
||
|
CronExpression.dayOfWeekValidCharacters = /^[?,*\dL#/-]+$/;
|
||
|
CronExpression.dayOfMonthValidCharacters = /^[?,*\dL/-]+$/;
|
||
|
CronExpression.validCharacters = {
|
||
|
second: CronExpression.standardValidCharacters,
|
||
|
minute: CronExpression.standardValidCharacters,
|
||
|
hour: CronExpression.standardValidCharacters,
|
||
|
dayOfMonth: CronExpression.dayOfMonthValidCharacters,
|
||
|
month: CronExpression.standardValidCharacters,
|
||
|
dayOfWeek: CronExpression.dayOfWeekValidCharacters,
|
||
|
};
|
||
|
|
||
|
CronExpression._isValidConstraintChar = function _isValidConstraintChar(constraints, value) {
|
||
|
if (typeof value !== 'string') {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return constraints.chars.some(function(char) {
|
||
|
return value.indexOf(char) > -1;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Parse input interval
|
||
|
*
|
||
|
* @param {String} field Field symbolic name
|
||
|
* @param {String} value Field value
|
||
|
* @param {Array} constraints Range upper and lower constraints
|
||
|
* @return {Array} Sequence of sorted values
|
||
|
* @private
|
||
|
*/
|
||
|
CronExpression._parseField = function _parseField (field, value, constraints) {
|
||
|
// Replace aliases
|
||
|
switch (field) {
|
||
|
case 'month':
|
||
|
case 'dayOfWeek':
|
||
|
var aliases = CronExpression.aliases[field];
|
||
|
|
||
|
value = value.replace(/[a-z]{3}/gi, function(match) {
|
||
|
match = match.toLowerCase();
|
||
|
|
||
|
if (typeof aliases[match] !== 'undefined') {
|
||
|
return aliases[match];
|
||
|
} else {
|
||
|
throw new Error('Validation error, cannot resolve alias "' + match + '"');
|
||
|
}
|
||
|
});
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Check for valid characters.
|
||
|
if (!(CronExpression.validCharacters[field].test(value))) {
|
||
|
throw new Error('Invalid characters, got value: ' + value);
|
||
|
}
|
||
|
|
||
|
// Replace '*' and '?'
|
||
|
if (value.indexOf('*') !== -1) {
|
||
|
value = value.replace(/\*/g, constraints.min + '-' + constraints.max);
|
||
|
} else if (value.indexOf('?') !== -1) {
|
||
|
value = value.replace(/\?/g, constraints.min + '-' + constraints.max);
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// Inline parsing functions
|
||
|
//
|
||
|
// Parser path:
|
||
|
// - parseSequence
|
||
|
// - parseRepeat
|
||
|
// - parseRange
|
||
|
|
||
|
/**
|
||
|
* Parse sequence
|
||
|
*
|
||
|
* @param {String} val
|
||
|
* @return {Array}
|
||
|
* @private
|
||
|
*/
|
||
|
function parseSequence (val) {
|
||
|
var stack = [];
|
||
|
|
||
|
function handleResult (result) {
|
||
|
if (result instanceof Array) { // Make sequence linear
|
||
|
for (var i = 0, c = result.length; i < c; i++) {
|
||
|
var value = result[i];
|
||
|
|
||
|
if (CronExpression._isValidConstraintChar(constraints, value)) {
|
||
|
stack.push(value);
|
||
|
continue;
|
||
|
}
|
||
|
// Check constraints
|
||
|
if (typeof value !== 'number' || Number.isNaN(value) || value < constraints.min || value > constraints.max) {
|
||
|
throw new Error(
|
||
|
'Constraint error, got value ' + value + ' expected range ' +
|
||
|
constraints.min + '-' + constraints.max
|
||
|
);
|
||
|
}
|
||
|
|
||
|
stack.push(value);
|
||
|
}
|
||
|
} else { // Scalar value
|
||
|
|
||
|
if (CronExpression._isValidConstraintChar(constraints, result)) {
|
||
|
stack.push(result);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
var numResult = +result;
|
||
|
|
||
|
// Check constraints
|
||
|
if (Number.isNaN(numResult) || numResult < constraints.min || numResult > constraints.max) {
|
||
|
throw new Error(
|
||
|
'Constraint error, got value ' + result + ' expected range ' +
|
||
|
constraints.min + '-' + constraints.max
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (field === 'dayOfWeek') {
|
||
|
numResult = numResult % 7;
|
||
|
}
|
||
|
|
||
|
stack.push(numResult);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var atoms = val.split(',');
|
||
|
if (!atoms.every(function (atom) {
|
||
|
return atom.length > 0;
|
||
|
})) {
|
||
|
throw new Error('Invalid list value format');
|
||
|
}
|
||
|
|
||
|
if (atoms.length > 1) {
|
||
|
for (var i = 0, c = atoms.length; i < c; i++) {
|
||
|
handleResult(parseRepeat(atoms[i]));
|
||
|
}
|
||
|
} else {
|
||
|
handleResult(parseRepeat(val));
|
||
|
}
|
||
|
|
||
|
stack.sort(CronExpression._sortCompareFn);
|
||
|
|
||
|
return stack;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse repetition interval
|
||
|
*
|
||
|
* @param {String} val
|
||
|
* @return {Array}
|
||
|
*/
|
||
|
function parseRepeat (val) {
|
||
|
var repeatInterval = 1;
|
||
|
var atoms = val.split('/');
|
||
|
|
||
|
if (atoms.length > 2) {
|
||
|
throw new Error('Invalid repeat: ' + val);
|
||
|
}
|
||
|
|
||
|
if (atoms.length > 1) {
|
||
|
if (atoms[0] == +atoms[0]) {
|
||
|
atoms = [atoms[0] + '-' + constraints.max, atoms[1]];
|
||
|
}
|
||
|
return parseRange(atoms[0], atoms[atoms.length - 1]);
|
||
|
}
|
||
|
|
||
|
return parseRange(val, repeatInterval);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse range
|
||
|
*
|
||
|
* @param {String} val
|
||
|
* @param {Number} repeatInterval Repetition interval
|
||
|
* @return {Array}
|
||
|
* @private
|
||
|
*/
|
||
|
function parseRange (val, repeatInterval) {
|
||
|
var stack = [];
|
||
|
var atoms = val.split('-');
|
||
|
|
||
|
if (atoms.length > 1 ) {
|
||
|
// Invalid range, return value
|
||
|
if (atoms.length < 2) {
|
||
|
return +val;
|
||
|
}
|
||
|
|
||
|
if (!atoms[0].length) {
|
||
|
if (!atoms[1].length) {
|
||
|
throw new Error('Invalid range: ' + val);
|
||
|
}
|
||
|
|
||
|
return +val;
|
||
|
}
|
||
|
|
||
|
// Validate range
|
||
|
var min = +atoms[0];
|
||
|
var max = +atoms[1];
|
||
|
|
||
|
if (Number.isNaN(min) || Number.isNaN(max) ||
|
||
|
min < constraints.min || max > constraints.max) {
|
||
|
throw new Error(
|
||
|
'Constraint error, got range ' +
|
||
|
min + '-' + max +
|
||
|
' expected range ' +
|
||
|
constraints.min + '-' + constraints.max
|
||
|
);
|
||
|
} else if (min > max) {
|
||
|
throw new Error('Invalid range: ' + val);
|
||
|
}
|
||
|
|
||
|
// Create range
|
||
|
var repeatIndex = +repeatInterval;
|
||
|
|
||
|
if (Number.isNaN(repeatIndex) || repeatIndex <= 0) {
|
||
|
throw new Error('Constraint error, cannot repeat at every ' + repeatIndex + ' time.');
|
||
|
}
|
||
|
|
||
|
// JS DOW is in range of 0-6 (SUN-SAT) but we also support 7 in the expression
|
||
|
// Handle case when range contains 7 instead of 0 and translate this value to 0
|
||
|
if (field === 'dayOfWeek' && max % 7 === 0) {
|
||
|
stack.push(0);
|
||
|
}
|
||
|
|
||
|
for (var index = min, count = max; index <= count; index++) {
|
||
|
var exists = stack.indexOf(index) !== -1;
|
||
|
if (!exists && repeatIndex > 0 && (repeatIndex % repeatInterval) === 0) {
|
||
|
repeatIndex = 1;
|
||
|
stack.push(index);
|
||
|
} else {
|
||
|
repeatIndex++;
|
||
|
}
|
||
|
}
|
||
|
return stack;
|
||
|
}
|
||
|
|
||
|
return Number.isNaN(+val) ? val : +val;
|
||
|
}
|
||
|
|
||
|
return parseSequence(value);
|
||
|
};
|
||
|
|
||
|
CronExpression._sortCompareFn = function(a, b) {
|
||
|
var aIsNumber = typeof a === 'number';
|
||
|
var bIsNumber = typeof b === 'number';
|
||
|
|
||
|
if (aIsNumber && bIsNumber) {
|
||
|
return a - b;
|
||
|
}
|
||
|
|
||
|
if (!aIsNumber && bIsNumber) {
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
if (aIsNumber && !bIsNumber) {
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
return a.localeCompare(b);
|
||
|
};
|
||
|
|
||
|
CronExpression._handleMaxDaysInMonth = function(mappedFields) {
|
||
|
// Filter out any day of month value that is larger than given month expects
|
||
|
if (mappedFields.month.length === 1) {
|
||
|
var daysInMonth = CronExpression.daysInMonth[mappedFields.month[0] - 1];
|
||
|
|
||
|
if (mappedFields.dayOfMonth[0] > daysInMonth) {
|
||
|
throw new Error('Invalid explicit day of month definition');
|
||
|
}
|
||
|
|
||
|
return mappedFields.dayOfMonth
|
||
|
.filter(function(dayOfMonth) {
|
||
|
return dayOfMonth === 'L' ? true : dayOfMonth <= daysInMonth;
|
||
|
})
|
||
|
.sort(CronExpression._sortCompareFn);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
CronExpression._freezeFields = function(fields) {
|
||
|
for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
|
||
|
var field = CronExpression.map[i]; // Field name
|
||
|
var value = fields[field];
|
||
|
fields[field] = Object.freeze(value);
|
||
|
}
|
||
|
return Object.freeze(fields);
|
||
|
};
|
||
|
|
||
|
CronExpression.prototype._applyTimezoneShift = function(currentDate, dateMathVerb, method) {
|
||
|
if ((method === 'Month') || (method === 'Day')) {
|
||
|
var prevTime = currentDate.getTime();
|
||
|
currentDate[dateMathVerb + method]();
|
||
|
var currTime = currentDate.getTime();
|
||
|
if (prevTime === currTime) {
|
||
|
// Jumped into a not existent date due to a DST transition
|
||
|
if ((currentDate.getMinutes() === 0) &&
|
||
|
(currentDate.getSeconds() === 0)) {
|
||
|
currentDate.addHour();
|
||
|
} else if ((currentDate.getMinutes() === 59) &&
|
||
|
(currentDate.getSeconds() === 59)) {
|
||
|
currentDate.subtractHour();
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
var previousHour = currentDate.getHours();
|
||
|
currentDate[dateMathVerb + method]();
|
||
|
var currentHour = currentDate.getHours();
|
||
|
var diff = currentHour - previousHour;
|
||
|
if (diff === 2) {
|
||
|
// Starting DST
|
||
|
if (this.fields.hour.length !== 24) {
|
||
|
// Hour is specified
|
||
|
this._dstStart = currentHour;
|
||
|
}
|
||
|
} else if ((diff === 0) &&
|
||
|
(currentDate.getMinutes() === 0) &&
|
||
|
(currentDate.getSeconds() === 0)) {
|
||
|
// Ending DST
|
||
|
if (this.fields.hour.length !== 24) {
|
||
|
// Hour is specified
|
||
|
this._dstEnd = currentHour;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Find next or previous matching schedule date
|
||
|
*
|
||
|
* @return {CronDate}
|
||
|
* @private
|
||
|
*/
|
||
|
CronExpression.prototype._findSchedule = function _findSchedule (reverse) {
|
||
|
|
||
|
/**
|
||
|
* Match field value
|
||
|
*
|
||
|
* @param {String} value
|
||
|
* @param {Array} sequence
|
||
|
* @return {Boolean}
|
||
|
* @private
|
||
|
*/
|
||
|
function matchSchedule (value, sequence) {
|
||
|
for (var i = 0, c = sequence.length; i < c; i++) {
|
||
|
if (sequence[i] >= value) {
|
||
|
return sequence[i] === value;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return sequence[0] === value;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helps determine if the provided date is the correct nth occurence of the
|
||
|
* desired day of week.
|
||
|
*
|
||
|
* @param {CronDate} date
|
||
|
* @param {Number} nthDayOfWeek
|
||
|
* @return {Boolean}
|
||
|
* @private
|
||
|
*/
|
||
|
function isNthDayMatch(date, nthDayOfWeek) {
|
||
|
if (nthDayOfWeek < 6) {
|
||
|
if (
|
||
|
date.getDate() < 8 &&
|
||
|
nthDayOfWeek === 1 // First occurence has to happen in first 7 days of the month
|
||
|
) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
var offset = date.getDate() % 7 ? 1 : 0; // Math is off by 1 when dayOfWeek isn't divisible by 7
|
||
|
var adjustedDate = date.getDate() - (date.getDate() % 7); // find the first occurance
|
||
|
var occurrence = Math.floor(adjustedDate / 7) + offset;
|
||
|
|
||
|
return occurrence === nthDayOfWeek;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Helper function that checks if 'L' is in the array
|
||
|
*
|
||
|
* @param {Array} expressions
|
||
|
*/
|
||
|
function isLInExpressions(expressions) {
|
||
|
return expressions.length > 0 && expressions.some(function(expression) {
|
||
|
return typeof expression === 'string' && expression.indexOf('L') >= 0;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
|
||
|
// Whether to use backwards directionality when searching
|
||
|
reverse = reverse || false;
|
||
|
var dateMathVerb = reverse ? 'subtract' : 'add';
|
||
|
|
||
|
var currentDate = new CronDate(this._currentDate, this._tz);
|
||
|
var startDate = this._startDate;
|
||
|
var endDate = this._endDate;
|
||
|
|
||
|
// Find matching schedule
|
||
|
var startTimestamp = currentDate.getTime();
|
||
|
var stepCount = 0;
|
||
|
|
||
|
function isLastWeekdayOfMonthMatch(expressions) {
|
||
|
return expressions.some(function(expression) {
|
||
|
// There might be multiple expressions and not all of them will contain
|
||
|
// the "L".
|
||
|
if (!isLInExpressions([expression])) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// The first character represents the weekday
|
||
|
var weekday = Number.parseInt(expression[0]) % 7;
|
||
|
|
||
|
if (Number.isNaN(weekday)) {
|
||
|
throw new Error('Invalid last weekday of the month expression: ' + expression);
|
||
|
}
|
||
|
|
||
|
return currentDate.getDay() === weekday && currentDate.isLastWeekdayOfMonth();
|
||
|
});
|
||
|
}
|
||
|
|
||
|
while (stepCount < LOOP_LIMIT) {
|
||
|
stepCount++;
|
||
|
|
||
|
// Validate timespan
|
||
|
if (reverse) {
|
||
|
if (startDate && (currentDate.getTime() - startDate.getTime() < 0)) {
|
||
|
throw new Error('Out of the timespan range');
|
||
|
}
|
||
|
} else {
|
||
|
if (endDate && (endDate.getTime() - currentDate.getTime()) < 0) {
|
||
|
throw new Error('Out of the timespan range');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Day of month and week matching:
|
||
|
//
|
||
|
// "The day of a command's execution can be specified by two fields --
|
||
|
// day of month, and day of week. If both fields are restricted (ie,
|
||
|
// aren't *), the command will be run when either field matches the cur-
|
||
|
// rent time. For example, "30 4 1,15 * 5" would cause a command to be
|
||
|
// run at 4:30 am on the 1st and 15th of each month, plus every Friday."
|
||
|
//
|
||
|
// http://unixhelp.ed.ac.uk/CGI/man-cgi?crontab+5
|
||
|
//
|
||
|
|
||
|
var dayOfMonthMatch = matchSchedule(currentDate.getDate(), this.fields.dayOfMonth);
|
||
|
if (isLInExpressions(this.fields.dayOfMonth)) {
|
||
|
dayOfMonthMatch = dayOfMonthMatch || currentDate.isLastDayOfMonth();
|
||
|
}
|
||
|
var dayOfWeekMatch = matchSchedule(currentDate.getDay(), this.fields.dayOfWeek);
|
||
|
if (isLInExpressions(this.fields.dayOfWeek)) {
|
||
|
dayOfWeekMatch = dayOfWeekMatch || isLastWeekdayOfMonthMatch(this.fields.dayOfWeek);
|
||
|
}
|
||
|
var isDayOfMonthWildcardMatch = this.fields.dayOfMonth.length >= CronExpression.daysInMonth[currentDate.getMonth()];
|
||
|
var isDayOfWeekWildcardMatch = this.fields.dayOfWeek.length === CronExpression.constraints[5].max - CronExpression.constraints[5].min + 1;
|
||
|
var currentHour = currentDate.getHours();
|
||
|
|
||
|
// Add or subtract day if select day not match with month (according to calendar)
|
||
|
if (!dayOfMonthMatch && (!dayOfWeekMatch || isDayOfWeekWildcardMatch)) {
|
||
|
this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Add or subtract day if not day of month is set (and no match) and day of week is wildcard
|
||
|
if (!isDayOfMonthWildcardMatch && isDayOfWeekWildcardMatch && !dayOfMonthMatch) {
|
||
|
this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Add or subtract day if not day of week is set (and no match) and day of month is wildcard
|
||
|
if (isDayOfMonthWildcardMatch && !isDayOfWeekWildcardMatch && !dayOfWeekMatch) {
|
||
|
this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Add or subtract day if day of week & nthDayOfWeek are set (and no match)
|
||
|
if (
|
||
|
this._nthDayOfWeek > 0 &&
|
||
|
!isNthDayMatch(currentDate, this._nthDayOfWeek)
|
||
|
) {
|
||
|
this._applyTimezoneShift(currentDate, dateMathVerb, 'Day');
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Match month
|
||
|
if (!matchSchedule(currentDate.getMonth() + 1, this.fields.month)) {
|
||
|
this._applyTimezoneShift(currentDate, dateMathVerb, 'Month');
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Match hour
|
||
|
if (!matchSchedule(currentHour, this.fields.hour)) {
|
||
|
if (this._dstStart !== currentHour) {
|
||
|
this._dstStart = null;
|
||
|
this._applyTimezoneShift(currentDate, dateMathVerb, 'Hour');
|
||
|
continue;
|
||
|
} else if (!matchSchedule(currentHour - 1, this.fields.hour)) {
|
||
|
currentDate[dateMathVerb + 'Hour']();
|
||
|
continue;
|
||
|
}
|
||
|
} else if (this._dstEnd === currentHour) {
|
||
|
if (!reverse) {
|
||
|
this._dstEnd = null;
|
||
|
this._applyTimezoneShift(currentDate, 'add', 'Hour');
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Match minute
|
||
|
if (!matchSchedule(currentDate.getMinutes(), this.fields.minute)) {
|
||
|
this._applyTimezoneShift(currentDate, dateMathVerb, 'Minute');
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Match second
|
||
|
if (!matchSchedule(currentDate.getSeconds(), this.fields.second)) {
|
||
|
this._applyTimezoneShift(currentDate, dateMathVerb, 'Second');
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Increase a second in case in the first iteration the currentDate was not
|
||
|
// modified
|
||
|
if (startTimestamp === currentDate.getTime()) {
|
||
|
if ((dateMathVerb === 'add') || (currentDate.getMilliseconds() === 0)) {
|
||
|
this._applyTimezoneShift(currentDate, dateMathVerb, 'Second');
|
||
|
} else {
|
||
|
currentDate.setMilliseconds(0);
|
||
|
}
|
||
|
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (stepCount >= LOOP_LIMIT) {
|
||
|
throw new Error('Invalid expression, loop limit exceeded');
|
||
|
}
|
||
|
|
||
|
this._currentDate = new CronDate(currentDate, this._tz);
|
||
|
this._hasIterated = true;
|
||
|
|
||
|
return currentDate;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Find next suitable date
|
||
|
*
|
||
|
* @public
|
||
|
* @return {CronDate|Object}
|
||
|
*/
|
||
|
CronExpression.prototype.next = function next () {
|
||
|
var schedule = this._findSchedule();
|
||
|
|
||
|
// Try to return ES6 compatible iterator
|
||
|
if (this._isIterator) {
|
||
|
return {
|
||
|
value: schedule,
|
||
|
done: !this.hasNext()
|
||
|
};
|
||
|
}
|
||
|
|
||
|
return schedule;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Find previous suitable date
|
||
|
*
|
||
|
* @public
|
||
|
* @return {CronDate|Object}
|
||
|
*/
|
||
|
CronExpression.prototype.prev = function prev () {
|
||
|
var schedule = this._findSchedule(true);
|
||
|
|
||
|
// Try to return ES6 compatible iterator
|
||
|
if (this._isIterator) {
|
||
|
return {
|
||
|
value: schedule,
|
||
|
done: !this.hasPrev()
|
||
|
};
|
||
|
}
|
||
|
|
||
|
return schedule;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Check if next suitable date exists
|
||
|
*
|
||
|
* @public
|
||
|
* @return {Boolean}
|
||
|
*/
|
||
|
CronExpression.prototype.hasNext = function() {
|
||
|
var current = this._currentDate;
|
||
|
var hasIterated = this._hasIterated;
|
||
|
|
||
|
try {
|
||
|
this._findSchedule();
|
||
|
return true;
|
||
|
} catch (err) {
|
||
|
return false;
|
||
|
} finally {
|
||
|
this._currentDate = current;
|
||
|
this._hasIterated = hasIterated;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Check if previous suitable date exists
|
||
|
*
|
||
|
* @public
|
||
|
* @return {Boolean}
|
||
|
*/
|
||
|
CronExpression.prototype.hasPrev = function() {
|
||
|
var current = this._currentDate;
|
||
|
var hasIterated = this._hasIterated;
|
||
|
|
||
|
try {
|
||
|
this._findSchedule(true);
|
||
|
return true;
|
||
|
} catch (err) {
|
||
|
return false;
|
||
|
} finally {
|
||
|
this._currentDate = current;
|
||
|
this._hasIterated = hasIterated;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Iterate over expression iterator
|
||
|
*
|
||
|
* @public
|
||
|
* @param {Number} steps Numbers of steps to iterate
|
||
|
* @param {Function} callback Optional callback
|
||
|
* @return {Array} Array of the iterated results
|
||
|
*/
|
||
|
CronExpression.prototype.iterate = function iterate (steps, callback) {
|
||
|
var dates = [];
|
||
|
|
||
|
if (steps >= 0) {
|
||
|
for (var i = 0, c = steps; i < c; i++) {
|
||
|
try {
|
||
|
var item = this.next();
|
||
|
dates.push(item);
|
||
|
|
||
|
// Fire the callback
|
||
|
if (callback) {
|
||
|
callback(item, i);
|
||
|
}
|
||
|
} catch (err) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
for (var i = 0, c = steps; i > c; i--) {
|
||
|
try {
|
||
|
var item = this.prev();
|
||
|
dates.push(item);
|
||
|
|
||
|
// Fire the callback
|
||
|
if (callback) {
|
||
|
callback(item, i);
|
||
|
}
|
||
|
} catch (err) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return dates;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Reset expression iterator state
|
||
|
*
|
||
|
* @public
|
||
|
*/
|
||
|
CronExpression.prototype.reset = function reset (newDate) {
|
||
|
this._currentDate = new CronDate(newDate || this._options.currentDate);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Stringify the expression
|
||
|
*
|
||
|
* @public
|
||
|
* @param {Boolean} [includeSeconds] Should stringify seconds
|
||
|
* @return {String}
|
||
|
*/
|
||
|
CronExpression.prototype.stringify = function stringify(includeSeconds) {
|
||
|
var resultArr = [];
|
||
|
for (var i = includeSeconds ? 0 : 1, c = CronExpression.map.length; i < c; ++i) {
|
||
|
var field = CronExpression.map[i];
|
||
|
var value = this.fields[field];
|
||
|
var constraint = CronExpression.constraints[i];
|
||
|
|
||
|
if (field === 'dayOfMonth' && this.fields.month.length === 1) {
|
||
|
constraint = { min: 1, max: CronExpression.daysInMonth[this.fields.month[0] - 1] };
|
||
|
} else if (field === 'dayOfWeek') {
|
||
|
// Prefer 0-6 range when serializing day of week field
|
||
|
constraint = { min: 0, max: 6 };
|
||
|
value = value[value.length - 1] === 7 ? value.slice(0, -1) : value;
|
||
|
}
|
||
|
|
||
|
resultArr.push(stringifyField(value, constraint.min, constraint.max));
|
||
|
}
|
||
|
return resultArr.join(' ');
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Parse input expression (async)
|
||
|
*
|
||
|
* @public
|
||
|
* @param {String} expression Input expression
|
||
|
* @param {Object} [options] Parsing options
|
||
|
*/
|
||
|
CronExpression.parse = function parse(expression, options) {
|
||
|
var self = this;
|
||
|
if (typeof options === 'function') {
|
||
|
options = {};
|
||
|
}
|
||
|
|
||
|
function parse (expression, options) {
|
||
|
if (!options) {
|
||
|
options = {};
|
||
|
}
|
||
|
|
||
|
if (typeof options.currentDate === 'undefined') {
|
||
|
options.currentDate = new CronDate(undefined, self._tz);
|
||
|
}
|
||
|
|
||
|
// Is input expression predefined?
|
||
|
if (CronExpression.predefined[expression]) {
|
||
|
expression = CronExpression.predefined[expression];
|
||
|
}
|
||
|
|
||
|
// Split fields
|
||
|
var fields = [];
|
||
|
var atoms = (expression + '').trim().split(/\s+/);
|
||
|
|
||
|
if (atoms.length > 6) {
|
||
|
throw new Error('Invalid cron expression');
|
||
|
}
|
||
|
|
||
|
// Resolve fields
|
||
|
var start = (CronExpression.map.length - atoms.length);
|
||
|
for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
|
||
|
var field = CronExpression.map[i]; // Field name
|
||
|
var value = atoms[atoms.length > c ? i : i - start]; // Field value
|
||
|
|
||
|
if (i < start || !value) { // Use default value
|
||
|
fields.push(CronExpression._parseField(
|
||
|
field,
|
||
|
CronExpression.parseDefaults[i],
|
||
|
CronExpression.constraints[i]
|
||
|
)
|
||
|
);
|
||
|
} else {
|
||
|
var val = field === 'dayOfWeek' ? parseNthDay(value) : value;
|
||
|
|
||
|
fields.push(CronExpression._parseField(
|
||
|
field,
|
||
|
val,
|
||
|
CronExpression.constraints[i]
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var mappedFields = {};
|
||
|
for (var i = 0, c = CronExpression.map.length; i < c; i++) {
|
||
|
var key = CronExpression.map[i];
|
||
|
mappedFields[key] = fields[i];
|
||
|
}
|
||
|
|
||
|
var dayOfMonth = CronExpression._handleMaxDaysInMonth(mappedFields);
|
||
|
mappedFields.dayOfMonth = dayOfMonth || mappedFields.dayOfMonth;
|
||
|
return new CronExpression(mappedFields, options);
|
||
|
|
||
|
/**
|
||
|
* Parses out the # special character for the dayOfWeek field & adds it to options.
|
||
|
*
|
||
|
* @param {String} val
|
||
|
* @return {String}
|
||
|
* @private
|
||
|
*/
|
||
|
function parseNthDay(val) {
|
||
|
var atoms = val.split('#');
|
||
|
if (atoms.length > 1) {
|
||
|
var nthValue = +atoms[atoms.length - 1];
|
||
|
if(/,/.test(val)) {
|
||
|
throw new Error('Constraint error, invalid dayOfWeek `#` and `,` '
|
||
|
+ 'special characters are incompatible');
|
||
|
}
|
||
|
if(/\//.test(val)) {
|
||
|
throw new Error('Constraint error, invalid dayOfWeek `#` and `/` '
|
||
|
+ 'special characters are incompatible');
|
||
|
}
|
||
|
if(/-/.test(val)) {
|
||
|
throw new Error('Constraint error, invalid dayOfWeek `#` and `-` '
|
||
|
+ 'special characters are incompatible');
|
||
|
}
|
||
|
if (atoms.length > 2 || Number.isNaN(nthValue) || (nthValue < 1 || nthValue > 5)) {
|
||
|
throw new Error('Constraint error, invalid dayOfWeek occurrence number (#)');
|
||
|
}
|
||
|
|
||
|
options.nthDayOfWeek = nthValue;
|
||
|
return atoms[0];
|
||
|
}
|
||
|
return val;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return parse(expression, options);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Convert cron fields back to Cron Expression
|
||
|
*
|
||
|
* @public
|
||
|
* @param {Object} fields Input fields
|
||
|
* @param {Object} [options] Parsing options
|
||
|
* @return {Object}
|
||
|
*/
|
||
|
CronExpression.fieldsToExpression = function fieldsToExpression(fields, options) {
|
||
|
function validateConstraints (field, values, constraints) {
|
||
|
if (!values) {
|
||
|
throw new Error('Validation error, Field ' + field + ' is missing');
|
||
|
}
|
||
|
if (values.length === 0) {
|
||
|
throw new Error('Validation error, Field ' + field + ' contains no values');
|
||
|
}
|
||
|
for (var i = 0, c = values.length; i < c; i++) {
|
||
|
var value = values[i];
|
||
|
|
||
|
if (CronExpression._isValidConstraintChar(constraints, value)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Check constraints
|
||
|
if (typeof value !== 'number' || Number.isNaN(value) || value < constraints.min || value > constraints.max) {
|
||
|
throw new Error(
|
||
|
'Constraint error, got value ' + value + ' expected range ' +
|
||
|
constraints.min + '-' + constraints.max
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var mappedFields = {};
|
||
|
for (var i = 0, c = CronExpression.map.length; i < c; ++i) {
|
||
|
var field = CronExpression.map[i]; // Field name
|
||
|
var values = fields[field];
|
||
|
validateConstraints(
|
||
|
field,
|
||
|
values,
|
||
|
CronExpression.constraints[i]
|
||
|
);
|
||
|
var copy = [];
|
||
|
var j = -1;
|
||
|
while (++j < values.length) {
|
||
|
copy[j] = values[j];
|
||
|
}
|
||
|
values = copy.sort(CronExpression._sortCompareFn)
|
||
|
.filter(function(item, pos, ary) {
|
||
|
return !pos || item !== ary[pos - 1];
|
||
|
});
|
||
|
if (values.length !== copy.length) {
|
||
|
throw new Error('Validation error, Field ' + field + ' contains duplicate values');
|
||
|
}
|
||
|
mappedFields[field] = values;
|
||
|
}
|
||
|
var dayOfMonth = CronExpression._handleMaxDaysInMonth(mappedFields);
|
||
|
mappedFields.dayOfMonth = dayOfMonth || mappedFields.dayOfMonth;
|
||
|
return new CronExpression(mappedFields, options || {});
|
||
|
};
|
||
|
|
||
|
module.exports = CronExpression;
|