358 lines
14 KiB
JavaScript
358 lines
14 KiB
JavaScript
import { sortAsc, 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;
|
||
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;
|
||
}
|
||
}
|