336 lines
11 KiB
JavaScript
336 lines
11 KiB
JavaScript
import { CronDate } from "./date.js";
|
|
|
|
/**
|
|
* Name for each part of the cron pattern
|
|
* @typedef {("seconds" | "minutes" | "hours" | "days" | "months" | "daysOfWeek")} CronPatternPart
|
|
*/
|
|
|
|
/**
|
|
* Offset, 0 or -1.
|
|
*
|
|
* 0 for seconds,minutes and hours as they start on 1.
|
|
* -1 on days and months, as the start on 0
|
|
*
|
|
* @typedef {Number} CronIndexOffset
|
|
*/
|
|
|
|
/**
|
|
* Create a CronPattern instance from pattern string ('* * * * * *')
|
|
* @constructor
|
|
* @param {string} pattern - Input pattern
|
|
* @param {string} timezone - Input timezone, used for '?'-substitution
|
|
*/
|
|
function CronPattern (pattern, timezone) {
|
|
|
|
this.pattern = pattern;
|
|
this.timezone = timezone;
|
|
|
|
this.seconds = Array(60).fill(0); // 0-59
|
|
this.minutes = Array(60).fill(0); // 0-59
|
|
this.hours = Array(24).fill(0); // 0-23
|
|
this.days = Array(31).fill(0); // 0-30 in array, 1-31 in config
|
|
this.months = Array(12).fill(0); // 0-11 in array, 1-12 in config
|
|
this.daysOfWeek = Array(8).fill(0); // 0-7 Where 0 = Sunday and 7=Sunday;
|
|
|
|
this.lastDayOfMonth = false;
|
|
|
|
this.parse();
|
|
|
|
}
|
|
|
|
/**
|
|
* Parse current pattern, will throw on any type of failure
|
|
* @private
|
|
*/
|
|
CronPattern.prototype.parse = function () {
|
|
|
|
// Sanity check
|
|
if( !(typeof this.pattern === "string" || this.pattern.constructor === String) ) {
|
|
throw new TypeError("CronPattern: Pattern has to be of type string.");
|
|
}
|
|
|
|
// Split configuration on whitespace
|
|
const parts = this.pattern.trim().replace(/\s+/g, " ").split(" ");
|
|
|
|
// Validite number of configuration entries
|
|
if( parts.length < 5 || parts.length > 6 ) {
|
|
throw new TypeError("CronPattern: invalid configuration format ('" + this.pattern + "'), exacly five or six space separated parts required.");
|
|
}
|
|
|
|
// If seconds is omitted, insert 0 for seconds
|
|
if( parts.length === 5) {
|
|
parts.unshift("0");
|
|
}
|
|
|
|
// Convert 'L' to '*' and add lastDayOfMonth flag,
|
|
// and set days to 28,29,30,31 as those are the only days that can be the last day of month
|
|
if(parts[3].toUpperCase() == "L") {
|
|
parts[3] = "28,29,30,31";
|
|
this.lastDayOfMonth = true;
|
|
}
|
|
|
|
// Replace alpha representations
|
|
parts[4] = this.replaceAlphaMonths(parts[4]);
|
|
parts[5] = this.replaceAlphaDays(parts[5]);
|
|
|
|
// Implement '?' in the simplest possible way - replace ? with current value, before further processing
|
|
let 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());
|
|
parts[3] = parts[3].replace("?", initDate.getDate());
|
|
parts[4] = parts[4].replace("?", initDate.getMonth()+1); // getMonth is zero indexed while pattern starts from 1
|
|
parts[5] = parts[5].replace("?", initDate.getDay());
|
|
|
|
// Check part content
|
|
this.throwAtIllegalCharacters(parts);
|
|
|
|
// Parse parts into arrays, validates as we go
|
|
this.partToArray("seconds", parts[0], 0);
|
|
this.partToArray("minutes", parts[1], 0);
|
|
this.partToArray("hours", parts[2], 0);
|
|
this.partToArray("days", parts[3], -1);
|
|
this.partToArray("months", parts[4], -1);
|
|
this.partToArray("daysOfWeek", parts[5], 0);
|
|
|
|
// 0 = Sunday, 7 = Sunday
|
|
if( this.daysOfWeek[7] ) {
|
|
this.daysOfWeek[0] = 1;
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* Convert current part (seconds/minutes etc) to an array of 1 or 0 depending on if the part is about to trigger a run or not.
|
|
* @private
|
|
*
|
|
* @param {CronPatternPart} type - Seconds/minutes etc
|
|
* @param {string} conf - Current pattern part - *, 0-1 etc
|
|
* @param {CronIndexOffset} valueIndexOffset
|
|
* @param {boolean} [recursed] - Is this a recursed call
|
|
*/
|
|
CronPattern.prototype.partToArray = function (type, conf, valueIndexOffset, recursed) {
|
|
|
|
const arr = this[type];
|
|
|
|
// First off, handle wildcard
|
|
if( conf === "*" ) {
|
|
for( let i = 0; i < arr.length; i++ ) {
|
|
arr[i] = 1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Handle separated entries (,) by recursion
|
|
const split = conf.split(",");
|
|
if( split.length > 1 ) {
|
|
for( let i = 0; i < split.length; i++ ) {
|
|
this.partToArray(type, split[i], valueIndexOffset, true);
|
|
}
|
|
|
|
// Handle range with stepping (x-y/z)
|
|
} else if( conf.indexOf("-") !== -1 && conf.indexOf("/") !== -1 ) {
|
|
if (recursed) throw new Error("CronPattern: Range with stepping cannot coexist with ,");
|
|
|
|
this.handleRangeWithStepping(conf, type, valueIndexOffset);
|
|
|
|
// Handle range
|
|
} else if( conf.indexOf("-") !== -1 ) {
|
|
if (recursed) throw new Error("CronPattern: Range with stepping cannot coexist with ,");
|
|
|
|
this.handleRange(conf, type, valueIndexOffset);
|
|
|
|
// Handle stepping
|
|
} else if( conf.indexOf("/") !== -1 ) {
|
|
if (recursed) throw new Error("CronPattern: Range with stepping cannot coexist with ,");
|
|
|
|
this.handleStepping(conf, type, valueIndexOffset);
|
|
|
|
} else {
|
|
this.handleNumber(conf, type, valueIndexOffset);
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* After converting JAN-DEC, SUN-SAT only 0-9 * , / - are allowed, throw if anything else pops up
|
|
* @private
|
|
*
|
|
* @param {string[]} parts - Each part split as strings
|
|
*/
|
|
CronPattern.prototype.throwAtIllegalCharacters = function (parts) {
|
|
const reValidCron = /[^/*0-9,-]+/;
|
|
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.");
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Nothing but a number left, handle that
|
|
* @private
|
|
*
|
|
* @param {string} conf - Current part, expected to be a number, as a string
|
|
* @param {string} type - One of "seconds", "minutes" etc
|
|
* @param {number} valueIndexOffset - -1 for day of month, and month, as they start at 1. 0 for seconds, hours, minutes
|
|
*/
|
|
CronPattern.prototype.handleNumber = function (conf, type, valueIndexOffset) {
|
|
const i = (parseInt(conf, 10) + valueIndexOffset);
|
|
|
|
if( i < 0 || i >= this[type].length ) {
|
|
throw new TypeError("CronPattern: " + type + " value out of range: '" + conf + "'");
|
|
}
|
|
|
|
this[type][i] = 1;
|
|
};
|
|
|
|
/**
|
|
* Take care of ranges with stepping (e.g. 3-23/5)
|
|
* @private
|
|
*
|
|
* @param {string} conf - Current part, expected to be a string like 3-23/5
|
|
* @param {string} type - One of "seconds", "minutes" etc
|
|
* @param {number} valueIndexOffset - -1 for day of month, and month, as they start at 1. 0 for seconds, hours, minutes
|
|
*/
|
|
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;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Take care of ranges (e.g. 1-20)
|
|
* @private
|
|
*
|
|
* @param {string} conf - Current part, expected to be a string like 1-20
|
|
* @param {string} type - One of "seconds", "minutes" etc
|
|
* @param {number} valueIndexOffset - -1 for day of month, and month, as they start at 1. 0 for seconds, hours, minutes
|
|
*/
|
|
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,
|
|
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)");
|
|
}
|
|
|
|
// Check that value is within range
|
|
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;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle stepping (e.g. * / 14)
|
|
* @private
|
|
*
|
|
* @param {string} conf - Current part, expected to be a string like * /20 (without the space)
|
|
* @param {string} type - One of "seconds", "minutes" etc
|
|
*/
|
|
CronPattern.prototype.handleStepping = function (conf, type) {
|
|
|
|
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);
|
|
}
|
|
|
|
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, steps cannot be greater than maximum value of part ("+this[type].length+")");
|
|
|
|
for( let i = start; i < this[type].length; i+= steps ) {
|
|
this[type][i] = 1;
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Replace day name with day numbers
|
|
* @private
|
|
*
|
|
* @param {string} conf - Current part, expected to be a string that might contain sun,mon etc.
|
|
*
|
|
* @returns {string} - conf with 0 instead of sun etc.
|
|
*/
|
|
CronPattern.prototype.replaceAlphaDays = function (conf) {
|
|
return conf
|
|
.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 month name with month numbers
|
|
* @private
|
|
*
|
|
* @param {string} conf - Current part, expected to be a string that might contain jan,feb etc.
|
|
*
|
|
* @returns {string} - conf with 0 instead of sun etc.
|
|
*/
|
|
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");
|
|
};
|
|
|
|
export { CronPattern }; |