372 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			372 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| "use strict";
 | |
| Object.defineProperty(exports, "__esModule", { value: true });
 | |
| exports.CronFieldCollection = void 0;
 | |
| const fields_1 = require("./fields");
 | |
| /**
 | |
|  * Represents a complete set of cron fields.
 | |
|  * @class CronFieldCollection
 | |
|  */
 | |
| class CronFieldCollection {
 | |
|     #second;
 | |
|     #minute;
 | |
|     #hour;
 | |
|     #dayOfMonth;
 | |
|     #month;
 | |
|     #dayOfWeek;
 | |
|     /**
 | |
|      * Creates a new CronFieldCollection instance by partially overriding fields from an existing one.
 | |
|      * @param {CronFieldCollection} base - The base CronFieldCollection to copy fields from
 | |
|      * @param {CronFieldOverride} fields - The fields to override, can be CronField instances or raw values
 | |
|      * @returns {CronFieldCollection} A new CronFieldCollection instance
 | |
|      * @example
 | |
|      * const base = new CronFieldCollection({
 | |
|      *   second: new CronSecond([0]),
 | |
|      *   minute: new CronMinute([0]),
 | |
|      *   hour: new CronHour([12]),
 | |
|      *   dayOfMonth: new CronDayOfMonth([1]),
 | |
|      *   month: new CronMonth([1]),
 | |
|      *   dayOfWeek: new CronDayOfWeek([1])
 | |
|      * });
 | |
|      *
 | |
|      * // Using CronField instances
 | |
|      * const modified1 = CronFieldCollection.from(base, {
 | |
|      *   hour: new CronHour([15]),
 | |
|      *   minute: new CronMinute([30])
 | |
|      * });
 | |
|      *
 | |
|      * // Using raw values
 | |
|      * const modified2 = CronFieldCollection.from(base, {
 | |
|      *   hour: [15],        // Will create new CronHour
 | |
|      *   minute: [30]       // Will create new CronMinute
 | |
|      * });
 | |
|      */
 | |
|     static from(base, fields) {
 | |
|         return new CronFieldCollection({
 | |
|             second: this.resolveField(fields_1.CronSecond, base.second, fields.second),
 | |
|             minute: this.resolveField(fields_1.CronMinute, base.minute, fields.minute),
 | |
|             hour: this.resolveField(fields_1.CronHour, base.hour, fields.hour),
 | |
|             dayOfMonth: this.resolveField(fields_1.CronDayOfMonth, base.dayOfMonth, fields.dayOfMonth),
 | |
|             month: this.resolveField(fields_1.CronMonth, base.month, fields.month),
 | |
|             dayOfWeek: this.resolveField(fields_1.CronDayOfWeek, base.dayOfWeek, fields.dayOfWeek),
 | |
|         });
 | |
|     }
 | |
|     /**
 | |
|      * Resolves a field value, either using the provided CronField instance or creating a new one from raw values.
 | |
|      * @param constructor - The constructor for creating new field instances
 | |
|      * @param baseField - The base field to use if no override is provided
 | |
|      * @param fieldValue - The override value, either a CronField instance or raw values
 | |
|      * @returns The resolved CronField instance
 | |
|      * @private
 | |
|      */
 | |
|     static resolveField(constructor, baseField, fieldValue) {
 | |
|         if (!fieldValue) {
 | |
|             return baseField;
 | |
|         }
 | |
|         if (fieldValue instanceof fields_1.CronField) {
 | |
|             return fieldValue;
 | |
|         }
 | |
|         return new constructor(fieldValue);
 | |
|     }
 | |
|     /**
 | |
|      * CronFieldCollection constructor. Initializes the cron fields with the provided values.
 | |
|      * @param {CronFields} param0 - The cron fields values
 | |
|      * @throws {Error} if validation fails
 | |
|      * @example
 | |
|      * const cronFields = new CronFieldCollection({
 | |
|      *   second: new CronSecond([0]),
 | |
|      *   minute: new CronMinute([0, 30]),
 | |
|      *   hour: new CronHour([9]),
 | |
|      *   dayOfMonth: new CronDayOfMonth([15]),
 | |
|      *   month: new CronMonth([1]),
 | |
|      *   dayOfWeek: new CronDayOfTheWeek([1, 2, 3, 4, 5]),
 | |
|      * })
 | |
|      *
 | |
|      * console.log(cronFields.second.values); // [0]
 | |
|      * console.log(cronFields.minute.values); // [0, 30]
 | |
|      * console.log(cronFields.hour.values); // [9]
 | |
|      * console.log(cronFields.dayOfMonth.values); // [15]
 | |
|      * console.log(cronFields.month.values); // [1]
 | |
|      * console.log(cronFields.dayOfWeek.values); // [1, 2, 3, 4, 5]
 | |
|      */
 | |
|     constructor({ second, minute, hour, dayOfMonth, month, dayOfWeek }) {
 | |
|         if (!second) {
 | |
|             throw new Error('Validation error, Field second is missing');
 | |
|         }
 | |
|         if (!minute) {
 | |
|             throw new Error('Validation error, Field minute is missing');
 | |
|         }
 | |
|         if (!hour) {
 | |
|             throw new Error('Validation error, Field hour is missing');
 | |
|         }
 | |
|         if (!dayOfMonth) {
 | |
|             throw new Error('Validation error, Field dayOfMonth is missing');
 | |
|         }
 | |
|         if (!month) {
 | |
|             throw new Error('Validation error, Field month is missing');
 | |
|         }
 | |
|         if (!dayOfWeek) {
 | |
|             throw new Error('Validation error, Field dayOfWeek is missing');
 | |
|         }
 | |
|         if (month.values.length === 1 && !dayOfMonth.hasLastChar) {
 | |
|             if (!(parseInt(dayOfMonth.values[0], 10) <= fields_1.CronMonth.daysInMonth[month.values[0] - 1])) {
 | |
|                 throw new Error('Invalid explicit day of month definition');
 | |
|             }
 | |
|         }
 | |
|         this.#second = second;
 | |
|         this.#minute = minute;
 | |
|         this.#hour = hour;
 | |
|         this.#month = month;
 | |
|         this.#dayOfWeek = dayOfWeek;
 | |
|         this.#dayOfMonth = dayOfMonth;
 | |
|     }
 | |
|     /**
 | |
|      * Returns the second field.
 | |
|      * @returns {CronSecond}
 | |
|      */
 | |
|     get second() {
 | |
|         return this.#second;
 | |
|     }
 | |
|     /**
 | |
|      * Returns the minute field.
 | |
|      * @returns {CronMinute}
 | |
|      */
 | |
|     get minute() {
 | |
|         return this.#minute;
 | |
|     }
 | |
|     /**
 | |
|      * Returns the hour field.
 | |
|      * @returns {CronHour}
 | |
|      */
 | |
|     get hour() {
 | |
|         return this.#hour;
 | |
|     }
 | |
|     /**
 | |
|      * Returns the day of the month field.
 | |
|      * @returns {CronDayOfMonth}
 | |
|      */
 | |
|     get dayOfMonth() {
 | |
|         return this.#dayOfMonth;
 | |
|     }
 | |
|     /**
 | |
|      * Returns the month field.
 | |
|      * @returns {CronMonth}
 | |
|      */
 | |
|     get month() {
 | |
|         return this.#month;
 | |
|     }
 | |
|     /**
 | |
|      * Returns the day of the week field.
 | |
|      * @returns {CronDayOfWeek}
 | |
|      */
 | |
|     get dayOfWeek() {
 | |
|         return this.#dayOfWeek;
 | |
|     }
 | |
|     /**
 | |
|      * Returns a string representation of the cron fields.
 | |
|      * @param {(number | CronChars)[]} input - The cron fields values
 | |
|      * @static
 | |
|      * @returns {FieldRange[]} - The compacted cron fields
 | |
|      */
 | |
|     static compactField(input) {
 | |
|         if (input.length === 0) {
 | |
|             return [];
 | |
|         }
 | |
|         // Initialize the output array and current IFieldRange
 | |
|         const output = [];
 | |
|         let current = undefined;
 | |
|         input.forEach((item, i, arr) => {
 | |
|             // If the current FieldRange is undefined, create a new one with the current item as the start.
 | |
|             if (current === undefined) {
 | |
|                 current = { start: item, count: 1 };
 | |
|                 return;
 | |
|             }
 | |
|             // Cache the previous and next items in the array.
 | |
|             const prevItem = arr[i - 1] || current.start;
 | |
|             const nextItem = arr[i + 1];
 | |
|             // If the current item is 'L' or 'W', push the current FieldRange to the output and
 | |
|             // create a new FieldRange with the current item as the start.
 | |
|             // 'L' and 'W' characters are special cases that need to be handled separately.
 | |
|             if (item === 'L' || item === 'W') {
 | |
|                 output.push(current);
 | |
|                 output.push({ start: item, count: 1 });
 | |
|                 current = undefined;
 | |
|                 return;
 | |
|             }
 | |
|             // If the current step is undefined and there is a next item, update the current IFieldRange.
 | |
|             // This block checks if the current step needs to be updated and does so if needed.
 | |
|             if (current.step === undefined && nextItem !== undefined) {
 | |
|                 const step = item - prevItem;
 | |
|                 const nextStep = nextItem - item;
 | |
|                 // If the current step is less or equal to the next step, update the current FieldRange to include the current item.
 | |
|                 if (step <= nextStep) {
 | |
|                     current = { ...current, count: 2, end: item, step };
 | |
|                     return;
 | |
|                 }
 | |
|                 current.step = 1;
 | |
|             }
 | |
|             // If the difference between the current item and the current end is equal to the current step,
 | |
|             // update the current IFieldRange's count and end.
 | |
|             // This block checks if the current item is part of the current range and updates the range accordingly.
 | |
|             if (item - (current.end ?? 0) === current.step) {
 | |
|                 current.count++;
 | |
|                 current.end = item;
 | |
|             }
 | |
|             else {
 | |
|                 // If the count is 1, push a new FieldRange with the current start.
 | |
|                 // This handles the case where the current range has only one element.
 | |
|                 if (current.count === 1) {
 | |
|                     // If the count is 2, push two separate IFieldRanges, one for each element.
 | |
|                     output.push({ start: current.start, count: 1 });
 | |
|                 }
 | |
|                 else if (current.count === 2) {
 | |
|                     output.push({ start: current.start, count: 1 });
 | |
|                     // current.end can never be undefined here but typescript doesn't know that
 | |
|                     // this is why we ?? it and then ignore the prevItem in the coverage
 | |
|                     output.push({
 | |
|                         start: current.end ?? /* istanbul ignore next - see above */ prevItem,
 | |
|                         count: 1,
 | |
|                     });
 | |
|                 }
 | |
|                 else {
 | |
|                     // Otherwise, push the current FieldRange to the output.
 | |
|                     output.push(current);
 | |
|                 }
 | |
|                 // Reset the current FieldRange with the current item as the start.
 | |
|                 current = { start: item, count: 1 };
 | |
|             }
 | |
|         });
 | |
|         // Push the final IFieldRange, if any, to the output array.
 | |
|         if (current) {
 | |
|             output.push(current);
 | |
|         }
 | |
|         return output;
 | |
|     }
 | |
|     /**
 | |
|      * Handles a single range.
 | |
|      * @param {CronField} field - The cron field to stringify
 | |
|      * @param {FieldRange} range {start: number, end: number, step: number, count: number} The range to handle.
 | |
|      * @param {number} max The maximum value for the field.
 | |
|      * @returns {string | null} The stringified range or null if it cannot be stringified.
 | |
|      * @private
 | |
|      */
 | |
|     static #handleSingleRange(field, range, max) {
 | |
|         const step = range.step;
 | |
|         if (!step) {
 | |
|             return null;
 | |
|         }
 | |
|         if (step === 1 && range.start === field.min && range.end && range.end >= max) {
 | |
|             return field.hasQuestionMarkChar ? '?' : '*';
 | |
|         }
 | |
|         if (step !== 1 && range.start === field.min && range.end && range.end >= max - step + 1) {
 | |
|             return `*/${step}`;
 | |
|         }
 | |
|         return null;
 | |
|     }
 | |
|     /**
 | |
|      * Handles multiple ranges.
 | |
|      * @param {FieldRange} range {start: number, end: number, step: number, count: number} The range to handle.
 | |
|      * @param {number} max The maximum value for the field.
 | |
|      * @returns {string} The stringified range.
 | |
|      * @private
 | |
|      */
 | |
|     static #handleMultipleRanges(range, max) {
 | |
|         const step = range.step;
 | |
|         if (step === 1) {
 | |
|             return `${range.start}-${range.end}`;
 | |
|         }
 | |
|         const multiplier = range.start === 0 ? range.count - 1 : range.count;
 | |
|         /* istanbul ignore if */
 | |
|         if (!step) {
 | |
|             throw new Error('Unexpected range step');
 | |
|         }
 | |
|         /* istanbul ignore if */
 | |
|         if (!range.end) {
 | |
|             throw new Error('Unexpected range end');
 | |
|         }
 | |
|         if (step * multiplier > range.end) {
 | |
|             const mapFn = (_, index) => {
 | |
|                 /* istanbul ignore if */
 | |
|                 if (typeof range.start !== 'number') {
 | |
|                     throw new Error('Unexpected range start');
 | |
|                 }
 | |
|                 return index % step === 0 ? range.start + index : null;
 | |
|             };
 | |
|             /* istanbul ignore if */
 | |
|             if (typeof range.start !== 'number') {
 | |
|                 throw new Error('Unexpected range start');
 | |
|             }
 | |
|             const seed = { length: range.end - range.start + 1 };
 | |
|             return Array.from(seed, mapFn)
 | |
|                 .filter((value) => value !== null)
 | |
|                 .join(',');
 | |
|         }
 | |
|         return range.end === max - step + 1 ? `${range.start}/${step}` : `${range.start}-${range.end}/${step}`;
 | |
|     }
 | |
|     /**
 | |
|      * Returns a string representation of the cron fields.
 | |
|      * @param {CronField} field - The cron field to stringify
 | |
|      * @static
 | |
|      * @returns {string} - The stringified cron field
 | |
|      */
 | |
|     stringifyField(field) {
 | |
|         let max = field.max;
 | |
|         let values = field.values;
 | |
|         if (field instanceof fields_1.CronDayOfWeek) {
 | |
|             max = 6;
 | |
|             const dayOfWeek = this.#dayOfWeek.values;
 | |
|             values = dayOfWeek[dayOfWeek.length - 1] === 7 ? dayOfWeek.slice(0, -1) : dayOfWeek;
 | |
|         }
 | |
|         if (field instanceof fields_1.CronDayOfMonth) {
 | |
|             max = this.#month.values.length === 1 ? fields_1.CronMonth.daysInMonth[this.#month.values[0] - 1] : field.max;
 | |
|         }
 | |
|         const ranges = CronFieldCollection.compactField(values);
 | |
|         if (ranges.length === 1) {
 | |
|             const singleRangeResult = CronFieldCollection.#handleSingleRange(field, ranges[0], max);
 | |
|             if (singleRangeResult) {
 | |
|                 return singleRangeResult;
 | |
|             }
 | |
|         }
 | |
|         return ranges
 | |
|             .map((range) => {
 | |
|             const value = range.count === 1 ? range.start.toString() : CronFieldCollection.#handleMultipleRanges(range, max);
 | |
|             if (field instanceof fields_1.CronDayOfWeek && field.nthDay > 0) {
 | |
|                 return `${value}#${field.nthDay}`;
 | |
|             }
 | |
|             return value;
 | |
|         })
 | |
|             .join(',');
 | |
|     }
 | |
|     /**
 | |
|      * Returns a string representation of the cron field values.
 | |
|      * @param {boolean} includeSeconds - Whether to include seconds in the output
 | |
|      * @returns {string} The formatted cron string
 | |
|      */
 | |
|     stringify(includeSeconds = false) {
 | |
|         const arr = [];
 | |
|         if (includeSeconds) {
 | |
|             arr.push(this.stringifyField(this.#second)); // second
 | |
|         }
 | |
|         arr.push(this.stringifyField(this.#minute), // minute
 | |
|         this.stringifyField(this.#hour), // hour
 | |
|         this.stringifyField(this.#dayOfMonth), // dayOfMonth
 | |
|         this.stringifyField(this.#month), // month
 | |
|         this.stringifyField(this.#dayOfWeek));
 | |
|         return arr.join(' ');
 | |
|     }
 | |
|     /**
 | |
|      * Returns a serialized representation of the cron fields values.
 | |
|      * @returns {SerializedCronFields} An object containing the cron field values
 | |
|      */
 | |
|     serialize() {
 | |
|         return {
 | |
|             second: this.#second.serialize(),
 | |
|             minute: this.#minute.serialize(),
 | |
|             hour: this.#hour.serialize(),
 | |
|             dayOfMonth: this.#dayOfMonth.serialize(),
 | |
|             month: this.#month.serialize(),
 | |
|             dayOfWeek: this.#dayOfWeek.serialize(),
 | |
|         };
 | |
|     }
 | |
| }
 | |
| exports.CronFieldCollection = CronFieldCollection;
 |