408 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			408 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| "use strict";
 | |
| Object.defineProperty(exports, "__esModule", { value: true });
 | |
| exports.CronExpression = exports.LOOPS_LIMIT_EXCEEDED_ERROR_MESSAGE = exports.TIME_SPAN_OUT_OF_BOUNDS_ERROR_MESSAGE = void 0;
 | |
| const CronDate_1 = require("./CronDate");
 | |
| /**
 | |
|  * Error message for when the current date is outside the specified time span.
 | |
|  */
 | |
| exports.TIME_SPAN_OUT_OF_BOUNDS_ERROR_MESSAGE = 'Out of the time span range';
 | |
| /**
 | |
|  * Error message for when the loop limit is exceeded during iteration.
 | |
|  */
 | |
| exports.LOOPS_LIMIT_EXCEEDED_ERROR_MESSAGE = 'Invalid expression, loop limit exceeded';
 | |
| /**
 | |
|  * Cron iteration loop safety limit
 | |
|  */
 | |
| const LOOP_LIMIT = 10000;
 | |
| /**
 | |
|  * Class representing a Cron expression.
 | |
|  */
 | |
| class CronExpression {
 | |
|     #options;
 | |
|     #tz;
 | |
|     #currentDate;
 | |
|     #startDate;
 | |
|     #endDate;
 | |
|     #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.#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;
 | |
|         let currentDateValue = options.currentDate ?? options.startDate;
 | |
|         if (currentDateValue) {
 | |
|             const tempCurrentDate = new CronDate_1.CronDate(currentDateValue, this.#tz);
 | |
|             if (this.#startDate && tempCurrentDate.getTime() < this.#startDate.getTime()) {
 | |
|                 currentDateValue = this.#startDate;
 | |
|             }
 | |
|             else if (this.#endDate && tempCurrentDate.getTime() > this.#endDate.getTime()) {
 | |
|                 currentDateValue = this.#endDate;
 | |
|             }
 | |
|         }
 | |
|         this.#currentDate = new CronDate_1.CronDate(currentDateValue, this.#tz);
 | |
|         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.#fields.dayOfWeek.nthDay > 0) {
 | |
|             const weekInMonth = Math.ceil(dt.getDate() / 7);
 | |
|             if (weekInMonth !== this.#fields.dayOfWeek.nthDay) {
 | |
|                 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;
 | |
|     }
 | |
|     /**
 | |
|      * Validates the current date against the start and end dates of the cron expression.
 | |
|      * If the current date is outside the specified time span, an error is thrown.
 | |
|      *
 | |
|      * @param currentDate {CronDate} - The current date to validate.
 | |
|      * @throws {Error} If the current date is outside the specified time span.
 | |
|      * @private
 | |
|      */
 | |
|     #validateTimeSpan(currentDate) {
 | |
|         if (!this.#startDate && !this.#endDate) {
 | |
|             return;
 | |
|         }
 | |
|         const currentTime = currentDate.getTime();
 | |
|         if (this.#startDate && currentTime < this.#startDate.getTime()) {
 | |
|             throw new Error(exports.TIME_SPAN_OUT_OF_BOUNDS_ERROR_MESSAGE);
 | |
|         }
 | |
|         if (this.#endDate && currentTime > this.#endDate.getTime()) {
 | |
|             throw new Error(exports.TIME_SPAN_OUT_OF_BOUNDS_ERROR_MESSAGE);
 | |
|         }
 | |
|     }
 | |
|     /**
 | |
|      * 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 startTimestamp = currentDate.getTime();
 | |
|         let stepCount = 0;
 | |
|         while (++stepCount < LOOP_LIMIT) {
 | |
|             this.#validateTimeSpan(currentDate);
 | |
|             if (!this.#matchDayOfMonth(currentDate)) {
 | |
|                 currentDate.applyDateOperation(dateMathVerb, CronDate_1.TimeUnit.Day, this.#fields.hour.values.length);
 | |
|                 continue;
 | |
|             }
 | |
|             if (!(this.#fields.dayOfWeek.nthDay <= 0 || Math.ceil(currentDate.getDate() / 7) === this.#fields.dayOfWeek.nthDay)) {
 | |
|                 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;
 | |
|         }
 | |
|         /* istanbul ignore next - should be impossible under normal use to trigger the branch */
 | |
|         if (stepCount > LOOP_LIMIT) {
 | |
|             throw new Error(exports.LOOPS_LIMIT_EXCEEDED_ERROR_MESSAGE);
 | |
|         }
 | |
|         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;
 |