383 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			383 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| "use strict";
 | |
| Object.defineProperty(exports, "__esModule", { value: true });
 | |
| exports.CronExpressionParser = exports.DayOfWeek = exports.Months = exports.CronUnit = exports.PredefinedExpressions = void 0;
 | |
| const CronFieldCollection_1 = require("./CronFieldCollection");
 | |
| const CronExpression_1 = require("./CronExpression");
 | |
| const random_1 = require("./utils/random");
 | |
| const fields_1 = require("./fields");
 | |
| var PredefinedExpressions;
 | |
| (function (PredefinedExpressions) {
 | |
|     PredefinedExpressions["@yearly"] = "0 0 0 1 1 *";
 | |
|     PredefinedExpressions["@annually"] = "0 0 0 1 1 *";
 | |
|     PredefinedExpressions["@monthly"] = "0 0 0 1 * *";
 | |
|     PredefinedExpressions["@weekly"] = "0 0 0 * * 0";
 | |
|     PredefinedExpressions["@daily"] = "0 0 0 * * *";
 | |
|     PredefinedExpressions["@hourly"] = "0 0 * * * *";
 | |
|     PredefinedExpressions["@minutely"] = "0 * * * * *";
 | |
|     PredefinedExpressions["@secondly"] = "* * * * * *";
 | |
|     PredefinedExpressions["@weekdays"] = "0 0 0 * * 1-5";
 | |
|     PredefinedExpressions["@weekends"] = "0 0 0 * * 0,6";
 | |
| })(PredefinedExpressions || (exports.PredefinedExpressions = PredefinedExpressions = {}));
 | |
| var CronUnit;
 | |
| (function (CronUnit) {
 | |
|     CronUnit["Second"] = "Second";
 | |
|     CronUnit["Minute"] = "Minute";
 | |
|     CronUnit["Hour"] = "Hour";
 | |
|     CronUnit["DayOfMonth"] = "DayOfMonth";
 | |
|     CronUnit["Month"] = "Month";
 | |
|     CronUnit["DayOfWeek"] = "DayOfWeek";
 | |
| })(CronUnit || (exports.CronUnit = CronUnit = {}));
 | |
| // these need to be lowercase for the parser to work
 | |
| var Months;
 | |
| (function (Months) {
 | |
|     Months[Months["jan"] = 1] = "jan";
 | |
|     Months[Months["feb"] = 2] = "feb";
 | |
|     Months[Months["mar"] = 3] = "mar";
 | |
|     Months[Months["apr"] = 4] = "apr";
 | |
|     Months[Months["may"] = 5] = "may";
 | |
|     Months[Months["jun"] = 6] = "jun";
 | |
|     Months[Months["jul"] = 7] = "jul";
 | |
|     Months[Months["aug"] = 8] = "aug";
 | |
|     Months[Months["sep"] = 9] = "sep";
 | |
|     Months[Months["oct"] = 10] = "oct";
 | |
|     Months[Months["nov"] = 11] = "nov";
 | |
|     Months[Months["dec"] = 12] = "dec";
 | |
| })(Months || (exports.Months = Months = {}));
 | |
| // these need to be lowercase for the parser to work
 | |
| var DayOfWeek;
 | |
| (function (DayOfWeek) {
 | |
|     DayOfWeek[DayOfWeek["sun"] = 0] = "sun";
 | |
|     DayOfWeek[DayOfWeek["mon"] = 1] = "mon";
 | |
|     DayOfWeek[DayOfWeek["tue"] = 2] = "tue";
 | |
|     DayOfWeek[DayOfWeek["wed"] = 3] = "wed";
 | |
|     DayOfWeek[DayOfWeek["thu"] = 4] = "thu";
 | |
|     DayOfWeek[DayOfWeek["fri"] = 5] = "fri";
 | |
|     DayOfWeek[DayOfWeek["sat"] = 6] = "sat";
 | |
| })(DayOfWeek || (exports.DayOfWeek = DayOfWeek = {}));
 | |
| /**
 | |
|  * Static class that parses a cron expression and returns a CronExpression object.
 | |
|  * @static
 | |
|  * @class CronExpressionParser
 | |
|  */
 | |
| class CronExpressionParser {
 | |
|     /**
 | |
|      * Parses a cron expression and returns a CronExpression object.
 | |
|      * @param {string} expression - The cron expression to parse.
 | |
|      * @param {CronExpressionOptions} [options={}] - The options to use when parsing the expression.
 | |
|      * @param {boolean} [options.strict=false] - If true, will throw an error if the expression contains both dayOfMonth and dayOfWeek.
 | |
|      * @param {CronDate} [options.currentDate=new CronDate(undefined, 'UTC')] - The date to use when calculating the next/previous occurrence.
 | |
|      *
 | |
|      * @returns {CronExpression} A CronExpression object.
 | |
|      */
 | |
|     static parse(expression, options = {}) {
 | |
|         const { strict = false, hashSeed } = options;
 | |
|         const rand = (0, random_1.seededRandom)(hashSeed);
 | |
|         expression = PredefinedExpressions[expression] || expression;
 | |
|         const rawFields = CronExpressionParser.#getRawFields(expression, strict);
 | |
|         if (!(rawFields.dayOfMonth === '*' || rawFields.dayOfWeek === '*' || !strict)) {
 | |
|             throw new Error('Cannot use both dayOfMonth and dayOfWeek together in strict mode!');
 | |
|         }
 | |
|         const second = CronExpressionParser.#parseField(CronUnit.Second, rawFields.second, fields_1.CronSecond.constraints, rand);
 | |
|         const minute = CronExpressionParser.#parseField(CronUnit.Minute, rawFields.minute, fields_1.CronMinute.constraints, rand);
 | |
|         const hour = CronExpressionParser.#parseField(CronUnit.Hour, rawFields.hour, fields_1.CronHour.constraints, rand);
 | |
|         const month = CronExpressionParser.#parseField(CronUnit.Month, rawFields.month, fields_1.CronMonth.constraints, rand);
 | |
|         const dayOfMonth = CronExpressionParser.#parseField(CronUnit.DayOfMonth, rawFields.dayOfMonth, fields_1.CronDayOfMonth.constraints, rand);
 | |
|         const { dayOfWeek: _dayOfWeek, nthDayOfWeek } = CronExpressionParser.#parseNthDay(rawFields.dayOfWeek);
 | |
|         const dayOfWeek = CronExpressionParser.#parseField(CronUnit.DayOfWeek, _dayOfWeek, fields_1.CronDayOfWeek.constraints, rand);
 | |
|         const fields = new CronFieldCollection_1.CronFieldCollection({
 | |
|             second: new fields_1.CronSecond(second, { rawValue: rawFields.second }),
 | |
|             minute: new fields_1.CronMinute(minute, { rawValue: rawFields.minute }),
 | |
|             hour: new fields_1.CronHour(hour, { rawValue: rawFields.hour }),
 | |
|             dayOfMonth: new fields_1.CronDayOfMonth(dayOfMonth, { rawValue: rawFields.dayOfMonth }),
 | |
|             month: new fields_1.CronMonth(month, { rawValue: rawFields.month }),
 | |
|             dayOfWeek: new fields_1.CronDayOfWeek(dayOfWeek, { rawValue: rawFields.dayOfWeek, nthDayOfWeek }),
 | |
|         });
 | |
|         return new CronExpression_1.CronExpression(fields, { ...options, expression });
 | |
|     }
 | |
|     /**
 | |
|      * Get the raw fields from a cron expression.
 | |
|      * @param {string} expression - The cron expression to parse.
 | |
|      * @param {boolean} strict - If true, will throw an error if the expression contains both dayOfMonth and dayOfWeek.
 | |
|      * @private
 | |
|      * @returns {RawCronFields} The raw fields.
 | |
|      */
 | |
|     static #getRawFields(expression, strict) {
 | |
|         if (strict && !expression.length) {
 | |
|             throw new Error('Invalid cron expression');
 | |
|         }
 | |
|         expression = expression || '0 * * * * *';
 | |
|         const atoms = expression.trim().split(/\s+/);
 | |
|         if (strict && atoms.length < 6) {
 | |
|             throw new Error('Invalid cron expression, expected 6 fields');
 | |
|         }
 | |
|         if (atoms.length > 6) {
 | |
|             throw new Error('Invalid cron expression, too many fields');
 | |
|         }
 | |
|         const defaults = ['*', '*', '*', '*', '*', '0'];
 | |
|         if (atoms.length < defaults.length) {
 | |
|             atoms.unshift(...defaults.slice(atoms.length));
 | |
|         }
 | |
|         const [second, minute, hour, dayOfMonth, month, dayOfWeek] = atoms;
 | |
|         return { second, minute, hour, dayOfMonth, month, dayOfWeek };
 | |
|     }
 | |
|     /**
 | |
|      * Parse a field from a cron expression.
 | |
|      * @param {CronUnit} field - The field to parse.
 | |
|      * @param {string} value - The value of the field.
 | |
|      * @param {CronConstraints} constraints - The constraints for the field.
 | |
|      * @private
 | |
|      * @returns {(number | string)[]} The parsed field.
 | |
|      */
 | |
|     static #parseField(field, value, constraints, rand) {
 | |
|         // Replace aliases for month and dayOfWeek
 | |
|         if (field === CronUnit.Month || field === CronUnit.DayOfWeek) {
 | |
|             value = value.replace(/[a-z]{3}/gi, (match) => {
 | |
|                 match = match.toLowerCase();
 | |
|                 const replacer = Months[match] || DayOfWeek[match];
 | |
|                 if (replacer === undefined) {
 | |
|                     throw new Error(`Validation error, cannot resolve alias "${match}"`);
 | |
|                 }
 | |
|                 return replacer.toString();
 | |
|             });
 | |
|         }
 | |
|         // Check for valid characters
 | |
|         if (!constraints.validChars.test(value)) {
 | |
|             throw new Error(`Invalid characters, got value: ${value}`);
 | |
|         }
 | |
|         value = this.#parseWildcard(value, constraints);
 | |
|         value = this.#parseHashed(value, constraints, rand);
 | |
|         return this.#parseSequence(field, value, constraints);
 | |
|     }
 | |
|     /**
 | |
|      * Parse a wildcard from a cron expression.
 | |
|      * @param {string} value - The value to parse.
 | |
|      * @param {CronConstraints} constraints - The constraints for the field.
 | |
|      * @private
 | |
|      */
 | |
|     static #parseWildcard(value, constraints) {
 | |
|         return value.replace(/[*?]/g, constraints.min + '-' + constraints.max);
 | |
|     }
 | |
|     /**
 | |
|      * Parse a hashed value from a cron expression.
 | |
|      * @param {string} value - The value to parse.
 | |
|      * @param {CronConstraints} constraints - The constraints for the field.
 | |
|      * @param {PRNG} rand - The random number generator to use.
 | |
|      * @private
 | |
|      */
 | |
|     static #parseHashed(value, constraints, rand) {
 | |
|         const randomValue = rand();
 | |
|         return value.replace(/H(?:\((\d+)-(\d+)\))?(?:\/(\d+))?/g, (_, min, max, step) => {
 | |
|             // H(range)/step
 | |
|             if (min && max && step) {
 | |
|                 const minNum = parseInt(min, 10);
 | |
|                 const maxNum = parseInt(max, 10);
 | |
|                 const stepNum = parseInt(step, 10);
 | |
|                 if (minNum > maxNum) {
 | |
|                     throw new Error(`Invalid range: ${minNum}-${maxNum}, min > max`);
 | |
|                 }
 | |
|                 if (stepNum <= 0) {
 | |
|                     throw new Error(`Invalid step: ${stepNum}, must be positive`);
 | |
|                 }
 | |
|                 const minStart = Math.max(minNum, constraints.min);
 | |
|                 const offset = Math.floor(randomValue * stepNum);
 | |
|                 const values = [];
 | |
|                 for (let i = Math.floor(minStart / stepNum) * stepNum + offset; i <= maxNum; i += stepNum) {
 | |
|                     if (i >= minStart) {
 | |
|                         values.push(i);
 | |
|                     }
 | |
|                 }
 | |
|                 return values.join(',');
 | |
|             }
 | |
|             // H(range)
 | |
|             else if (min && max) {
 | |
|                 const minNum = parseInt(min, 10);
 | |
|                 const maxNum = parseInt(max, 10);
 | |
|                 if (minNum > maxNum) {
 | |
|                     throw new Error(`Invalid range: ${minNum}-${maxNum}, min > max`);
 | |
|                 }
 | |
|                 return String(Math.floor(randomValue * (maxNum - minNum + 1)) + minNum);
 | |
|             }
 | |
|             // H/step
 | |
|             else if (step) {
 | |
|                 const stepNum = parseInt(step, 10);
 | |
|                 // Validate step
 | |
|                 if (stepNum <= 0) {
 | |
|                     throw new Error(`Invalid step: ${stepNum}, must be positive`);
 | |
|                 }
 | |
|                 const offset = Math.floor(randomValue * stepNum);
 | |
|                 const values = [];
 | |
|                 for (let i = Math.floor(constraints.min / stepNum) * stepNum + offset; i <= constraints.max; i += stepNum) {
 | |
|                     if (i >= constraints.min) {
 | |
|                         values.push(i);
 | |
|                     }
 | |
|                 }
 | |
|                 return values.join(',');
 | |
|             }
 | |
|             // H
 | |
|             else {
 | |
|                 return String(Math.floor(randomValue * (constraints.max - constraints.min + 1) + constraints.min));
 | |
|             }
 | |
|         });
 | |
|     }
 | |
|     /**
 | |
|      * Parse a sequence from a cron expression.
 | |
|      * @param {CronUnit} field - The field to parse.
 | |
|      * @param {string} val - The sequence to parse.
 | |
|      * @param {CronConstraints} constraints - The constraints for the field.
 | |
|      * @private
 | |
|      */
 | |
|     static #parseSequence(field, val, constraints) {
 | |
|         const stack = [];
 | |
|         function handleResult(result, constraints) {
 | |
|             if (Array.isArray(result)) {
 | |
|                 stack.push(...result);
 | |
|             }
 | |
|             else {
 | |
|                 if (CronExpressionParser.#isValidConstraintChar(constraints, result)) {
 | |
|                     stack.push(result);
 | |
|                 }
 | |
|                 else {
 | |
|                     const v = parseInt(result.toString(), 10);
 | |
|                     const isValid = v >= constraints.min && v <= constraints.max;
 | |
|                     if (!isValid) {
 | |
|                         throw new Error(`Constraint error, got value ${result} expected range ${constraints.min}-${constraints.max}`);
 | |
|                     }
 | |
|                     stack.push(field === CronUnit.DayOfWeek ? v % 7 : result);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         const atoms = val.split(',');
 | |
|         atoms.forEach((atom) => {
 | |
|             if (!(atom.length > 0)) {
 | |
|                 throw new Error('Invalid list value format');
 | |
|             }
 | |
|             handleResult(CronExpressionParser.#parseRepeat(field, atom, constraints), constraints);
 | |
|         });
 | |
|         return stack;
 | |
|     }
 | |
|     /**
 | |
|      * Parse repeat from a cron expression.
 | |
|      * @param {CronUnit} field - The field to parse.
 | |
|      * @param {string} val - The repeat to parse.
 | |
|      * @param {CronConstraints} constraints - The constraints for the field.
 | |
|      * @private
 | |
|      * @returns {(number | string)[]} The parsed repeat.
 | |
|      */
 | |
|     static #parseRepeat(field, val, constraints) {
 | |
|         const atoms = val.split('/');
 | |
|         if (atoms.length > 2) {
 | |
|             throw new Error(`Invalid repeat: ${val}`);
 | |
|         }
 | |
|         if (atoms.length === 2) {
 | |
|             if (!isNaN(parseInt(atoms[0], 10))) {
 | |
|                 atoms[0] = `${atoms[0]}-${constraints.max}`;
 | |
|             }
 | |
|             return CronExpressionParser.#parseRange(field, atoms[0], parseInt(atoms[1], 10), constraints);
 | |
|         }
 | |
|         return CronExpressionParser.#parseRange(field, val, 1, constraints);
 | |
|     }
 | |
|     /**
 | |
|      * Validate a cron range.
 | |
|      * @param {number} min - The minimum value of the range.
 | |
|      * @param {number} max - The maximum value of the range.
 | |
|      * @param {CronConstraints} constraints - The constraints for the field.
 | |
|      * @private
 | |
|      * @returns {void}
 | |
|      * @throws {Error} Throws an error if the range is invalid.
 | |
|      */
 | |
|     static #validateRange(min, max, constraints) {
 | |
|         const isValid = !isNaN(min) && !isNaN(max) && min >= constraints.min && max <= constraints.max;
 | |
|         if (!isValid) {
 | |
|             throw new Error(`Constraint error, got range ${min}-${max} expected range ${constraints.min}-${constraints.max}`);
 | |
|         }
 | |
|         if (min > max) {
 | |
|             throw new Error(`Invalid range: ${min}-${max}, min(${min}) > max(${max})`);
 | |
|         }
 | |
|     }
 | |
|     /**
 | |
|      * Validate a cron repeat interval.
 | |
|      * @param {number} repeatInterval - The repeat interval to validate.
 | |
|      * @private
 | |
|      * @returns {void}
 | |
|      * @throws {Error} Throws an error if the repeat interval is invalid.
 | |
|      */
 | |
|     static #validateRepeatInterval(repeatInterval) {
 | |
|         if (!(!isNaN(repeatInterval) && repeatInterval > 0)) {
 | |
|             throw new Error(`Constraint error, cannot repeat at every ${repeatInterval} time.`);
 | |
|         }
 | |
|     }
 | |
|     /**
 | |
|      * Create a range from a cron expression.
 | |
|      * @param {CronUnit} field - The field to parse.
 | |
|      * @param {number} min - The minimum value of the range.
 | |
|      * @param {number} max - The maximum value of the range.
 | |
|      * @param {number} repeatInterval - The repeat interval of the range.
 | |
|      * @private
 | |
|      * @returns {number[]} The created range.
 | |
|      */
 | |
|     static #createRange(field, min, max, repeatInterval) {
 | |
|         const stack = [];
 | |
|         if (field === CronUnit.DayOfWeek && max % 7 === 0) {
 | |
|             stack.push(0);
 | |
|         }
 | |
|         for (let index = min; index <= max; index += repeatInterval) {
 | |
|             if (stack.indexOf(index) === -1) {
 | |
|                 stack.push(index);
 | |
|             }
 | |
|         }
 | |
|         return stack;
 | |
|     }
 | |
|     /**
 | |
|      * Parse a range from a cron expression.
 | |
|      * @param {CronUnit} field - The field to parse.
 | |
|      * @param {string} val - The range to parse.
 | |
|      * @param {number} repeatInterval - The repeat interval of the range.
 | |
|      * @param {CronConstraints} constraints - The constraints for the field.
 | |
|      * @private
 | |
|      * @returns {number[] | string[] | number | string} The parsed range.
 | |
|      */
 | |
|     static #parseRange(field, val, repeatInterval, constraints) {
 | |
|         const atoms = val.split('-');
 | |
|         if (atoms.length <= 1) {
 | |
|             return isNaN(+val) ? val : +val;
 | |
|         }
 | |
|         const [min, max] = atoms.map((num) => parseInt(num, 10));
 | |
|         this.#validateRange(min, max, constraints);
 | |
|         this.#validateRepeatInterval(repeatInterval);
 | |
|         // Create range
 | |
|         return this.#createRange(field, min, max, repeatInterval);
 | |
|     }
 | |
|     /**
 | |
|      * Parse a cron expression.
 | |
|      * @param {string} val - The cron expression to parse.
 | |
|      * @private
 | |
|      * @returns {string} The parsed cron expression.
 | |
|      */
 | |
|     static #parseNthDay(val) {
 | |
|         const atoms = val.split('#');
 | |
|         if (atoms.length <= 1) {
 | |
|             return { dayOfWeek: atoms[0] };
 | |
|         }
 | |
|         const nthValue = +atoms[atoms.length - 1];
 | |
|         const matches = val.match(/([,-/])/);
 | |
|         if (matches !== null) {
 | |
|             throw new Error(`Constraint error, invalid dayOfWeek \`#\` and \`${matches?.[0]}\` special characters are incompatible`);
 | |
|         }
 | |
|         if (!(atoms.length <= 2 && !isNaN(nthValue) && nthValue >= 1 && nthValue <= 5)) {
 | |
|             throw new Error('Constraint error, invalid dayOfWeek occurrence number (#)');
 | |
|         }
 | |
|         return { dayOfWeek: atoms[0], nthDayOfWeek: nthValue };
 | |
|     }
 | |
|     /**
 | |
|      * Checks if a character is valid for a field.
 | |
|      * @param {CronConstraints} constraints - The constraints for the field.
 | |
|      * @param {string | number} value - The value to check.
 | |
|      * @private
 | |
|      * @returns {boolean} Whether the character is valid for the field.
 | |
|      */
 | |
|     static #isValidConstraintChar(constraints, value) {
 | |
|         return constraints.chars.some((char) => value.toString().includes(char));
 | |
|     }
 | |
| }
 | |
| exports.CronExpressionParser = CronExpressionParser;
 |