cron-parser
Version:
Node.js library for parsing crontab instructions
366 lines (365 loc) • 14.5 kB
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 {FieldRange} range {start: number, end: number, step: number, count: number} The range to handle.
* @param {number} min The minimum value for the field.
* @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(range, min, max) {
const step = range.step;
if (!step) {
return null;
}
if (step === 1 && range.start === min && range.end && range.end >= max) {
return '*';
}
if (step !== 1 && range.start === 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(ranges[0], field.min, max);
if (singleRangeResult) {
return singleRangeResult;
}
}
return ranges
.map((range) => range.count === 1 ? range.start.toString() : CronFieldCollection.#handleMultipleRanges(range, max))
.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;