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