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

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