123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002 |
- '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;
|