cron-schedule
Version:
A zero-dependency cron parser and scheduler for Node.js, Deno and the browser.
166 lines • 6.36 kB
JavaScript
import { Cron } from './cron.js';
const secondConstraint = {
min: 0,
max: 59,
};
const minuteConstraint = {
min: 0,
max: 59,
};
const hourConstraint = {
min: 0,
max: 23,
};
const dayConstraint = {
min: 1,
max: 31,
};
const monthConstraint = {
min: 1,
max: 12,
aliases: {
jan: '1',
feb: '2',
mar: '3',
apr: '4',
may: '5',
jun: '6',
jul: '7',
aug: '8',
sep: '9',
oct: '10',
nov: '11',
dec: '12',
},
};
const weekdayConstraint = {
min: 0,
max: 7,
aliases: {
mon: '1',
tue: '2',
wed: '3',
thu: '4',
fri: '5',
sat: '6',
sun: '7',
},
};
const timeNicknames = {
'@yearly': '0 0 1 1 *',
'@annually': '0 0 1 1 *',
'@monthly': '0 0 1 * *',
'@weekly': '0 0 * * 0',
'@daily': '0 0 * * *',
'@hourly': '0 * * * *',
'@minutely': '* * * * *',
};
function parseElement(element, constraint) {
const result = new Set();
// If returned set of numbers is empty, the scheduler class interpretes the emtpy set of numbers as all valid values of the constraint
if (element === '*') {
for (let i = constraint.min; i <= constraint.max; i = i + 1) {
result.add(i);
}
return result;
}
// If the element is a list, parse each element in the list.
const listElements = element.split(',');
if (listElements.length > 1) {
for (const listElement of listElements) {
const parsedListElement = parseElement(listElement, constraint);
for (const x of parsedListElement) {
result.add(x);
}
}
return result;
}
// Helper function to parse a single element, which includes checking for alias, valid number and constraint min and max.
const parseSingleElement = (singleElement) => {
var _a, _b;
singleElement =
(_b = (_a = constraint.aliases) === null || _a === void 0 ? void 0 : _a[singleElement.toLowerCase()]) !== null && _b !== void 0 ? _b : singleElement;
const parsedElement = Number.parseInt(singleElement, 10);
if (Number.isNaN(parsedElement)) {
throw new Error(`Failed to parse ${element}: ${singleElement} is NaN.`);
}
if (parsedElement < constraint.min || parsedElement > constraint.max) {
throw new Error(`Failed to parse ${element}: ${singleElement} is outside of constraint range of ${constraint.min} - ${constraint.max}.`);
}
return parsedElement;
};
// Detect if the element is a range.
// Possible range formats: 'start-end', 'start-end/step', '*', '*/step'.
// Where start and end can be numbers or aliases.
// Capture groups: 1: start-end, 2: start, 3: end, 4: /step, 5: step.
const rangeSegments = /^(([0-9a-zA-Z]+)-([0-9a-zA-Z]+)|\*)(\/([0-9]+))?$/.exec(element);
// If not, it must be a single element.
if (rangeSegments === null) {
result.add(parseSingleElement(element));
return result;
}
// If it is a range, get start and end of the range.
let parsedStart = rangeSegments[1] === '*'
? constraint.min
: parseSingleElement(rangeSegments[2]);
const parsedEnd = rangeSegments[1] === '*'
? constraint.max
: parseSingleElement(rangeSegments[3]);
// need to catch Sunday, which gets parsed here as 7, but is also legitimate at the start of a range as 0, to avoid the out of order error
if (constraint === weekdayConstraint &&
parsedStart === 7 &&
// this check ensures that sun-sun is not incorrectly parsed as [0,1,2,3,4,5,6]
parsedEnd !== 7) {
parsedStart = 0;
}
if (parsedStart > parsedEnd) {
throw new Error(`Failed to parse ${element}: Invalid range (start: ${parsedStart}, end: ${parsedEnd}).`);
}
// Check whether there is a custom step defined for the range, defaulting to 1.
const step = rangeSegments[5];
let parsedStep = 1;
if (step !== undefined) {
parsedStep = Number.parseInt(step, 10);
if (Number.isNaN(parsedStep)) {
throw new Error(`Failed to parse step: ${step} is NaN.`);
}
if (parsedStep < 1) {
throw new Error(`Failed to parse step: Expected ${step} to be greater than 0.`);
}
}
// Go from start to end of the range by the given steps.
for (let i = parsedStart; i <= parsedEnd; i = i + parsedStep) {
result.add(i);
}
return result;
}
/** Parses a cron expression into a Cron instance. */
export function parseCronExpression(cronExpression) {
var _a;
if (typeof cronExpression !== 'string') {
throw new TypeError('Invalid cron expression: must be of type string.');
}
// Convert time nicknames.
cronExpression = (_a = timeNicknames[cronExpression.toLowerCase()]) !== null && _a !== void 0 ? _a : cronExpression;
// Split the cron expression into its elements, removing empty elements (extra whitespaces).
const elements = cronExpression.split(' ').filter((elem) => elem.length > 0);
if (elements.length < 5 || elements.length > 6) {
throw new Error('Invalid cron expression: expected 5 or 6 elements.');
}
const rawSeconds = elements.length === 6 ? elements[0] : '0';
const rawMinutes = elements.length === 6 ? elements[1] : elements[0];
const rawHours = elements.length === 6 ? elements[2] : elements[1];
const rawDays = elements.length === 6 ? elements[3] : elements[2];
const rawMonths = elements.length === 6 ? elements[4] : elements[3];
const rawWeekdays = elements.length === 6 ? elements[5] : elements[4];
return new Cron({
seconds: parseElement(rawSeconds, secondConstraint),
minutes: parseElement(rawMinutes, minuteConstraint),
hours: parseElement(rawHours, hourConstraint),
days: parseElement(rawDays, dayConstraint),
// months in cron are indexed by 1, but Cron expects indexes by 0, so we need to reduce all set values by one.
months: new Set(Array.from(parseElement(rawMonths, monthConstraint)).map((x) => x - 1)),
weekdays: new Set(Array.from(parseElement(rawWeekdays, weekdayConstraint)).map((x) => x % 7)),
});
}
//# sourceMappingURL=cron-parser.js.map