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