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
			| 
											3 years 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; |