180 lines
8.4 KiB
JavaScript
180 lines
8.4 KiB
JavaScript
import { _parse } from './parser';
|
|
import { CronosDate, CronosTimezone } from './date';
|
|
import { flatMap } from './utils';
|
|
const hourinms = 60 * 60 * 1000;
|
|
const findFirstFrom = (from, list) => list.findIndex(n => n >= from);
|
|
export 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 });
|
|
}
|
|
}
|