UNPKG

cron-parser

Version:

Node.js library for parsing crontab instructions

383 lines (382 loc) 17.4 kB
"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;