UNPKG

cron-parser

Version:

Node.js library for parsing crontab instructions

379 lines (378 loc) 16 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CronExpression = void 0; const CronDate_1 = require("./CronDate"); /** * Cron iteration loop safety limit */ const LOOP_LIMIT = 10000; /** * Class representing a Cron expression. */ class CronExpression { #options; #tz; #currentDate; #startDate; #endDate; #nthDayOfWeek; #fields; /** * Creates a new CronExpression instance. * * @param {CronFieldCollection} fields - Cron fields. * @param {CronExpressionOptions} options - Parser options. */ constructor(fields, options) { this.#options = options; this.#tz = options.tz; this.#currentDate = new CronDate_1.CronDate(options.currentDate, this.#tz); this.#startDate = options.startDate ? new CronDate_1.CronDate(options.startDate, this.#tz) : null; this.#endDate = options.endDate ? new CronDate_1.CronDate(options.endDate, this.#tz) : null; this.#nthDayOfWeek = options.nthDayOfWeek || 0; this.#fields = fields; } /** * Getter for the cron fields. * * @returns {CronFieldCollection} Cron fields. */ get fields() { return this.#fields; } /** * Converts cron fields back to a CronExpression instance. * * @public * @param {Record<string, number[]>} fields - The input cron fields object. * @param {CronExpressionOptions} [options] - Optional parsing options. * @returns {CronExpression} - A new CronExpression instance. */ static fieldsToExpression(fields, options) { return new CronExpression(fields, options || {}); } /** * Checks if the given value matches any element in the sequence. * * @param {number} value - The value to be matched. * @param {number[]} sequence - The sequence to be checked against. * @returns {boolean} - True if the value matches an element in the sequence; otherwise, false. * @memberof CronExpression * @private */ static #matchSchedule(value, sequence) { return sequence.some((element) => element === value); } /** * Determines if the current date matches the last specified weekday of the month. * * @param {Array<(number|string)>} expressions - An array of expressions containing weekdays and "L" for the last weekday. * @param {CronDate} currentDate - The current date object. * @returns {boolean} - True if the current date matches the last specified weekday of the month; otherwise, false. * @memberof CronExpression * @private */ static #isLastWeekdayOfMonthMatch(expressions, currentDate) { const isLastWeekdayOfMonth = currentDate.isLastWeekdayOfMonth(); return expressions.some((expression) => { // The first character represents the weekday const weekday = parseInt(expression.toString().charAt(0), 10) % 7; if (Number.isNaN(weekday)) { throw new Error(`Invalid last weekday of the month expression: ${expression}`); } // Check if the current date matches the last specified weekday of the month return currentDate.getDay() === weekday && isLastWeekdayOfMonth; }); } /** * Find the next scheduled date based on the cron expression. * @returns {CronDate} - The next scheduled date or an ES6 compatible iterator object. * @memberof CronExpression * @public */ next() { return this.#findSchedule(); } /** * Find the previous scheduled date based on the cron expression. * @returns {CronDate} - The previous scheduled date or an ES6 compatible iterator object. * @memberof CronExpression * @public */ prev() { return this.#findSchedule(true); } /** * Check if there is a next scheduled date based on the current date and cron expression. * @returns {boolean} - Returns true if there is a next scheduled date, false otherwise. * @memberof CronExpression * @public */ hasNext() { const current = this.#currentDate; try { this.#findSchedule(); return true; } catch { return false; } finally { this.#currentDate = current; } } /** * Check if there is a previous scheduled date based on the current date and cron expression. * @returns {boolean} - Returns true if there is a previous scheduled date, false otherwise. * @memberof CronExpression * @public */ hasPrev() { const current = this.#currentDate; try { this.#findSchedule(true); return true; } catch { return false; } finally { this.#currentDate = current; } } /** * Iterate over a specified number of steps and optionally execute a callback function for each step. * @param {number} steps - The number of steps to iterate. Positive value iterates forward, negative value iterates backward. * @returns {CronDate[]} - An array of iterator fields or CronDate objects. * @memberof CronExpression * @public */ take(limit) { const items = []; if (limit >= 0) { for (let i = 0; i < limit; i++) { try { items.push(this.next()); } catch { return items; } } } else { for (let i = 0; i > limit; i--) { try { items.push(this.prev()); } catch { return items; } } } return items; } /** * Reset the iterators current date to a new date or the initial date. * @param {Date | CronDate} [newDate] - Optional new date to reset to. If not provided, it will reset to the initial date. * @memberof CronExpression * @public */ reset(newDate) { this.#currentDate = new CronDate_1.CronDate(newDate || this.#options.currentDate); } /** * Generate a string representation of the cron expression. * @param {boolean} [includeSeconds=false] - Whether to include the seconds field in the string representation. * @returns {string} - The string representation of the cron expression. * @memberof CronExpression * @public */ stringify(includeSeconds = false) { return this.#fields.stringify(includeSeconds); } /** * Check if the cron expression includes the given date * @param {Date|CronDate} date * @returns {boolean} */ includesDate(date) { const { second, minute, hour, month } = this.#fields; const dt = new CronDate_1.CronDate(date, this.#tz); // Check basic time fields first if (!second.values.includes(dt.getSeconds()) || !minute.values.includes(dt.getMinutes()) || !hour.values.includes(dt.getHours()) || !month.values.includes((dt.getMonth() + 1))) { return false; } // Check day of month and day of week using the same logic as #findSchedule if (!this.#matchDayOfMonth(dt)) { return false; } // Check nth day of week if specified if (this.#nthDayOfWeek > 0) { const weekInMonth = Math.ceil(dt.getDate() / 7); if (weekInMonth !== this.#nthDayOfWeek) { return false; } } return true; } /** * Returns the string representation of the cron expression. * @returns {CronDate} - The next schedule date. */ toString() { /* istanbul ignore next - should be impossible under normal use to trigger the or branch */ return this.#options.expression || this.stringify(true); } /** * Determines if the given date matches the cron expression's day of month and day of week fields. * * The function checks the following rules: * Rule 1: If both "day of month" and "day of week" are restricted (not wildcard), then one or both must match the current day. * Rule 2: If "day of month" is restricted and "day of week" is not restricted, then "day of month" must match the current day. * Rule 3: If "day of month" is a wildcard, "day of week" is not a wildcard, and "day of week" matches the current day, then the match is accepted. * If none of the rules match, the match is rejected. * * @param {CronDate} currentDate - The current date to be evaluated against the cron expression. * @returns {boolean} Returns true if the current date matches the cron expression's day of month and day of week fields, otherwise false. * @memberof CronExpression * @private */ #matchDayOfMonth(currentDate) { // Check if day of month and day of week fields are wildcards or restricted (not wildcard). const isDayOfMonthWildcardMatch = this.#fields.dayOfMonth.isWildcard; const isRestrictedDayOfMonth = !isDayOfMonthWildcardMatch; const isDayOfWeekWildcardMatch = this.#fields.dayOfWeek.isWildcard; const isRestrictedDayOfWeek = !isDayOfWeekWildcardMatch; // Calculate if the current date matches the day of month and day of week fields. const matchedDOM = CronExpression.#matchSchedule(currentDate.getDate(), this.#fields.dayOfMonth.values) || (this.#fields.dayOfMonth.hasLastChar && currentDate.isLastDayOfMonth()); const matchedDOW = CronExpression.#matchSchedule(currentDate.getDay(), this.#fields.dayOfWeek.values) || (this.#fields.dayOfWeek.hasLastChar && CronExpression.#isLastWeekdayOfMonthMatch(this.#fields.dayOfWeek.values, currentDate)); // Rule 1: Both "day of month" and "day of week" are restricted; one or both must match the current day. if (isRestrictedDayOfMonth && isRestrictedDayOfWeek && (matchedDOM || matchedDOW)) { return true; } // Rule 2: "day of month" restricted and "day of week" not restricted; "day of month" must match the current day. if (matchedDOM && !isRestrictedDayOfWeek) { return true; } // Rule 3: "day of month" is a wildcard, "day of week" is not a wildcard, and "day of week" matches the current day. if (isDayOfMonthWildcardMatch && !isDayOfWeekWildcardMatch && matchedDOW) { return true; } // If none of the rules match, the match is rejected. return false; } /** * Determines if the current hour matches the cron expression. * * @param {CronDate} currentDate - The current date object. * @param {DateMathOp} dateMathVerb - The date math operation enumeration value. * @param {boolean} reverse - A flag indicating whether the matching should be done in reverse order. * @returns {boolean} - True if the current hour matches the cron expression; otherwise, false. */ #matchHour(currentDate, dateMathVerb, reverse) { const currentHour = currentDate.getHours(); const isMatch = CronExpression.#matchSchedule(currentHour, this.#fields.hour.values); const isDstStart = currentDate.dstStart === currentHour; const isDstEnd = currentDate.dstEnd === currentHour; if (!isMatch && !isDstStart) { currentDate.dstStart = null; currentDate.applyDateOperation(dateMathVerb, CronDate_1.TimeUnit.Hour, this.#fields.hour.values.length); return false; } if (isDstStart && !CronExpression.#matchSchedule(currentHour - 1, this.#fields.hour.values)) { currentDate.invokeDateOperation(dateMathVerb, CronDate_1.TimeUnit.Hour); return false; } if (isDstEnd && !reverse) { currentDate.dstEnd = null; currentDate.applyDateOperation(CronDate_1.DateMathOp.Add, CronDate_1.TimeUnit.Hour, this.#fields.hour.values.length); return false; } return true; } /** * Finds the next or previous schedule based on the cron expression. * * @param {boolean} [reverse=false] - If true, finds the previous schedule; otherwise, finds the next schedule. * @returns {CronDate} - The next or previous schedule date. * @private */ #findSchedule(reverse = false) { const dateMathVerb = reverse ? CronDate_1.DateMathOp.Subtract : CronDate_1.DateMathOp.Add; const currentDate = new CronDate_1.CronDate(this.#currentDate); const startDate = this.#startDate; const endDate = this.#endDate; const startTimestamp = currentDate.getTime(); let stepCount = 0; while (stepCount++ < LOOP_LIMIT) { /* istanbul ignore next - should be impossible under normal use to trigger the branch */ if (stepCount > LOOP_LIMIT) { throw new Error('Invalid expression, loop limit exceeded'); } if (reverse && startDate && startDate.getTime() > currentDate.getTime()) { throw new Error('Out of the timespan range'); } if (!reverse && endDate && currentDate.getTime() > endDate.getTime()) { throw new Error('Out of the timespan range'); } if (!this.#matchDayOfMonth(currentDate)) { currentDate.applyDateOperation(dateMathVerb, CronDate_1.TimeUnit.Day, this.#fields.hour.values.length); continue; } if (!(this.#nthDayOfWeek <= 0 || Math.ceil(currentDate.getDate() / 7) === this.#nthDayOfWeek)) { currentDate.applyDateOperation(dateMathVerb, CronDate_1.TimeUnit.Day, this.#fields.hour.values.length); continue; } if (!CronExpression.#matchSchedule(currentDate.getMonth() + 1, this.#fields.month.values)) { currentDate.applyDateOperation(dateMathVerb, CronDate_1.TimeUnit.Month, this.#fields.hour.values.length); continue; } if (!this.#matchHour(currentDate, dateMathVerb, reverse)) { continue; } if (!CronExpression.#matchSchedule(currentDate.getMinutes(), this.#fields.minute.values)) { currentDate.applyDateOperation(dateMathVerb, CronDate_1.TimeUnit.Minute, this.#fields.hour.values.length); continue; } if (!CronExpression.#matchSchedule(currentDate.getSeconds(), this.#fields.second.values)) { currentDate.applyDateOperation(dateMathVerb, CronDate_1.TimeUnit.Second, this.#fields.hour.values.length); continue; } if (startTimestamp === currentDate.getTime()) { if (dateMathVerb === 'Add' || currentDate.getMilliseconds() === 0) { currentDate.applyDateOperation(dateMathVerb, CronDate_1.TimeUnit.Second, this.#fields.hour.values.length); } continue; } break; } if (currentDate.getMilliseconds() !== 0) { currentDate.setMilliseconds(0); } this.#currentDate = currentDate; return currentDate; } /** * Returns an iterator for iterating through future CronDate instances * * @name Symbol.iterator * @memberof CronExpression * @returns {Iterator<CronDate>} An iterator object for CronExpression that returns CronDate values. */ [Symbol.iterator]() { return { next: () => { const schedule = this.#findSchedule(); return { value: schedule, done: !this.hasNext() }; }, }; } } exports.CronExpression = CronExpression; exports.default = CronExpression;