Skip to content

Instantly share code, notes, and snippets.

@ShepTeck

ShepTeck/Code.gs Secret

Last active June 13, 2023 15:42
Show Gist options
  • Save ShepTeck/bb6bda4f4a1692b5cc4a49715a41c776 to your computer and use it in GitHub Desktop.
Save ShepTeck/bb6bda4f4a1692b5cc4a49715a41c776 to your computer and use it in GitHub Desktop.
quartz_croner_js
function myFunction() {
cronerFunctionsLG();
var job = Cron('0 30 2 28 1/1 ? *').nextRuns(25);
console.log(job);
/*
Quartz Cron Parts
Seconds Minutes Hours DayOfMonth Month DayOfWeek Year
? - Any
* - Every
x/y - iteration x=start(range) y=number of times repeated
0 0/1 * 1/1 * ? * Every minute -- PASSED
0 0 07-19 ? * MON-FRI * Between 07:00 AM and 07:59 PM, Monday through Friday -- PASSED
0 0 07-19/4 ? * SAT-SUN * Every 4 hours, between 07:00 AM and 07:59 PM, Saturday through Sunday -- PASSED
0 0 14,15 ? * * * At 02:00 PM and 03:00 PM -- PASSED
0 0 0/2 1/1 * ? * Every 2 hours -- PASSED
0 0 9 1/1 * ? * At 09:00 AM -- PASSED
0 0 0/12 1/1 * ? * Every 12 hours -- PASSED
0 0 06 ? * FRI * At 06:00 AM, only on Friday -- PASSED
0 0 0/4 1/1 * ? * Every 4 hours -- PASSED
0 50 7 ? * MON,WED,FRI * At 07:50 AM, only on Monday, Wednesday, and Friday -- PASSED
0 00 19 ? * MON,TUE,WED,THU,FRI,SAT * At 07:00 PM, only on Monday, Tuesday, Wednesday, Thursday, Friday, and Saturday -- PASSED
0 30 2 28 1/1 ? * At 02:30 AM, on day 28 of the month -- PASSED
0 0 22 29,30 DEC ? * At 10:00 PM, on day 29 and 30 of the month, only in December -- PASSED
*/
}
function quartzCronerFunctions() {
(function(global, factory) {
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, global.Cron = factory())
})(this, function() {
var maxRecordLevels = PropertiesService.getScriptProperties().getProperty('maxRecordLevels');
var timesThroughRecursion = 0;
/////////////////////////////////////
// Aaron Sheppard's Quartz Croner JS
// Modified version: 06-08-2023
//
// Updated version of the hexagon.github croner.js
// full credit for originals to the authors
// Croner - MIT License - Hexagon <github.com/Hexagon>
//
// Modified to accept Quartz Cron format with text weekdays
// as a stand-alone JS for use in Google Aps Script
// without any import or dependecies
////////////////////////////////////
function minitz(y, m, d, h, i, s, tz, throwOnInvalid) {
return minitz.fromTZ(minitz.tp(y, m, d, h, i, s, tz), throwOnInvalid);
}
minitz.fromTZISO = (localTimeStr, tz, throwOnInvalid) => {
return minitz.fromTZ(parseISOLocal(localTimeStr, tz), throwOnInvalid);
};
minitz.fromTZ = function(tp, throwOnInvalid) {
const
// Construct a fake Date object with UTC date/time set to local date/time in source timezone
inDate = new Date(Date.UTC(
tp.y,
tp.m - 1,
tp.d,
tp.h,
tp.i,
tp.s
)),
// Get offset between UTC and source timezone
offset = getTimezoneOffset(tp.tz, inDate),
// Remove offset from inDate to hopefully get a true date object
dateGuess = new Date(inDate.getTime() - offset),
// Get offset between UTC and guessed time in target timezone
dateOffsGuess = getTimezoneOffset(tp.tz, dateGuess);
// If offset between guessed true date object and UTC matches initial calculation, the guess
// was spot on
if ((dateOffsGuess - offset) === 0) {
return dateGuess;
} else {
// Not quite there yet, make a second try on guessing the local time, adjust by the offset indicated by the previous guess
// Try recreating input time again
// Then calculate and check the offset again
const
dateGuess2 = new Date(inDate.getTime() - dateOffsGuess),
dateOffsGuess2 = getTimezoneOffset(tp.tz, dateGuess2);
if ((dateOffsGuess2 - dateOffsGuess) === 0) {
// All good, return local time
return dateGuess2;
} else if(!throwOnInvalid && (dateOffsGuess2 - dateOffsGuess) > 0) {
// We're most probably dealing with a DST transition where we should use the offset of the second guess
return dateGuess2;
} else if (!throwOnInvalid) {
// We're most probably dealing with a DST transition where we should use the offset of the initial guess
return dateGuess;
} else {
// Input time is invalid, and the library is instructed to throw, so let's do it
throw new Error("Invalid date passed to fromTZ()");
}
}
};
minitz.toTZ = function (d, tzStr) {
// - replace narrow no break space with regular space to compensate for bug in Node.js 19.1
const localDateString = d.toLocaleString("en-US", {timeZone: tzStr}).replace(/[\u202f]/," ");
const td = new Date(localDateString);
return {
y: td.getFullYear(),
m: td.getMonth() + 1,
d: td.getDate(),
h: td.getHours(),
i: td.getMinutes(),
s: td.getSeconds(),
tz: tzStr
};
};
minitz.tp = (y,m,d,h,i,s,tz) => { return { y, m, d, h, i, s, tz: tz }; };
function getTimezoneOffset(timeZone, date = new Date()) {
// Get timezone
const tz = date.toLocaleString("en-US", {timeZone: timeZone, timeZoneName: "short"}).split(" ").slice(-1)[0];
// Extract time in en-US format
// - replace narrow no break space with regular space to compensate for bug in Node.js 19.1
const dateString = date.toLocaleString("en-US").replace(/[\u202f]/," ");
// Check ms offset between GMT and extracted timezone
return Date.parse(`${dateString} GMT`) - Date.parse(`${dateString} ${tz}`);
}
function parseISOLocal(dtStr, tz) {
// Parse date using built in Date.parse
const pd = new Date(Date.parse(dtStr));
// Check for completeness
if (isNaN(pd)) {
throw new Error("minitz: Invalid ISO8601 passed to parser.");
}
// If
// * date/time is specified in UTC (Z-flag included)
// * or UTC offset is specified (+ or - included after character 9 (20200101 or 2020-01-0))
// Return time in utc, else return local time and include timezone identifier
const stringEnd = dtStr.substring(9);
if (dtStr.includes("Z") || stringEnd.includes("-") || stringEnd.includes("+")) {
return minitz.tp(pd.getUTCFullYear(), pd.getUTCMonth()+1, pd.getUTCDate(),pd.getUTCHours(), pd.getUTCMinutes(),pd.getUTCSeconds(), "Etc/UTC");
} else {
return minitz.tp(pd.getFullYear(), pd.getMonth()+1, pd.getDate(),pd.getHours(), pd.getMinutes(),pd.getSeconds(), tz);
}
// Treat date as local time, in target timezone
}
minitz.minitz = minitz;
////////////////////////////////////
// This import is only used by tsc for generating type definitions from js/jsdoc
// deno-lint-ignore no-unused-vars
const DaysOfMonth = [31,28,31,30,31,30,31,31,30,31,30,31];
const RecursionSteps = [
["month", "year", 0],
["day", "month", -1],
["hour", "day", 0],
["minute", "hour", 0],
["second", "minute", 0],
];
function CronDate (d, tz) {
this.tz = tz;
// Populate object using input date, or throw
if (d && d instanceof Date) {
if (!isNaN(d)) {
this.fromDate(d);
} else {
throw new TypeError("CronDate: Invalid date passed to CronDate constructor");
}
} else if (d === void 0) {
this.fromDate(new Date());
} else if (d && typeof d === "string") {
this.fromString(d);
} else if (d instanceof CronDate) {
this.fromCronDate(d);
} else {
throw new TypeError("CronDate: Invalid type (" + typeof d + ") passed to CronDate constructor");
}
}
CronDate.prototype.fromDate = function (inDate) {
if (this.tz !== void 0) {
if (typeof this.tz === "number") {
this.ms = inDate.getUTCMilliseconds();
this.second = inDate.getUTCSeconds();
this.minute = inDate.getUTCMinutes()+this.tz;
this.hour = inDate.getUTCHours();
this.day = inDate.getUTCDate();
this.month = inDate.getUTCMonth();
this.year = inDate.getUTCFullYear();
// Minute could be out of bounds, apply
this.apply();
} else {
const d = minitz.toTZ(inDate, this.tz);
this.ms = inDate.getMilliseconds();
this.second = d.s;
this.minute = d.i;
this.hour = d.h;
this.day = d.d;
this.month = d.m - 1;
this.year = d.y;
}
} else {
this.ms = inDate.getMilliseconds();
this.second = inDate.getSeconds();
this.minute = inDate.getMinutes();
this.hour = inDate.getHours();
this.day = inDate.getDate();
this.month = inDate.getMonth();
this.year = inDate.getFullYear();
}
};
CronDate.prototype.fromCronDate = function (d) {
this.tz = d.tz;
this.year = d.year;
this.month = d.month;
this.day = d.day;
this.hour = d.hour;
this.minute = d.minute;
this.second = d.second;
this.ms = d.ms;
//console.log('year: '+this.year+' month: '+this.month+' day: '+this.day+' hour: '+this.hour+' minute: '+this.minute+' second: '+this.second);
};
CronDate.prototype.apply = function () {
// If any value could be out of bounds, apply
if (this.month>11||this.day>DaysOfMonth[this.month]||this.hour>59||this.minute>59||this.second>59||this.hour<0||this.minute<0||this.second<0) {
const d = new Date(Date.UTC(this.year, this.month, this.day, this.hour, this.minute, this.second, this.ms));
this.ms = d.getUTCMilliseconds();
this.second = d.getUTCSeconds();
this.minute = d.getUTCMinutes();
this.hour = d.getUTCHours();
this.day = d.getUTCDate();
this.month = d.getUTCMonth();
this.year = d.getUTCFullYear();
return true;
} else {
return false;
}
};
CronDate.prototype.fromString = function (str) {
return this.fromDate(minitz.fromTZISO(str, this.tz));
};
CronDate.prototype.findNext = function (options, target, pattern, offset) {
const originalTarget = this[target];
// In the conditions below, local time is not relevant. And as new Date(Date.UTC(y,m,d)) is way faster
// than new Date(y,m,d). We use the UTC functions to set/get date parts.
// Pre-calculate last day of month if needed
let lastDayOfMonth;
if (pattern.lastDayOfMonth || pattern.lastWeekdayOfMonth) {
// This is an optimization for every month except February, which has a different number of days in different years
if (this.month !== 1) {
lastDayOfMonth = DaysOfMonth[this.month];
} else {
lastDayOfMonth = new Date(Date.UTC(this.year, this.month + 1, 0, 0, 0, 0, 0)).getUTCDate();
}
}
// Pre-calculate weekday if needed
// Calculate offset weekday by ((fDomWeekDay + (targetDate - 1)) % 7)
const fDomWeekDay = (!pattern.starDOW && target === "day") ? new Date(Date.UTC(this.year, this.month, 1, 0, 0, 0, 0)).getUTCDay() : undefined;
for (let i = this[target] + offset; i < pattern[target].length; i++) {
// this applies to all "levels"
let match = pattern[target][i];
// Special case for last day of month
if (target === "day" && pattern.lastDayOfMonth && i - offset === lastDayOfMonth - 1) {
match = true;
}
// Special case for day of week
if (target === "day" && !pattern.starDOW) {
let dowMatch = pattern.dayOfWeek[(fDomWeekDay + ((i - offset) - 1)) % 7];
// Extra check for l-flag
if (dowMatch && pattern.lastWeekdayOfMonth) {
dowMatch = dowMatch && (i - offset + 1 > lastDayOfMonth - 7);
}
// If we use legacyMode, and dayOfMonth is specified - use "OR" to combine day of week with day of month
// In all other cases use "AND"
if (options.legacyMode && !pattern.starDOM) {
match = match || dowMatch;
} else {
match = match && dowMatch;
}
//console.log('date target='+target);
//console.log('date dowMatch='+dowMatch);
//console.log('date match='+match);
}
if (match) {
this[target] = i - offset;
//console.log('date if match='+(originalTarget !== this[target]) ? 2 : 1);
// Return 2 if changed, 1 if unchanged
return (originalTarget !== this[target]) ? 2 : 1;
}
}
// Return 3 if part was not matched
return 3;
};
CronDate.prototype.recurse = function (pattern, options, doing) {
//cycles through every level so if a schedule has every day and every month and every hour at the half;
// it will pull aprox. 288 (12 months * 28(to 31) days * 24 hours * 2 1/2 hours) for each year and will
// only stop at the max year limit no matter how many 'max records' in order to get the pattern
// this is causing a out of memory error.
// Find next month (or whichever part we're at)
const res = this.findNext(options, RecursionSteps[doing][0], pattern, RecursionSteps[doing][2]);
//timesThroughRecursion = timesThroughRecursion+1;
// console.log('timesThroughRecursion:'+timesThroughRecursion+'maxRecordLevels: '+maxRecordLevels)
if (timesThroughRecursion <= maxRecordLevels) {
//let timesThroughRecursion = doing +1 ;
// Month (or whichever part we're at) changed
if (res > 1) {
// Flag following levels for reset
let resetLevel = doing + 1;
while(resetLevel < RecursionSteps.length) {
this[RecursionSteps[resetLevel][0]] = -RecursionSteps[resetLevel][2];
resetLevel++;
}
// Parent changed
//console.log(' timesThroughRecursion: '+ this[RecursionSteps[timesThroughRecursion][1]] + ' maxRecordLevels: '//+maxRecordLevels);
//if (timesThroughRecursion <= maxRecordLevels) {
if (res=== 3 ) {
this[RecursionSteps[timesThroughRecursion][1]]++;
this[RecursionSteps[timesThroughRecursion][0]] = -RecursionSteps[timesThroughRecursion][2];
//console.log(' timesThroughRecursion: '+ timesThroughRecursion + ' maxRecordLevels: '+maxRecordLevels);
// Do increment parent, and reset current level
this[RecursionSteps[doing][1]]++;
this[RecursionSteps[doing][0]] = -RecursionSteps[doing][2];
this.apply();
// Restart
return this.recurse(pattern, options, 0);
} else if (this.apply()) {
return this.recurse(pattern, options, doing-1);
}
}
}
// Move to next level
doing += 1;
// Done?
if (doing >= RecursionSteps.length) {
return this;
// ... or out of bounds ?
} else if (this.year >= 2500) {
return null;
// ... oh, go to next part then
}
else {
return this.recurse(pattern, options, doing);
}
};
CronDate.prototype.increment = function (pattern, options, hasPreviousRun) {
// Move to next second, or increment according to minimum interval indicated by option `interval: x`
// Do not increment a full interval if this is the very first run
this.second += (options.interval > 1 && hasPreviousRun) ? options.interval : 1;
// Always reset milliseconds, so we are at the next second exactly
this.ms = 0;
// Make sure seconds has not gotten out of bounds
this.apply();
// Recursively change each part (y, m, d ...) until next match is found, return null on failure
return this.recurse(pattern, options, 0);
};
function getNextDayOfWeek(field, year, month, day) {
const daysOfWeek = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
const cronDays = field.toLowerCase().split(',');
const currentDay = new Date(year, month - 1, day);
for (let i = 0; i < 7; i++) {
const nextDay = new Date(year, month - 1, day + i);
const nextDayOfWeek = nextDay.getDay();
const nextDayOfWeekStr = daysOfWeek[nextDayOfWeek].substring(0, 3);
if (cronDays.includes(nextDayOfWeekStr) && nextDay > currentDay) {
return nextDay;
}
}
return null;
}
CronDate.prototype.getDate = function (internal) {
// If this is an internal call, return the date as is
// Also use this option when no timezone or utcOffset is set
if (internal || this.tz === void 0) {
return new Date(this.year, this.month, this.day, this.hour, this.minute, this.second, this.ms);
} else {
// If .tz is a number, it indicates offset in minutes. UTC timestamp of the internal date objects will be off by the same number of minutes.
// Restore this, and return a date object with correct time set.
if (typeof this.tz === "number") {
return new Date(Date.UTC(this.year, this.month, this.day, this.hour, this.minute-this.tz, this.second, this.ms));
// If .tz is something else (hopefully a string), it indicates the timezone of the "local time" of the internal date object
// Use minitz to create a normal Date object, and return that.
} else {
return minitz(this.year, this.month+1, this.day, this.hour, this.minute, this.second, this.tz);
}
}
};
CronDate.prototype.getTime = function () {
return this.getDate().getTime();
};
//////////////////////////////////
function CronPattern(pattern) {
if (!(typeof pattern === "string" || pattern.constructor === String)) {
throw new TypeError("CronPattern: Pattern has to be of type string.");
}
this.pattern = pattern.trim();
this.lastDayOfMonth = false;
this.lastWeekdayOfMonth = false;
this.starDOM = false;
this.starDOW = false;
this.second = new Array(60).fill(0);
this.minute = new Array(60).fill(0);
this.hour = new Array(24).fill(0);
this.day = new Array(31).fill(0);
this.month = new Array(12).fill(0);
this.dayOfWeek = new Array(7).fill(0);
this.year = [];
this.parse();
}
CronPattern.prototype.parse = function () {
if (this.pattern.indexOf("@") >= 0) {
this.pattern = this.handleNicknames(this.pattern);
}
const parts = this.pattern.replace(/\s+/g, " ").split(" ");
if (parts.length > 7) {
throw new TypeError(
"CronPattern: Invalid configuration format ('" +
this.pattern +
"'), exactly seven space-separated parts required."
);
}
// if the 'year' part is not included assume 'every'
if (parts.length <= 6) {
parts[6] = '*';
}
//replacing "?" in DayOfWeek for Quartz syntax to work with the minitz library
if(parts[5] === "?"){
parts[5] = parts[5].replace("?", "*")
};
//replacing "-" in DayOfWeek for Quartz syntax to work with minitz comma delimited list
if(parts[5].indexOf("-") >= 0 ){
parts[5] = parts[5].replace("-", ",")
};
if (parts[3] === "*" && parts[5] === "*") {
// Both dayOfMonth and dayOfWeek are set to *
this.starDOM = true;
this.starDOW = true;
} else if (parts[3] === "*") {
// Only dayOfMonth is set to *
this.starDOM = true;
} else if (parts[5] === "*") {
// Only dayOfWeek is set to *
this.starDOW = true;
}
if (parts[4].length >= 3) {
parts[4] = this.replaceAlphaMonths(parts[4]);
}
if (parts[5].length >= 3) {
parts[5] = this.replaceAlphaDays(parts[5]);
}
if (this.pattern.indexOf("?") >= 0) {
const initDate = new CronDate(new Date(), this.timezone).getDate(true);
parts[0] = parts[0].replace("?", initDate.getSeconds());
parts[1] = parts[1].replace("?", initDate.getMinutes());
parts[2] = parts[2].replace("?", initDate.getHours());
if (!this.starDOM) parts[3] = parts[3].replace("?", initDate.getDate());
parts[4] = parts[4].replace("?", initDate.getMonth() + 1);
if (!this.starDOW) parts[5] = parts[5].replace("?", initDate.getDay());
}
this.throwAtIllegalCharacters(parts);
this.partToArray("second", parts[0], 0);
this.partToArray("minute", parts[1], 0);
this.partToArray("hour", parts[2], 0);
this.partToArray("day", parts[3], -1);
this.partToArray("month", parts[4], -1);
this.partToArray("dayOfWeek", parts[5], 0);
this.partToArray("year", parts[6], 0);
if (this.dayOfWeek[7]) {
this.dayOfWeek[0] = 1;
}
};
CronPattern.prototype.partToArray = function (type, conf, valueIndexOffset) {
const arr = this[type];
if (conf === "*") {
arr.fill(1);
return;
}
if (conf === "?") {
arr.fill(1);
return;
}
const split = conf.split(",");
if (split.length > 1) {
for (let i = 0; i < split.length; i++) {
this.partToArray(type, split[i], valueIndexOffset);
}
} else if (conf.indexOf("-") !== -1 && conf.indexOf("/") !== -1) {
this.handleRangeWithStepping(conf, type, valueIndexOffset);
} else if (conf.indexOf("-") !== -1) {
this.handleRange(conf, type, valueIndexOffset);
} else if (conf.indexOf("/") !== -1) {
this.handleStepping(conf, type, valueIndexOffset);
} else if (conf !== "") {
this.handleNumber(conf, type, valueIndexOffset);
}
};
CronPattern.prototype.throwAtIllegalCharacters = function (parts) {
const reValidCron = /[^0-9*\/,\-\?^A-Z]+/;
for(let i = 0; i < parts.length; i++) {
if( reValidCron.test(parts[i]) ) {
throw new TypeError("CronPattern: configuration entry " + i + " (" + parts[i] + ") contains illegal characters.");
}
}
};
CronPattern.prototype.handleNumber = function (conf, type, valueIndexOffset) {
const i = parseInt(conf, 10) + valueIndexOffset;
if (isNaN(i)) {
throw new TypeError("CronPattern: " + type + " is not a number: '" + conf + "'");
}
let maxRange;
if (type === "dayOfWeek") {
maxRange = 7; // Day of week range: 0-6 (Sunday-Saturday)
} else if (type === "year") {
maxRange = 3000; // year range to 3000
} else {
maxRange = this[type].length;
}
if (i < 0 || i > maxRange) {
throw new TypeError("CronPattern: " + type + " value out of range: '" + conf + "'");
}
this[type][i] = 1;
};
CronPattern.prototype.handleRangeWithStepping = function (conf, type, valueIndexOffset) {
const matches = conf.match(/^(\d+)-(\d+)\/(\d+)$/);
if (matches === null) {
throw new TypeError("CronPattern: Syntax error, illegal range with stepping: '" + conf + "'");
}
let [, lower, upper, steps] = matches;
lower = parseInt(lower, 10) + valueIndexOffset;
upper = parseInt(upper, 10) + valueIndexOffset;
steps = parseInt(steps, 10);
if (isNaN(lower)) {
throw new TypeError("CronPattern: Syntax error, illegal lower range (NaN)");
}
if (isNaN(upper)) {
throw new TypeError("CronPattern: Syntax error, illegal upper range (NaN)");
}
if (isNaN(steps)) {
throw new TypeError("CronPattern: Syntax error, illegal stepping: (NaN)");
}
if (steps === 0) {
throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");
}
if (steps > this[type].length) {
throw new TypeError(
"CronPattern: Syntax error, steps cannot be greater than maximum value of part (" +
this[type].length +
")"
);
}
if (lower < 0 || upper >= this[type].length) {
throw new TypeError("CronPattern: Value out of range: '" + conf + "'");
}
if (lower > upper) {
throw new TypeError("CronPattern: From value is larger than to value: '" + conf + "'");
}
for (let i = lower; i <= upper; i += steps) {
this[type][i] = 1;
}
};
CronPattern.prototype.handleRange = function (conf, type, valueIndexOffset) {
const split = conf.split("-");
if (split.length !== 2) {
throw new TypeError("CronPattern: Syntax error, illegal range: '" + conf + "'");
}
const lower = parseInt(split[0], 10) + valueIndexOffset;
const upper = parseInt(split[1], 10) + valueIndexOffset;
if (isNaN(lower)) {
throw new TypeError("CronPattern: Syntax error, illegal lower range (NaN)");
} else if (isNaN(upper)) {
throw new TypeError("CronPattern: Syntax error, illegal upper range (NaN)");
}
if (lower < 0 || upper >= this[type].length) {
throw new TypeError("CronPattern: Value out of range: '" + conf + "'");
}
if (lower > upper) {
throw new TypeError("CronPattern: From value is larger than to value: '" + conf + "'");
}
for (let i = lower; i <= upper; i++) {
this[type][i] = 1;
}
};
CronPattern.prototype.handleStepping = function (conf, type, valueIndexOffset) {
const split = conf.split("/");
if (split.length !== 2) {
throw new TypeError("CronPattern: Syntax error, illegal stepping: '" + conf + "'");
}
let start = 0;
if (split[0] !== "*") {
start = parseInt(split[0], 10);
}
if (split[0] !== "?") {
start = parseInt(split[0], 10);
}
const steps = parseInt(split[1], 10);
if (isNaN(steps)) {
throw new TypeError("CronPattern: Syntax error, illegal stepping: (NaN)");
}
if (steps === 0) {
throw new TypeError("CronPattern: Syntax error, illegal stepping: 0");
}
if (steps > this[type].length) {
throw new TypeError(
"CronPattern: Syntax error, max steps for part is (" + this[type].length + ")"
);
}
for (let i = start; i < this[type].length; i += steps) {
this[type][i] = 1;
}
};
CronPattern.prototype.replaceAlphaDays = function (conf) {
return conf
.replace(/-sun/gi, "-7") // choose 7 if sunday is the upper value of a range because the upper value must not be smaller than the lower value
.replace(/sun/gi, "0")
.replace(/mon/gi, "1")
.replace(/tue/gi, "2")
.replace(/wed/gi, "3")
.replace(/thu/gi, "4")
.replace(/fri/gi, "5")
.replace(/sat/gi, "6")
.replace(/-SUN/gi, "-7") // choose 7 if sunday is the upper value of a range because the upper value must not be smaller than the lower value
.replace(/SUN/gi, "0")
.replace(/MON/gi, "1")
.replace(/TUE/gi, "2")
.replace(/WED/gi, "3")
.replace(/THU/gi, "4")
.replace(/FRI/gi, "5")
.replace(/SAT/gi, "6");
};
CronPattern.prototype.replaceAlphaMonths = function (conf) {
return conf
.replace(/jan/gi, "1")
.replace(/feb/gi, "2")
.replace(/mar/gi, "3")
.replace(/apr/gi, "4")
.replace(/may/gi, "5")
.replace(/jun/gi, "6")
.replace(/jul/gi, "7")
.replace(/aug/gi, "8")
.replace(/sep/gi, "9")
.replace(/oct/gi, "10")
.replace(/nov/gi, "11")
.replace(/dec/gi, "12");
};
CronPattern.prototype.handleNicknames = function (pattern) {
// Replace textual representations of pattern
const cleanPattern = pattern.trim().toLowerCase();
if (cleanPattern === "@yearly" || cleanPattern === "@annually") {
return "0 0 1 1 *";
} else if (cleanPattern === "@monthly") {
return "0 0 1 * *";
} else if (cleanPattern === "@weekly") {
return "0 0 * * 0";
} else if (cleanPattern === "@daily") {
return "0 0 * * *";
} else if (cleanPattern === "@hourly") {
return "0 * * * *";
} else {
return pattern;
}
};
/////////////////////////////////
// This import is only used by tsc for generating type definitions from js/jsdoc
// deno-lint-ignore no-unused-vars
function CronOptions(options) {
// If no options are passed, create empty object
if (options === void 0) {
options = {};
}
// Don't duplicate the 'name' property
delete options.name;
// Keep options, or set defaults
options.legacyMode = (options.legacyMode === void 0) ? true : options.legacyMode;
options.paused = (options.paused === void 0) ? false : options.paused;
options.maxRuns = (options.maxRuns === void 0) ? Infinity : options.maxRuns;
options.catch = (options.catch === void 0) ? false : options.catch;
options.interval = (options.interval === void 0) ? 0 : parseInt(options.interval, 10);
options.utcOffset = (options.utcOffset === void 0) ? void 0 : parseInt(options.utcOffset, 10);
options.unref = (options.unref === void 0) ? false : options.unref;
// startAt is set, validate it
if( options.startAt ) {
options.startAt = new CronDate(options.startAt, options.timezone);
}
if( options.stopAt ) {
options.stopAt = new CronDate(options.stopAt, options.timezone);
}
// Validate interval
if (options.interval !== null) {
if (isNaN(options.interval)) {
throw new Error("CronOptions: Supplied value for interval is not a number");
} else if (options.interval < 0) {
throw new Error("CronOptions: Supplied value for interval can not be negative");
}
}
// Validate utcOffset
if (options.utcOffset !== void 0) {
// Limit range for utcOffset
if (isNaN(options.utcOffset)) {
throw new Error("CronOptions: Invalid value passed for utcOffset, should be number representing minutes offset from UTC.");
} else if (options.utcOffset < -870 || options.utcOffset > 870 ) {
throw new Error("CronOptions: utcOffset out of bounds.");
}
// Do not allow both timezone and utcOffset
if (options.utcOffset !== void 0 && options.timezone) {
throw new Error("CronOptions: Combining 'utcOffset' with 'timezone' is not allowed.");
}
}
// Unref should be true, false or undefined
if (options.unref !== true && options.unref !== false) {
throw new Error("CronOptions: Unref should be either true, false or undefined(false).");
}
return options;
}
/////////////////////////////////
/////////////////////////////////
function isFunction(v) {
return (
Object.prototype.toString.call(v) === "[object Function]" ||
"function" === typeof v ||
v instanceof Function
);
}
function unrefTimer(timer) {
/* global Deno */
if (typeof Deno !== "undefined" && typeof Deno.unrefTimer !== "undefined") {
Deno.unrefTimer(timer);
// Node
} else if (timer && typeof timer.unref !== "undefined") {
timer.unref();
}
}
//export { isFunction, unrefTimer };
////////////////////////////////
const maxDelay = Math.pow(2, 32 - 1) - 1;
const scheduledJobs = [];
function Cron(pattern, fnOrOptions1, fnOrOptions2) {
// Optional "new" keyword
if (!(this instanceof Cron)) {
return new Cron(pattern, fnOrOptions1, fnOrOptions2);
}
// Make options and func optional and interchangable
let options, func;
if (isFunction(fnOrOptions1)) {
func = fnOrOptions1;
} else if (typeof fnOrOptions1 === "object") {
options = fnOrOptions1;
} else if (fnOrOptions1 !== void 0) {
throw new Error(
"Cron: Invalid argument passed for optionsIn. Should be one of function, or object (options).",
);
}
if (isFunction(fnOrOptions2)) {
func = fnOrOptions2;
} else if (typeof fnOrOptions2 === "object") {
options = fnOrOptions2;
} else if (fnOrOptions2 !== void 0) {
throw new Error(
"Cron: Invalid argument passed for funcIn. Should be one of function, or object (options).",
);
}
this.name = options ? options.name : void 0;
this.options = CronOptions(options);
this._states = {
kill: false,
blocking: false,
previousRun: void 0,
currentRun: void 0,
once: void 0,
currentTimeout: void 0,
maxRuns: options ? options.maxRuns : void 0,
paused: options ? options.paused : false,
pattern: void 0,
};
// Check if we got a date, or a pattern supplied as first argument
// Then set either this._states.once or this._states.pattern
if (
pattern &&
(pattern instanceof Date || ((typeof pattern === "string") && pattern.indexOf(":") > 0))
) {
this._states.once = new CronDate(pattern, this.options.timezone || this.options.utcOffset);
} else {
this._states.pattern = new CronPattern(pattern, this.options.timezone);
}
// Only store the job in scheduledJobs if a name is specified in the options.
if (this.name) {
const existing = scheduledJobs.find((j) => j.name === this.name);
if (existing) {
throw new Error(
"Cron: Tried to initialize new named job '" + this.name + "', but name already taken.",
);
} else {
scheduledJobs.push(this);
}
}
// Allow shorthand scheduling
if (func !== void 0) {
this.fn = func;
this.schedule();
}
return this;
}
Cron.prototype.nextRun = function (prev, startDate) {
const now = startDate || new Date();
const next = this._next(prev);
if (next && next.getTime() < now.getTime()) {
return this._next(next, now);
}
return next ? next.getDate() : null;;
};
Cron.prototype.nextRuns = function (n, previous) {
if (n > this._states.maxRuns) {
n = this._states.maxRuns;
}
const enumeration = [];
let prev = previous || this._states.currentRun;
while (n-- && (prev = this.nextRun(prev))) {
enumeration.push(prev);
}
return enumeration;
};
Cron.prototype.getPattern = function () {
return this._states.pattern ? this._states.pattern.pattern : void 0;
};
Cron.prototype.isRunning = function () {
const msLeft = this.msToNext(this._states.currentRun);
const isRunning = !this._states.paused;
const isScheduled = this.fn !== void 0;
// msLeft will be null if _states.kill is set to true, so we don't need to check this one, but we do anyway...
const notIsKilled = !this._states.kill;
return isRunning && isScheduled && notIsKilled && msLeft !== null;
};
Cron.prototype.isStopped = function () {
return this._states.kill;
};
Cron.prototype.isBusy = function () {
return this._states.blocking;
};
Cron.prototype.currentRun = function () {
return this._states.currentRun ? this._states.currentRun.getDate() : null;
};
Cron.prototype.previousRun = function () {
return this._states.previousRun ? this._states.previousRun.getDate() : null;
};
Cron.prototype.msToNext = function (prev) {
// Get next run time
const next = this._next(prev);
// Default previous for millisecond calculation
prev = new CronDate(prev, this.options.timezone || this.options.utcOffset);
if (next) {
return (next.getTime(true) - prev.getTime(true));
} else {
return null;
}
};
Cron.prototype.stop = function () {
// If there is a job in progress, it will finish gracefully ...
// Flag as killed
this._states.kill = true;
// Stop any waiting timer
if (this._states.currentTimeout) {
clearTimeout(this._states.currentTimeout);
}
// Remove job from the scheduledJobs array to free up the name, and allow the job to be
// garbage collected
const jobIndex = scheduledJobs.indexOf(this);
if (jobIndex >= 0) {
scheduledJobs.splice(jobIndex, 1);
}
};
Cron.prototype.pause = function () {
this._states.paused = true;
return !this._states.kill;
};
Cron.prototype.resume = function () {
this._states.paused = false;
return !this._states.kill;
};
Cron.prototype.schedule = function (func, partial) {
// If a function is already scheduled, bail out
if (func && this.fn) {
throw new Error(
"Cron: It is not allowed to schedule two functions using the same Croner instance.",
);
// Update function if passed
} else if (func) {
this.fn = func;
}
// Get ms to next run, bail out early if any of them is null (no next run)
let waitMs = this.msToNext(partial ? partial : this._states.currentRun);
const target = this.nextRun(partial ? partial : this._states.currentRun);
if (waitMs === null || target === null) return this;
// setTimeout cant handle more than Math.pow(2, 32 - 1) - 1 ms
if (waitMs > maxDelay) {
waitMs = maxDelay;
}
// Start the timer loop
// _checkTrigger will either call _trigger (if it's time, croner isn't paused and whatever),
// or recurse back to this function to wait for next trigger
this._states.currentTimeout = setTimeout(() => this._checkTrigger(target), waitMs);
// If unref option is set - unref the current timeout, which allows the process to exit even if there is a pending schedule
if (this._states.currentTimeout && this.options.unref) {
unrefTimer(this._states.currentTimeout);
}
return this;
};
Cron.prototype._trigger = async function (initiationDate) {
this._states.blocking = true;
this._states.currentRun = new CronDate(
void 0, // We should use initiationDate, but that does not play well with fake timers in third party tests. In real world there is not much difference though */
this.options.timezone || this.options.utcOffset,
);
if (this.options.catch) {
try {
await this.fn(this, this.options.context);
} catch (_e) {
if (isFunction(this.options.catch)) {
this.options.catch(_e, this);
}
}
} else {
// Trigger the function without catching
await this.fn(this, this.options.context);
}
this._states.previousRun = new CronDate(
initiationDate,
this.options.timezone || this.options.utcOffset,
);
this._states.blocking = false;
};
Cron.prototype.trigger = async function () {
await this._trigger();
};
Cron.prototype._checkTrigger = function (target) {
const now = new Date(),
shouldRun = !this._states.paused && now.getTime() >= target,
isBlocked = this._states.blocking && this.options.protect;
if (shouldRun && !isBlocked) {
this._states.maxRuns--;
// We do not await this
this._trigger();
} else {
// If this trigger were blocked, and protect is a function, trigger protect (without awaiting it, even if it's an synchronous function)
if (shouldRun && isBlocked && isFunction(this.options.protect)) {
setTimeout(() => this.options.protect(this), 0);
}
}
// Always reschedule
this.schedule(undefined, now);
};
Cron.prototype._next = function (prev) {
const hasPreviousRun = (prev || this._states.currentRun) ? true : false;
// Ensure previous run is a CronDate
//console.log('prototype._next: prev= '+prev);
//console.log('prototype._next: this._states.currentRun= '+this._states.currentRun);
prev = new CronDate(prev, this.options.timezone || this.options.utcOffset);
//console.log(prev);
// Previous run should never be before startAt
if (this.options.startAt && prev && prev.getTime() < this.options.startAt.getTime()) {
prev = this.options.startAt;
}
// Calculate next run according to pattern or one-off timestamp, pass actual previous run to increment
const nextRun = this._states.once ||
new CronDate(prev, this.options.timezone || this.options.utcOffset).increment(
this._states.pattern,
this.options,
hasPreviousRun, // hasPreviousRun is used to allow
);
//console.log(nextRun);
if (this._states.once && this._states.once.getTime() <= prev.getTime()) {
return null;
} else if (
(nextRun === null) ||
(this._states.maxRuns <= 0) ||
(this._states.kill) ||
(this.options.stopAt && nextRun.getTime() >= this.options.stopAt.getTime())
) {
return null;
} else {
// All seem good, return next run
return nextRun;
}
};
Cron.Cron = Cron;
Cron.scheduledJobs = scheduledJobs;
return Cron
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment