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