'use strict';
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
const sortAsc = (a, b) => a - b;
|
function flatMap(arr, mapper) {
|
return arr.reduce((acc, val, i) => {
|
acc.push(...mapper(val, i, arr));
|
return acc;
|
}, []);
|
}
|
|
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;
|
var WarningType;
|
|
(function (WarningType) {
|
WarningType["IncrementLargerThanRange"] = "IncrementLargerThanRange";
|
})(WarningType || (WarningType = {}));
|
|
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('*');
|
class SecondsOrMinutesField extends Field {
|
constructor() {
|
super(...arguments);
|
this.first = 0;
|
this.last = 59;
|
}
|
|
}
|
class HoursField extends Field {
|
constructor() {
|
super(...arguments);
|
this.first = 0;
|
this.last = 23;
|
}
|
|
}
|
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;
|
}
|
|
}
|
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);
|
}
|
|
}
|
class MonthsField extends Field {
|
constructor(field) {
|
super(field.replace(monthReplacementRegex, match => {
|
return monthReplacements.indexOf(match) + 1 + '';
|
}));
|
this.first = 1;
|
this.last = 12;
|
}
|
|
}
|
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;
|
}
|
|
}
|
|
class CronosDate {
|
constructor(year, month = 1, day = 1, hour = 0, minute = 0, second = 0) {
|
this.year = year;
|
this.month = month;
|
this.day = day;
|
this.hour = hour;
|
this.minute = minute;
|
this.second = second;
|
}
|
|
static fromDate(date, timezone) {
|
if (!timezone) {
|
return new CronosDate(date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds());
|
}
|
|
return timezone['nativeDateToCronosDate'](date);
|
}
|
|
toDate(timezone) {
|
if (!timezone) {
|
return new Date(this.year, this.month - 1, this.day, this.hour, this.minute, this.second);
|
}
|
|
return timezone['cronosDateToNativeDate'](this);
|
}
|
|
static fromUTCTimestamp(timestamp) {
|
const date = new Date(timestamp);
|
return new CronosDate(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds());
|
}
|
|
toUTCTimestamp() {
|
return Date.UTC(this.year, this.month - 1, this.day, this.hour, this.minute, this.second);
|
}
|
|
copyWith({
|
year = this.year,
|
month = this.month,
|
day = this.day,
|
hour = this.hour,
|
minute = this.minute,
|
second = this.second
|
} = {}) {
|
return new CronosDate(year, month, day, hour, minute, second);
|
}
|
|
} // Adapted from Intl.DateTimeFormat timezone handling in https://github.com/moment/luxon
|
|
const ZoneCache = new Map();
|
class CronosTimezone {
|
constructor(IANANameOrOffset) {
|
if (typeof IANANameOrOffset === 'number') {
|
if (IANANameOrOffset > 840 || IANANameOrOffset < -840) throw new Error('Invalid offset');
|
this.fixedOffset = IANANameOrOffset;
|
return this;
|
}
|
|
const offsetMatch = IANANameOrOffset.match(/^([+-]?)(0[1-9]|1[0-4])(?::?([0-5][0-9]))?$/);
|
|
if (offsetMatch) {
|
this.fixedOffset = (offsetMatch[1] === '-' ? -1 : 1) * (parseInt(offsetMatch[2], 10) * 60 + (parseInt(offsetMatch[3], 10) || 0));
|
return this;
|
}
|
|
if (ZoneCache.has(IANANameOrOffset)) {
|
return ZoneCache.get(IANANameOrOffset);
|
}
|
|
try {
|
this.dateTimeFormat = new Intl.DateTimeFormat("en-US", {
|
hour12: false,
|
timeZone: IANANameOrOffset,
|
year: "numeric",
|
month: "2-digit",
|
day: "2-digit",
|
hour: "2-digit",
|
minute: "2-digit",
|
second: "2-digit"
|
});
|
} catch (err) {
|
throw new Error('Invalid IANA name or offset');
|
}
|
|
this.zoneName = IANANameOrOffset;
|
const currentYear = new Date().getUTCFullYear();
|
this.winterOffset = this.offset(Date.UTC(currentYear, 0, 1));
|
this.summerOffset = this.offset(Date.UTC(currentYear, 5, 1));
|
ZoneCache.set(IANANameOrOffset, this);
|
}
|
|
toString() {
|
if (this.fixedOffset) {
|
const absOffset = Math.abs(this.fixedOffset);
|
return [this.fixedOffset < 0 ? '-' : '+', Math.floor(absOffset / 60).toString().padStart(2, '0'), (absOffset % 60).toString().padStart(2, '0')].join('');
|
}
|
|
return this.zoneName;
|
}
|
|
offset(ts) {
|
if (!this.dateTimeFormat) return this.fixedOffset || 0;
|
const date = new Date(ts);
|
const {
|
year,
|
month,
|
day,
|
hour,
|
minute,
|
second
|
} = this.nativeDateToCronosDate(date);
|
const asUTC = Date.UTC(year, month - 1, day, hour, minute, second),
|
asTS = ts - ts % 1000;
|
return (asUTC - asTS) / 60000;
|
}
|
|
nativeDateToCronosDate(date) {
|
if (!this.dateTimeFormat) {
|
return CronosDate['fromUTCTimestamp'](date.getTime() + (this.fixedOffset || 0) * 60000);
|
}
|
|
return this.dateTimeFormat['formatToParts'] ? partsOffset(this.dateTimeFormat, date) : hackyOffset(this.dateTimeFormat, date);
|
}
|
|
cronosDateToNativeDate(date) {
|
if (!this.dateTimeFormat) {
|
return new Date(date['toUTCTimestamp']() - (this.fixedOffset || 0) * 60000);
|
}
|
|
const provisionalOffset = (date.month > 3 || date.month < 11 ? this.summerOffset : this.winterOffset) || 0;
|
const UTCTimestamp = date['toUTCTimestamp'](); // Find the right offset a given local time.
|
// Our UTC time is just a guess because our offset is just a guess
|
|
let utcGuess = UTCTimestamp - provisionalOffset * 60000; // Test whether the zone matches the offset for this ts
|
|
const o2 = this.offset(utcGuess); // If so, offset didn't change and we're done
|
|
if (provisionalOffset === o2) return new Date(utcGuess); // If not, change the ts by the difference in the offset
|
|
utcGuess -= (o2 - provisionalOffset) * 60000; // If that gives us the local time we want, we're done
|
|
const o3 = this.offset(utcGuess);
|
if (o2 === o3) return new Date(utcGuess); // If it's different, we're in a hole time. The offset has changed, but the we don't adjust the time
|
|
return new Date(UTCTimestamp - Math.min(o2, o3) * 60000);
|
}
|
|
}
|
|
function hackyOffset(dtf, date) {
|
const formatted = dtf.format(date).replace(/\u200E/g, ""),
|
parsed = formatted.match(/(\d+)\/(\d+)\/(\d+),? (\d+):(\d+):(\d+)/),
|
[, month, day, year, hour, minute, second] = (parsed !== null && parsed !== void 0 ? parsed : []).map(n => parseInt(n, 10));
|
return new CronosDate(year, month, day, hour % 24, minute, second);
|
}
|
|
function partsOffset(dtf, date) {
|
const formatted = dtf.formatToParts(date);
|
return new CronosDate(parseInt(formatted[4].value, 10), parseInt(formatted[0].value, 10), parseInt(formatted[2].value, 10), parseInt(formatted[6].value, 10) % 24, parseInt(formatted[8].value, 10), parseInt(formatted[10].value, 10));
|
}
|
|
const hourinms = 60 * 60 * 1000;
|
|
const findFirstFrom = (from, list) => list.findIndex(n => n >= from);
|
|
class CronosExpression {
|
constructor(cronString, seconds, minutes, hours, days, months, years) {
|
this.cronString = cronString;
|
this.seconds = seconds;
|
this.minutes = minutes;
|
this.hours = hours;
|
this.days = days;
|
this.months = months;
|
this.years = years;
|
this.skipRepeatedHour = true;
|
this.missingHour = 'insert';
|
this._warnings = null;
|
}
|
|
static parse(cronstring, options = {}) {
|
var _a;
|
|
const parsedFields = _parse(cronstring);
|
|
if (options.strict) {
|
let warnings = flatMap(parsedFields, field => field.warnings);
|
|
if (typeof options.strict === 'object') {
|
warnings = warnings.filter(warning => !!options.strict[warning.type]);
|
}
|
|
if (warnings.length > 0) {
|
throw new Error(`Strict mode: Parsing failed with ${warnings.length} warnings`);
|
}
|
}
|
|
const expr = new CronosExpression(cronstring, parsedFields[0].values, parsedFields[1].values, parsedFields[2].values, parsedFields[3].values, parsedFields[4].values, parsedFields[5]);
|
expr.timezone = options.timezone instanceof CronosTimezone ? options.timezone : options.timezone !== undefined ? new CronosTimezone(options.timezone) : undefined;
|
expr.skipRepeatedHour = options.skipRepeatedHour !== undefined ? options.skipRepeatedHour : expr.skipRepeatedHour;
|
expr.missingHour = (_a = options.missingHour) !== null && _a !== void 0 ? _a : expr.missingHour;
|
return expr;
|
}
|
|
get warnings() {
|
if (!this._warnings) {
|
const parsedFields = _parse(this.cronString);
|
|
this._warnings = flatMap(parsedFields, field => field.warnings);
|
}
|
|
return this._warnings;
|
}
|
|
toString() {
|
var _a, _b;
|
|
const showTzOpts = !this.timezone || !!this.timezone.zoneName;
|
const timezone = Object.entries({
|
tz: (_b = (_a = this.timezone) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : 'Local',
|
skipRepeatedHour: showTzOpts && this.skipRepeatedHour.toString(),
|
missingHour: showTzOpts && this.missingHour
|
}).map(([key, val]) => val && key + ': ' + val).filter(s => s).join(', ');
|
return `${this.cronString} (${timezone})`;
|
}
|
|
nextDate(afterDate = new Date()) {
|
var _a;
|
|
const fromCronosDate = CronosDate.fromDate(afterDate, this.timezone);
|
|
if (((_a = this.timezone) === null || _a === void 0 ? void 0 : _a.fixedOffset) !== undefined) {
|
return this._next(fromCronosDate).date;
|
}
|
|
const fromTimestamp = afterDate.getTime(),
|
fromLocalTimestamp = fromCronosDate['toUTCTimestamp'](),
|
prevHourLocalTimestamp = CronosDate.fromDate(new Date(fromTimestamp - hourinms), this.timezone)['toUTCTimestamp'](),
|
nextHourLocalTimestamp = CronosDate.fromDate(new Date(fromTimestamp + hourinms), this.timezone)['toUTCTimestamp'](),
|
nextHourRepeated = nextHourLocalTimestamp - fromLocalTimestamp === 0,
|
thisHourRepeated = fromLocalTimestamp - prevHourLocalTimestamp === 0,
|
thisHourMissing = fromLocalTimestamp - prevHourLocalTimestamp === hourinms * 2;
|
|
if (this.skipRepeatedHour && thisHourRepeated) {
|
return this._next(fromCronosDate.copyWith({
|
minute: 59,
|
second: 60
|
}), false).date;
|
}
|
|
if (this.missingHour === 'offset' && thisHourMissing) {
|
const nextDate = this._next(fromCronosDate.copyWith({
|
hour: fromCronosDate.hour - 1
|
})).date;
|
|
if (!nextDate || nextDate.getTime() > fromTimestamp) return nextDate;
|
}
|
|
let {
|
date: nextDate,
|
cronosDate: nextCronosDate
|
} = this._next(fromCronosDate);
|
|
if (this.missingHour !== 'offset' && nextCronosDate && nextDate) {
|
const nextDateNextHourTimestamp = nextCronosDate.copyWith({
|
hour: nextCronosDate.hour + 1
|
}).toDate(this.timezone).getTime();
|
|
if (nextDateNextHourTimestamp === nextDate.getTime()) {
|
if (this.missingHour === 'insert') {
|
return nextCronosDate.copyWith({
|
minute: 0,
|
second: 0
|
}).toDate(this.timezone);
|
} // this.missingHour === 'skip'
|
|
|
return this._next(nextCronosDate.copyWith({
|
minute: 59,
|
second: 59
|
})).date;
|
}
|
}
|
|
if (!this.skipRepeatedHour) {
|
if (nextHourRepeated && (!nextDate || nextDate.getTime() > fromTimestamp + hourinms)) {
|
nextDate = this._next(fromCronosDate.copyWith({
|
minute: 0,
|
second: 0
|
}), false).date;
|
}
|
|
if (nextDate && nextDate < afterDate) {
|
nextDate = new Date(nextDate.getTime() + hourinms);
|
}
|
}
|
|
return nextDate;
|
}
|
|
_next(date, after = true) {
|
const nextDate = this._nextYear(after ? date.copyWith({
|
second: date.second + 1
|
}) : date);
|
|
return {
|
cronosDate: nextDate,
|
date: nextDate ? nextDate.toDate(this.timezone) : null
|
};
|
}
|
|
nextNDates(afterDate = new Date(), n = 5) {
|
const dates = [];
|
let lastDate = afterDate;
|
|
for (let i = 0; i < n; i++) {
|
const date = this.nextDate(lastDate);
|
if (!date) break;
|
lastDate = date;
|
dates.push(date);
|
}
|
|
return dates;
|
}
|
|
_nextYear(fromDate) {
|
let year = fromDate.year;
|
let nextDate = null;
|
|
while (!nextDate) {
|
year = this.years.nextYear(year);
|
if (year === null) return null;
|
nextDate = this._nextMonth(year === fromDate.year ? fromDate : new CronosDate(year));
|
year++;
|
}
|
|
return nextDate;
|
}
|
|
_nextMonth(fromDate) {
|
let nextMonthIndex = findFirstFrom(fromDate.month, this.months);
|
let nextDate = null;
|
|
while (!nextDate) {
|
const nextMonth = this.months[nextMonthIndex];
|
if (nextMonth === undefined) return null;
|
nextDate = this._nextDay(nextMonth === fromDate.month ? fromDate : new CronosDate(fromDate.year, nextMonth));
|
nextMonthIndex++;
|
}
|
|
return nextDate;
|
}
|
|
_nextDay(fromDate) {
|
const days = this.days.getDays(fromDate.year, fromDate.month);
|
let nextDayIndex = findFirstFrom(fromDate.day, days);
|
let nextDate = null;
|
|
while (!nextDate) {
|
const nextDay = days[nextDayIndex];
|
if (nextDay === undefined) return null;
|
nextDate = this._nextHour(nextDay === fromDate.day ? fromDate : new CronosDate(fromDate.year, fromDate.month, nextDay));
|
nextDayIndex++;
|
}
|
|
return nextDate;
|
}
|
|
_nextHour(fromDate) {
|
let nextHourIndex = findFirstFrom(fromDate.hour, this.hours);
|
let nextDate = null;
|
|
while (!nextDate) {
|
const nextHour = this.hours[nextHourIndex];
|
if (nextHour === undefined) return null;
|
nextDate = this._nextMinute(nextHour === fromDate.hour ? fromDate : new CronosDate(fromDate.year, fromDate.month, fromDate.day, nextHour));
|
nextHourIndex++;
|
}
|
|
return nextDate;
|
}
|
|
_nextMinute(fromDate) {
|
let nextMinuteIndex = findFirstFrom(fromDate.minute, this.minutes);
|
let nextDate = null;
|
|
while (!nextDate) {
|
const nextMinute = this.minutes[nextMinuteIndex];
|
if (nextMinute === undefined) return null;
|
nextDate = this._nextSecond(nextMinute === fromDate.minute ? fromDate : new CronosDate(fromDate.year, fromDate.month, fromDate.day, fromDate.hour, nextMinute));
|
nextMinuteIndex++;
|
}
|
|
return nextDate;
|
}
|
|
_nextSecond(fromDate) {
|
const nextSecondIndex = findFirstFrom(fromDate.second, this.seconds),
|
nextSecond = this.seconds[nextSecondIndex];
|
if (nextSecond === undefined) return null;
|
return fromDate.copyWith({
|
second: nextSecond
|
});
|
}
|
|
}
|
|
const maxTimeout = Math.pow(2, 31) - 1;
|
const scheduledTasks = [];
|
let runningTimer = null;
|
|
function addTask(task) {
|
if (task['_timestamp'] !== undefined) {
|
const insertIndex = scheduledTasks.findIndex(t => t['_timestamp'] < task['_timestamp']);
|
if (insertIndex >= 0) scheduledTasks.splice(insertIndex, 0, task);else scheduledTasks.push(task);
|
}
|
}
|
|
function removeTask(task) {
|
const removeIndex = scheduledTasks.indexOf(task);
|
if (removeIndex >= 0) scheduledTasks.splice(removeIndex, 1);
|
|
if (scheduledTasks.length === 0 && runningTimer) {
|
clearTimeout(runningTimer);
|
runningTimer = null;
|
}
|
}
|
|
function runScheduledTasks(skipRun = false) {
|
if (runningTimer) clearTimeout(runningTimer);
|
const now = Date.now();
|
const removeIndex = scheduledTasks.findIndex(task => task['_timestamp'] <= now);
|
const tasksToRun = removeIndex >= 0 ? scheduledTasks.splice(removeIndex) : [];
|
|
for (let task of tasksToRun) {
|
if (!skipRun) task['_runTask']();
|
|
if (task.isRunning) {
|
task['_updateTimestamp']();
|
addTask(task);
|
}
|
}
|
|
const nextTask = scheduledTasks[scheduledTasks.length - 1];
|
|
if (nextTask) {
|
runningTimer = setTimeout(runScheduledTasks, Math.min(nextTask['_timestamp'] - Date.now(), maxTimeout));
|
} else runningTimer = null;
|
}
|
|
function refreshSchedulerTimer() {
|
for (const task of scheduledTasks) {
|
task['_updateTimestamp']();
|
if (!task.isRunning) removeTask(task);
|
}
|
|
scheduledTasks.sort((a, b) => b['_timestamp'] - a['_timestamp']);
|
runScheduledTasks(true);
|
}
|
|
class DateArraySequence {
|
constructor(dateLikes) {
|
this._dates = dateLikes.map(dateLike => {
|
const date = new Date(dateLike);
|
if (isNaN(date.getTime())) throw new Error('Invalid date');
|
return date;
|
}).sort((a, b) => a.getTime() - b.getTime());
|
}
|
|
nextDate(afterDate) {
|
const nextIndex = this._dates.findIndex(d => d > afterDate);
|
|
return nextIndex === -1 ? null : this._dates[nextIndex];
|
}
|
|
}
|
|
class CronosTask {
|
constructor(sequenceOrDates) {
|
this._listeners = {
|
'started': new Set(),
|
'stopped': new Set(),
|
'run': new Set(),
|
'ended': new Set()
|
};
|
if (Array.isArray(sequenceOrDates)) this._sequence = new DateArraySequence(sequenceOrDates);else if (typeof sequenceOrDates === 'string' || typeof sequenceOrDates === 'number' || sequenceOrDates instanceof Date) this._sequence = new DateArraySequence([sequenceOrDates]);else this._sequence = sequenceOrDates;
|
}
|
|
start() {
|
if (!this.isRunning) {
|
this._updateTimestamp();
|
|
addTask(this);
|
runScheduledTasks();
|
if (this.isRunning) this._emit('started');
|
}
|
|
return this;
|
}
|
|
stop() {
|
if (this.isRunning) {
|
this._timestamp = undefined;
|
removeTask(this);
|
|
this._emit('stopped');
|
}
|
|
return this;
|
}
|
|
get nextRun() {
|
return this.isRunning ? new Date(this._timestamp) : undefined;
|
}
|
|
get isRunning() {
|
return this._timestamp !== undefined;
|
}
|
|
_runTask() {
|
this._emit('run', this._timestamp);
|
}
|
|
_updateTimestamp() {
|
const nextDate = this._sequence.nextDate(new Date());
|
|
this._timestamp = nextDate ? nextDate.getTime() : undefined;
|
if (!this.isRunning) this._emit('ended');
|
}
|
|
on(event, listener) {
|
this._listeners[event].add(listener);
|
|
return this;
|
}
|
|
off(event, listener) {
|
this._listeners[event].delete(listener);
|
|
return this;
|
}
|
|
_emit(event, ...args) {
|
this._listeners[event].forEach(listener => {
|
listener.call(this, ...args);
|
});
|
}
|
|
}
|
|
function scheduleTask(cronString, task, options) {
|
const expression = CronosExpression.parse(cronString, options);
|
return new CronosTask(expression).on('run', task).start();
|
}
|
function validate(cronString, options) {
|
try {
|
CronosExpression.parse(cronString, options);
|
} catch (_unused) {
|
return false;
|
}
|
|
return true;
|
}
|
|
exports.CronosExpression = CronosExpression;
|
exports.CronosTask = CronosTask;
|
exports.CronosTimezone = CronosTimezone;
|
exports.refreshSchedulerTimer = refreshSchedulerTimer;
|
exports.scheduleTask = scheduleTask;
|
exports.validate = validate;
|
//# sourceMappingURL=index.js.map
|