UNPKG

cron-parser

Version:

Node.js library for parsing crontab instructions

366 lines (365 loc) 14.5 kB
"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;