cronosjs-extended
Version:
A cron based task scheduler for node and the browser, with extended syntax and timezone support.
384 lines (382 loc) • 15.5 kB
JavaScript
import { sortAsc, sortDesc, flatMap } from './utils';
const predefinedCronStrings = {
'@yearly': '0 0 0 1 1 * *',
'@annually': '0 0 0 1 1 * *',
'@monthly': '0 0 0 1 * * *',
'@weekly': '0 0 0 * * 0 *',
'@daily': '0 0 0 * * * *',
'@midnight': '0 0 0 * * * *',
'@hourly': '0 0 * * * * *',
};
const monthReplacements = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
const monthReplacementRegex = new RegExp(monthReplacements.join('|'), 'g');
const dayOfWeekReplacements = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
const dayOfWeekReplacementRegex = new RegExp(dayOfWeekReplacements.join('|'), 'g');
/*
"The actual range of times supported by ECMAScript Date objects is slightly smaller:
exactly –100,000,000 days to 100,000,000 days measured relative to midnight at the
beginning of 01 January, 1970 UTC. This gives a range of 8,640,000,000,000,000
milliseconds to either side of 01 January, 1970 UTC."
http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1
new Date(8640000000000000) => 00:00:00 13th Sep 275760
Largest full year valid as JS date = 275759
*/
const maxValidYear = 275759;
const minValidYear = 1;
export var WarningType;
(function (WarningType) {
WarningType["IncrementLargerThanRange"] = "IncrementLargerThanRange";
})(WarningType || (WarningType = {}));
export function _parse(cronstring) {
let expr = cronstring.trim().toLowerCase();
if (predefinedCronStrings[expr]) {
expr = predefinedCronStrings[expr];
}
const fields = expr.split(/\s+/g);
if (fields.length < 5 || fields.length > 7) {
throw new Error('Expression must have at least 5 fields, and no more than 7 fields');
}
switch (fields.length) {
case 5:
fields.unshift('0');
case 6:
fields.push('*');
}
return [
new SecondsOrMinutesField(fields[0]),
new SecondsOrMinutesField(fields[1]),
new HoursField(fields[2]),
new DaysField(fields[3], fields[5]),
new MonthsField(fields[4]),
new YearsField(fields[6])
];
}
function getIncrementLargerThanRangeWarnings(items, first, last) {
const warnings = [];
for (let item of items) {
let rangeLength;
if (item.step > 1 &&
item.step > (rangeLength = item.rangeLength(first, last))) {
warnings.push({
type: WarningType.IncrementLargerThanRange,
message: `Increment (${item.step}) is larger than range (${rangeLength}) for expression '${item.itemString}'`
});
}
}
return warnings;
}
class Field {
constructor(field) {
this.field = field;
}
parse() {
return this.field.split(',')
.map(item => FieldItem.parse(item, this.first, this.last, true));
}
get items() {
if (!this._items)
this._items = this.parse();
return this._items;
}
get values() {
return Field.getValues(this.items, this.first, this.last);
}
get warnings() {
return getIncrementLargerThanRangeWarnings(this.items, this.first, this.last);
}
static getValues(items, first, last) {
return Array.from(new Set(flatMap(items, item => item.values(first, last)))).sort(sortAsc);
}
}
class FieldItem {
constructor(itemString) {
this.itemString = itemString;
this.step = 1;
}
rangeLength(first, last) {
var _a, _b, _c, _d;
const start = (_b = (_a = this.range) === null || _a === void 0 ? void 0 : _a.from) !== null && _b !== void 0 ? _b : first, end = (_d = (_c = this.range) === null || _c === void 0 ? void 0 : _c.to) !== null && _d !== void 0 ? _d : last;
return (end < start) ? ((last - start) + (end - first) + 1) : (end - start);
}
values(first, last) {
const start = this.range ? this.range.from : first, rangeLength = this.rangeLength(first, last);
return Array(Math.floor(rangeLength / this.step) + 1)
.fill(0)
.map((_, i) => first + ((start - first + (this.step * i)) % (last - first + 1)));
}
get any() {
return this.range === undefined && this.step === 1;
}
get single() {
return !!this.range && this.range.from === this.range.to;
}
static parse(item, first, last, allowCyclicRange = false, transformer) {
var _a;
const fieldItem = new FieldItem(item);
const [match, all, startFrom, range, step] = ((_a = item.match(/^(?:(\*)|([0-9]+)|([0-9]+-[0-9]+))(?:\/([1-9][0-9]*))?$/)) !== null && _a !== void 0 ? _a : []);
if (!match)
throw new Error('Field item invalid format');
if (step) {
fieldItem.step = parseInt(step, 10);
}
if (startFrom) {
let start = parseInt(startFrom, 10);
start = transformer ? transformer(start) : start;
if (start < first || start > last)
throw new Error('Field item out of valid value range');
fieldItem.range = {
from: start,
to: step ? undefined : start
};
}
else if (range) {
const [rangeStart, rangeEnd] = range.split('-').map(x => {
const n = parseInt(x, 10);
return transformer ? transformer(n) : n;
});
if (rangeStart < first || rangeStart > last || rangeEnd < first || rangeEnd > last ||
(rangeEnd < rangeStart && !allowCyclicRange)) {
throw new Error('Field item range invalid, either value out of valid range or start greater than end in non wraparound field');
}
fieldItem.range = {
from: rangeStart,
to: rangeEnd
};
}
return fieldItem;
}
}
FieldItem.asterisk = new FieldItem('*');
export class SecondsOrMinutesField extends Field {
constructor() {
super(...arguments);
this.first = 0;
this.last = 59;
}
}
export class HoursField extends Field {
constructor() {
super(...arguments);
this.first = 0;
this.last = 23;
}
}
export class DaysField {
constructor(daysOfMonthField, daysOfWeekField) {
this.lastDay = false;
this.lastWeekday = false;
this.daysItems = [];
this.nearestWeekdayItems = [];
this.daysOfWeekItems = [];
this.lastDaysOfWeekItems = [];
this.nthDaysOfWeekItems = [];
for (let item of daysOfMonthField.split(',').map(s => s === '?' ? '*' : s)) {
if (item === 'l') {
this.lastDay = true;
}
else if (item === 'lw') {
this.lastWeekday = true;
}
else if (item.endsWith('w')) {
this.nearestWeekdayItems.push(FieldItem.parse(item.slice(0, -1), 1, 31));
}
else {
this.daysItems.push(FieldItem.parse(item, 1, 31));
}
}
const normalisedDaysOfWeekField = daysOfWeekField.replace(dayOfWeekReplacementRegex, match => dayOfWeekReplacements.indexOf(match) + '');
const parseDayOfWeek = (item) => FieldItem.parse(item, 0, 6, true, n => n === 7 ? 0 : n);
for (let item of normalisedDaysOfWeekField.split(',').map(s => s === '?' ? '*' : s)) {
const nthIndex = item.lastIndexOf('#');
if (item.endsWith('l')) {
this.lastDaysOfWeekItems.push(parseDayOfWeek(item.slice(0, -1)));
}
else if (nthIndex !== -1) {
const nth = item.slice(nthIndex + 1);
if (!/^[1-5]$/.test(nth))
throw new Error('Field item nth of month (#) invalid');
this.nthDaysOfWeekItems.push({
item: parseDayOfWeek(item.slice(0, nthIndex)),
nth: parseInt(nth, 10)
});
}
else {
this.daysOfWeekItems.push(parseDayOfWeek(item));
}
}
}
get values() {
return DaysFieldValues.fromField(this);
}
get warnings() {
const warnings = [], dayItems = [
...this.daysItems,
...this.nearestWeekdayItems,
], weekItems = [
...this.daysOfWeekItems,
...this.lastDaysOfWeekItems,
...this.nthDaysOfWeekItems.map(({ item }) => item),
];
warnings.push(...getIncrementLargerThanRangeWarnings(dayItems, 1, 31), ...getIncrementLargerThanRangeWarnings(weekItems, 0, 6));
return warnings;
}
get allDays() {
return (!this.lastDay &&
!this.lastWeekday &&
!this.nearestWeekdayItems.length &&
!this.lastDaysOfWeekItems.length &&
!this.nthDaysOfWeekItems.length &&
this.daysItems.length === 1 && this.daysItems[0].any &&
this.daysOfWeekItems.length === 1 && this.daysOfWeekItems[0].any);
}
}
export class DaysFieldValues {
constructor() {
this.lastDay = false;
this.lastWeekday = false;
this.days = [];
this.nearestWeekday = [];
this.daysOfWeek = [];
this.lastDaysOfWeek = [];
this.nthDaysOfWeek = [];
}
static fromField(field) {
const values = new DaysFieldValues();
const filterAnyItems = (items) => items.filter(item => !item.any);
values.lastDay = field.lastDay;
values.lastWeekday = field.lastWeekday;
values.days = Field.getValues(field.allDays ? [FieldItem.asterisk] : filterAnyItems(field.daysItems), 1, 31);
values.nearestWeekday = Field.getValues(field.nearestWeekdayItems, 1, 31);
values.daysOfWeek = Field.getValues(filterAnyItems(field.daysOfWeekItems), 0, 6);
values.lastDaysOfWeek = Field.getValues(field.lastDaysOfWeekItems, 0, 6);
const nthDaysHashes = new Set();
for (let item of field.nthDaysOfWeekItems) {
for (let n of item.item.values(0, 6)) {
let hash = n * 10 + item.nth;
if (!nthDaysHashes.has(hash)) {
nthDaysHashes.add(hash);
values.nthDaysOfWeek.push([n, item.nth]);
}
}
}
return values;
}
getDays(year, month) {
const days = new Set(this.days);
const lastDateOfMonth = new Date(year, month, 0).getDate();
const firstDayOfWeek = new Date(year, month - 1, 1).getDay();
const getNearestWeekday = (day) => {
if (day > lastDateOfMonth)
day = lastDateOfMonth;
const dayOfWeek = (day + firstDayOfWeek - 1) % 7;
let weekday = day + (dayOfWeek === 0 ? 1 : (dayOfWeek === 6 ? -1 : 0));
return weekday + (weekday < 1 ? 3 : (weekday > lastDateOfMonth ? -3 : 0));
};
if (this.lastDay) {
days.add(lastDateOfMonth);
}
if (this.lastWeekday) {
days.add(getNearestWeekday(lastDateOfMonth));
}
for (const day of this.nearestWeekday) {
days.add(getNearestWeekday(day));
}
if (this.daysOfWeek.length ||
this.lastDaysOfWeek.length ||
this.nthDaysOfWeek.length) {
const daysOfWeek = Array(7).fill(0).map(() => ([]));
for (let day = 1; day < 36; day++) {
daysOfWeek[(day + firstDayOfWeek - 1) % 7].push(day);
}
for (const dayOfWeek of this.daysOfWeek) {
for (const day of daysOfWeek[dayOfWeek]) {
days.add(day);
}
}
for (const dayOfWeek of this.lastDaysOfWeek) {
for (let i = daysOfWeek[dayOfWeek].length - 1; i >= 0; i--) {
if (daysOfWeek[dayOfWeek][i] <= lastDateOfMonth) {
days.add(daysOfWeek[dayOfWeek][i]);
break;
}
}
}
for (const [dayOfWeek, nthOfMonth] of this.nthDaysOfWeek) {
days.add(daysOfWeek[dayOfWeek][nthOfMonth - 1]);
}
}
return Array.from(days).filter(day => day <= lastDateOfMonth).sort(sortAsc);
}
}
export class MonthsField extends Field {
constructor(field) {
super(field.replace(monthReplacementRegex, match => {
return monthReplacements.indexOf(match) + 1 + '';
}));
this.first = 1;
this.last = 12;
}
}
export class YearsField extends Field {
constructor(field) {
super(field);
this.first = 1970;
this.last = 2099;
this.items;
}
parse() {
return this.field.split(',')
.map(item => FieldItem.parse(item, 0, maxValidYear));
}
get warnings() {
return getIncrementLargerThanRangeWarnings(this.items, this.first, maxValidYear);
}
nextYear(fromYear) {
var _a;
return (_a = this.items.reduce((years, item) => {
var _a, _b, _c, _d;
if (item.any)
years.push(fromYear);
else if (item.single) {
const year = item.range.from;
if (year >= fromYear)
years.push(year);
}
else {
const start = (_b = (_a = item.range) === null || _a === void 0 ? void 0 : _a.from) !== null && _b !== void 0 ? _b : this.first;
if (start > fromYear)
years.push(start);
else {
const nextYear = start + Math.ceil((fromYear - start) / item.step) * item.step;
if (nextYear <= ((_d = (_c = item.range) === null || _c === void 0 ? void 0 : _c.to) !== null && _d !== void 0 ? _d : maxValidYear))
years.push(nextYear);
}
}
return years;
}, []).sort(sortAsc)[0]) !== null && _a !== void 0 ? _a : null;
}
previousYear(fromYear) {
var _a;
return (_a = this.items.reduce((years, item) => {
var _a, _b, _c, _d;
if (item.any)
years.push(fromYear);
else if (item.single) {
const year = item.range.from;
if (year <= fromYear)
years.push(year);
}
else {
const end = (_b = (_a = item.range) === null || _a === void 0 ? void 0 : _a.to) !== null && _b !== void 0 ? _b : this.last;
if (end < fromYear)
years.push(end);
else {
const prevYear = end - Math.ceil((end - fromYear) / item.step) * item.step;
if (prevYear >= ((_d = (_c = item.range) === null || _c === void 0 ? void 0 : _c.from) !== null && _d !== void 0 ? _d : minValidYear))
years.push(prevYear);
}
}
return years;
}, []).sort(sortDesc)[0]) !== null && _a !== void 0 ? _a : null;
}
}