UNPKG

alchemymvc

Version:
1,502 lines (1,228 loc) 35.9 kB
const VAL_L = 'l'; const VAL_W = 'w'; const VAL_LW = 'lw'; const VAL_Q = '?'; const VAL_HASH = '#'; const VAL_STAR = '*'; const VAL_DASH = '-'; const VAL_SLASH = '/'; const FIELDS = ['second', 'minute', 'hour', 'day_of_month', 'month', 'day_of_week', 'year']; const FIELD_INFO = { second: {min: 0, max: 59}, minute: {min: 0, max: 59}, hour: {min: 0, max: 23}, day: {min: 1, max: 31}, day_of_month: {min: 1, max: 31}, month: { min: 1, max: 12, alias: { jan: 1, feb: 2, mar: 3, apr: 4, may: 5, jun: 6, jul: 7, aug: 8, sep: 9, oct: 10, nov: 11, dec: 12, }, }, day_of_week: { min: 0, max: 7, alias: { 7: 0, sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6, }, }, year: {min: 1970, max: 2099}, }; /** * The Cron class: * Represents a Cron frequency * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ const Cron = Function.inherits('Alchemy.Base', 'Alchemy.Cron', function Cron(input) { // The original input this.input = input; // The parsed expressions this.expressions = []; // Options this.options = {}; // The current expression this.current_expression = null; if (input) { this.parse(input); } }); /** * Predefined expressions * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ Cron.setStatic('PREDEFINED_EXPRESSIONS', { '@yearly' : '0 0 1 1 ?', '@monthly' : '0 0 1 * ?', '@weekly' : '0 0 ? * 0', '@daily' : '0 0 * * ?', '@hourly' : '0 * * * ?', }); /** * Undry this value * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Object} data * * @return {Cron} */ Cron.setStatic(function unDry(data) { let result = new Cron(data.input, data.options); return result; }); /** * Serialize this cron instance * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ Cron.setMethod(function toDry() { return { value: { input : this.input, options : this.options, } }; }); /** * Parse the input * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ Cron.setMethod(function parse(input) { if (!input) { throw new Error('The Cron expression cannot be empty'); } // Remember the input this.input = input; // Parse the expressions this.expressions = splitAndCleanup(input, '|').map(expression => this.parseSingleExpression(expression, this.options)); }); /** * Parse a single expression * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {string} expression * * @return {Cron.Expression} */ Cron.setMethod(function parseSingleExpression(expression) { let result = new CronExpression(expression); return result; }); /** * Get the next valid date this cron job will run at * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} current_date * * @return {Date} */ Cron.setMethod(function getNextDate(current_date) { if (!current_date) { current_date = new Date(); } let result, test; for (let expression of this.expressions) { test = expression.getNextDate(current_date, true); if (!result) { result = test; continue; } if (!test || test > result) { continue; } result = test; } return result; }); /** * Does the given date match the current cron expression? * Seconds will be ignored! * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date * * @return {Date} */ Cron.setMethod(function matches(date) { if (!date) { date = new Date(); } else { date = date.clone(); } // Go to the start of the minute (set seconds & ms to 0) date.startOf('minute'); let wanted_time = date.getTime(), test; for (let expression of this.expressions) { test = expression.getNextDate(date, false); if (!test) { continue; } if (test.getTime() == wanted_time) { return true; } } return false; }); /** * The CronExpression: * This represents a single Cron frequency expression * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ const CronExpression = Function.inherits('Alchemy.Base', 'Alchemy.Cron', function Expression(input) { this.expression = input; this.parse(); }); /** * Split the string, remove empty parts & deduplicate * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ function splitAndCleanup(input, separator) { let pieces = input.split(separator), result = new Set(), piece, i; for (i = 0; i < pieces.length; i++) { piece = pieces[i].trim(); if (!piece) continue; result.add(piece); } return [...result]; } /** * Parse a single expression * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ CronExpression.setMethod(function parse() { if (!this.expression) { throw new Error('Cron expression cannot be empty'); } let internal_expression = this.expression; let has_seconds = this.has_seconds; if (Cron.PREDEFINED_EXPRESSIONS[internal_expression]) { internal_expression = Cron.PREDEFINED_EXPRESSIONS[expression]; has_seconds = false; } const min_fields = has_seconds ? 5 : 4; const max_fields = has_seconds ? 7 : 6; const parts = internal_expression .split(/\s+/) .map((part) => part.trim()) .filter((part) => part); if (parts.length < min_fields || parts.length > max_fields) { let message = `Invalid cron expression [${this.expression}]. Expected [${min_fields} to ${max_fields}] fields but found [${parts.length}] fields.`; throw new Error(message); } // If seconds is not specified, then defaults to 0th sec if (!has_seconds) { parts.unshift('0'); } // If day of week is not specified, will default to ? if (parts.length === 5) { parts.push(VAL_Q); } // If year is not specified, then default to * if (parts.length === 6) { parts.push(VAL_STAR); } const field_parts = {}; for (let i = 0; i < FIELDS.length; i++) { field_parts[FIELDS[i]] = parts[i]; } const parsed = {}; for (const field of FIELDS) { if (field === 'second' && !has_seconds) { parsed[field] = {omit: true}; } else { parsed[field] = this.parseField(field, field_parts[field]); } } this.current_expression = null; this.parsed = parsed; }); /** * Parse a specific field of the current expression * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ CronExpression.setMethod(function parseField(field, value) { value = value.toLowerCase().trim(); if (value === VAL_STAR) { return {all: true}; } if (value === VAL_Q) { return this.parseQ(field, value); } const parts = splitAndCleanup(value, ','); const parsed = {}; for (const part of parts) { if (!part) { continue; } if (part.indexOf(VAL_SLASH) >= 0) { parsed.steps = parsed.steps || []; parsed.steps.push(this.parseStepRange(field, part)); } else if (part.indexOf(VAL_DASH) >= 0) { parsed.ranges = parsed.ranges || []; parsed.ranges.push(this.parseRange(field, part)); } else if (part.indexOf(VAL_HASH) >= 0) { parsed.nthDays = parsed.nthDays || []; parsed.nthDays.push(this.parseNth(field, part)); } else if (part === VAL_L) { parsed.lastDay = this.parseL(field, part); } else if (part === VAL_LW) { parsed.lastWeekday = this.parseLW(field, part); } else if (field === 'day_of_month' && part.indexOf(VAL_W) >= 0) { parsed.nearestWeekdays = parsed.nearestWeekdays || []; parsed.nearestWeekdays.push(this.parseNearestWeekday(field, part)); } else if (field === 'day_of_week' && part.endsWith(VAL_L)) { parsed.lastDays = parsed.lastDays || []; parsed.lastDays.push(this.parseLastDays(field, part)); } else { parsed.values = parsed.values || []; parsed.values.push(this.parseValue(field, part)); } } if (parsed.values) { parsed.values = dedupe(parsed.values); parsed.values.sort((a, b) => a - b); } return parsed; }); /** * Parse a question mark value for the current field * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ CronExpression.setMethod(function parseQ(field, value) { if (field === 'day_of_week' || field === 'day_of_month') { return {omit: true}; } throw this.createInvalidExpressionError( `Invalid Value [${value}] for field [${field}]. It can be specified only for [day_of_month or day_of_week] fields.` ); }); /** * Parse "L" syntax: the last day of the month * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ CronExpression.setMethod(function parseL(field, value) { if (field === 'day_of_week' || field === 'day_of_month') { return true; } throw this.createInvalidExpressionError( `Invalid value for [${value}] for field [${field}]. It can be used only for [day_of_month or day_of_week] fields.` ); }); /** * Parse "LW" syntax, meaning: "last workday of the month" * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ CronExpression.setMethod(function parseLW(field, value) { if (field === 'day_of_month') { return true; } throw this.createInvalidExpressionError( `Invalid value for [${value}] for field [${field}]. It can be used only for [day_of_month] fields.` ); }); /** * Parse the steps of a specific field value * (Like /5) * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ CronExpression.setMethod(function parseStepRange(field, value) { const parts = value.split(VAL_SLASH); if (parts.length != 2) { throw this.createInvalidExpressionError( `Invalid step range [${value}] for field [${field}]. Expected exactly 2 values separated by a / but got [${parts.length}] values.` ); } const info = FIELD_INFO[field]; const fromParts = parts[0].indexOf(VAL_DASH) >= 0 ? parts[0].split(VAL_DASH) : [parts[0]]; const from = fromParts[0] === VAL_STAR ? info.min : this.parseNumber(field, unalias(field, fromParts[0])); const to = fromParts.length > 1 ? this.parseNumber(field, unalias(field, fromParts[1])) : info.max; const step = this.parseNumber(field, unalias(field, parts[1])); if (from < info.min) { throw this.createInvalidExpressionError( `Invalid step range [${value}] for field [${field}]. From value [${from}] out of range. It must be greater than or equals to [${info.min}]` ); } if (to > info.max) { throw this.createInvalidExpressionError( `Invalid step range [${value}] for field [${field}]. To value [${to}] out of range. It must be less than or equals to [${info.max}]` ); } if (step > info.max) { throw this.createInvalidExpressionError( `Invalid step range [${value}] for field [${field}]. Step value [${value}] out of range. It must be less than or equals to [${info.max}]` ); } return {from, to, step}; }); /** * Parse a number for the current field. * If it turns out to not be a valid number, an error will be thrown. * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ CronExpression.setMethod(function parseNumber(field, value) { const num = parseInt(unalias(field, value), 10); if (Number.isNaN(num)) { throw this.createInvalidExpressionError(`Invalid numeric value [${value}] in field [${field}].`); } return num; }); /** * Parse a range (like 1-5) * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ CronExpression.setMethod(function parseRange(field, value) { const parts = value.split(VAL_DASH); if (parts.length != 2) { throw this.createInvalidExpressionError( `Invalid range [${value}] for field [${field}]. Range should have two values separated by a - but got [${parts.length}] values.` ); } const from = this.parseNumber(field, unalias(field, parts[0])); let to = this.parseNumber(field, unalias(field, parts[1])); // For day of week, sun will act as 0 or 7 depending on if it is in from or to if (field == 'day_of_week') { if (to === 0) { to = 7; } } if (from > to) { throw this.createInvalidExpressionError(`Invalid range [${value}] for field [${field}]. From value must be less than to value.`); } const info = FIELD_INFO[field]; if (from < info.min || to > info.max) { throw this.createInvalidExpressionError( `Invalid range [${value}] for field [${field}]. From or to value is out of allowed min/max values. Allowed values are between [${info.min}-${info.max}].` ); } return {from, to}; }); /** * Parse a value * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ CronExpression.setMethod(function parseValue(field, value) { const num = this.parseNumber(field, value); const info = FIELD_INFO[field]; if (num < info.min) { throw this.createInvalidExpressionError( `Value [${value}] out of range for field [${field}]. It must be greater than or equals to [${info.min}].` ); } if (info.max && num > info.max) { throw this.createInvalidExpressionError( `Value [${value}] out of range for field [${field}]. It must be less than or equals to [${info.max}].` ); } return num; }); /** * Parse Nth day of week (like 5#3, the 3rd friday of the month) * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ CronExpression.setMethod(function parseNth(field, value) { if (field !== 'day_of_week') { throw this.createInvalidExpressionError( `Invalid value [${value}] for field [${field}]. Nth day can be used only in [day_of_week] field.` ); } const parts = value.split(VAL_HASH); if (parts.length !== 2) { throw this.createInvalidExpressionError( `Invalid nth day value [${value}] for field [${field}]. It must be in [day_of_week#instance] format.` ); } const day_of_week = this.parseNumber(field, parts[0]); const instance = this.parseNumber(undefined, parts[1]); if (instance < 1 || instance > 5) { throw this.createInvalidExpressionError( `Invalid Day of Week instance value [${instance}] for field [${field}]. It must be between 1 and 5.` ); } return { day_of_week, instance: instance, }; }); /** * Parse * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ CronExpression.setMethod(function parseNearestWeekday(field, value) { if (field !== 'day_of_month') { throw this.createInvalidExpressionError( `Invalid value [${value}] for field [${field}]. Nearest weekday can be used only in [day_of_month] field.` ); } return this.parseNumber(field, value.split(VAL_W)[0]); }); /** * Parse "last days" expressions, like "sunl" * which means "Last sunday of the month" * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ CronExpression.setMethod(function parseLastDays(field, value) { return this.parseNumber(field, value.split(VAL_L)[0]); }); /** * Get the next valid date this cron job will run at * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} current_date * @param {boolean} add_one_second When false, the current date might be returned * * @return {Date} */ CronExpression.setMethod(function getNextDate(current_date, add_one_second = true) { if (!current_date) { current_date = new Date(); } let result = current_date.clone(); if (add_one_second) { result.add(1, 'second'); } let last_ts = result.getTime(), current_ts = last_ts; do { let modified = this._modifyDate(result); if (modified) { current_ts = result.getTime(); if (current_ts <= last_ts) { return false; } last_ts = current_ts; } else if (modified == null) { break; } else if (modified === false) { // Failed to get a next date! return false; } // Simple infinite loop fix if (result.getFullYear() > 2099) { return false; } } while (true); return result; }); /** * Do only 1 modification. As soon as that happens, return. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date * @param {Object} config * @param {string} unit */ CronExpression.setMethod(function _modifyDate(date) { const parsed = this.parsed; let result = this.modifyDate(date, parsed.second, 'second'); if (result != null) { return result; } result = this.modifyDate(date, parsed.minute, 'minute'); if (result != null) { return result; } result = this.modifyDate(date, parsed.hour, 'hour'); if (result != null) { return result; } result = this.modifyDate(date, parsed.day_of_month, 'day_of_month'); if (result != null) { return result; } result = this.modifyDate(date, parsed.day_of_week, 'day_of_week'); if (result != null) { return result; } result = this.modifyDate(date, parsed.month, 'month'); if (result != null) { return result; } result = this.modifyDate(date, parsed.year, 'year'); if (result != null) { return result; } return null; }); /** * Potentially modify the given date (in place) * using the field info & the given unit of time * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date * @param {Object} config * @param {string} unit */ CronExpression.setMethod(function modifyDate(date, config, unit) { if (unit == 'day_of_month') { unit = 'day'; } if (config.all) { return; } if (config.omit) { if (unit == 'second' && date.getSeconds() > 0) { date.add(1, 'minute'); date.startOf('minute'); return true; } else { return null; } } if (config.values) { return this.modifyDateUsingValues(date, config, unit); } if (config.ranges) { return this.modifyDateUsingRanges(date, config, unit); } if (config.steps) { return this.modifyDateUsingSteps(date, config, unit); } if (config.nthDays) { return this.modifyDateUsingNthDays(date, config, unit); } if (config.lastDays) { return this.modifyDateUsingLastDays(date, config, unit); } if (config.lastWeekday) { return this.modifyDateUsingLastWeekday(date, config, unit); } if (config.lastDay) { return this.modifyDateUsingLastDay(date, config, unit); } return null; }); /** * Potentially modify the given date (in place) * using the given values of the parsed field expression. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date The date to modify in-place * @param {Object} config The parsed Cron field expression * @param {string} unit The unit of time (day, month, second, hour, ...) * * @return {boolean|null} True if the date was modified, false if it failed, null if it wasn't modified */ CronExpression.setMethod(function modifyDateUsingValues(date, config, unit) { // Get all the allowed values for this unit const allowed_values = config.values; // Get the current amount of the given unit of the given date let current_amount = this.getUnit(date, unit); // If the current value is allowed, do nothing! if (allowed_values.indexOf(current_amount) > -1) { return; } // Get the max allowed amount of the current unit of the current date // (Once this amount has been reached, the value should loop around) let max_amount = this.getUnitMax(unit, date); let min_amount = this.getUnitMin(unit, date); // We're going to look for the smallest change to reach the next allowed value. // Initial value is positive infinity, so any first change will be smaller. let smallest_change = Infinity; for (let value of allowed_values) { let change = 0; if (current_amount < value) { // The next allowed value is in the future (without looping around) // so this will be a simple change change = value - current_amount; } else if (current_amount > value) { // The next allowed value is smaller than the current one, // so this probably means we'll have to loop around // (Only years can't loop around) change = (max_amount - current_amount) + value; // If the minimum amount is 0, we'll have to add 1 to the change if (min_amount == 0) { change += 1; } } else { // Shouldn't happen, but ignore it anyway continue; } // If the current calculated change is smaller than the previous one, // it'll become the new smallest change if (change < smallest_change) { smallest_change = change; } } // If the smalles change is not finite, no allowed values were found if (!isFinite(smallest_change)) { return false; } // Actually add the smallest change amount to the date this.addUnitToDate(date, unit, smallest_change); return true; }); /** * Potentially modify the given date (in place) * using the given ranges of the field config * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date * @param {Object} config * @param {string} unit */ CronExpression.setMethod(function modifyDateUsingRanges(date, config, unit) { // Get the current amount of the given unit of the given date let current_amount = this.getUnit(date, unit); // Get the max amount of the current date // (Once this amount has been reached, the value will loop around) let max_amount = this.getUnitMax(unit, date); let min_amount = this.getUnitMin(unit, date); // First see if the current unit is allowed by any of the ranges for (let range of config.ranges) { if (current_amount >= range.from && current_amount <= range.to) { // It's allowed! return null; } } // If not, we need to calculate the change to reach the next allowed value. // We'll do this for each range. The smallest change will be added to the date. let smallest_change = Infinity; for (let range of config.ranges) { let change = 0; if (current_amount < range.from) { change = range.from - current_amount; } else if (current_amount > range.to) { change = (max_amount - current_amount) + range.from; // If the minimum amount is 0, we'll have to add 1 to the change if (min_amount == 0) { change += 1; } } else { return false; } if (change < smallest_change) { smallest_change = change; } } this.addUnitToDate(date, unit, smallest_change); return true; }); /** * Potentially modify the given date (in place) * using the given steps of the field config. * Steps can be like "10-30/5" * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date * @param {Object} config * @param {string} unit */ CronExpression.setMethod(function modifyDateUsingSteps(date, config, unit) { // Get the current amount of the given unit of the given date let current_amount = this.getUnit(date, unit); // Get the max amount of the current date // (Once this amount has been reached, the value will loop around) let max_amount = this.getUnitMax(unit, date); let min_amount = this.getUnitMin(unit, date); // First see if the current unit is allowed by any of the steps for (let step of config.steps) { let to = step.to; if (to == 7 && unit == 'day_of_week') { to = 6; } if (current_amount >= step.from && current_amount <= to) { if ((current_amount - step.from) % step.step == 0) { // It's allowed! return null; } } } // If not, we need to calculate the change to reach the next allowed value. // We'll do this for each step. The smallest change will be added to the date. let smallest_change = Infinity; for (let step of config.steps) { let change = 0; let to = step.to; if (to == 7 && unit == 'day_of_week') { to = 6; } if (current_amount < step.from) { change = step.from - current_amount; } else if (current_amount >= to) { change = (max_amount - current_amount) + step.from; // If the minimum amount is 0, we'll have to add 1 to the change if (min_amount == 0) { change += 1; } } else { change = step.step - ((current_amount - step.from) % step.step); } if (change < smallest_change) { smallest_change = change; } } this.addUnitToDate(date, unit, smallest_change); return true; }); /** * Modify dates using Nth days * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date The date to modify in-place * @param {Object} config The parsed Cron field expression * @param {string} unit The unit of time. Should always be day_of_week * * @return {boolean|null} True if the date was modified, false if it failed, null if it wasn't modified */ CronExpression.setMethod(function modifyDateUsingNthDays(date, config, unit) { // Get the current amount of the given unit of the given date let current_dow = this.getUnit(date, 'day_of_week'); // Calculate the current instance of the current day let current_instance = Math.ceil(date.getDate() / 7); // First see if the current unit is allowed by any of the steps for (let entry of config.nthDays) { if (current_dow != entry.day_of_week) { continue; } // If the instance also matches, it's already good! if (current_instance == entry.instance) { return null; } } // Get the max amount of the current date // (Once this amount has been reached, the value will loop around) let max_amount = 6; // If not, we need to calculate the change to reach the next allowed value. // We'll do this for each step. The smallest change will be added to the date. let smallest_change = Infinity; for (let entry of config.nthDays) { let change = 0; // First: let's calculate the needed change to get the correct day_of_week // (It might even already be correct and stay at 0!) if (current_dow <= entry.day_of_week) { change = entry.day_of_week - current_dow; } else if (current_dow > entry.day_of_week) { change = (max_amount - current_dow) + entry.day_of_week + 1; } // Now let's calculate the change to get the correct instance let test = date.clone().add(change, 'days'), instance = Math.ceil(test.getDate() / 7); if (instance != entry.instance) { if (instance > entry.instance) { // The newly calculated date is getting further away from the wanted instance, // so we just have to keep on going. The next iteration will take care of that // @TODO: maybe make it go to the start of the next month? // Make sure it goes ahead by at least 1 more day change += 1; } else { change += (7 * (entry.instance - instance)); } } if (change > 0 && change < smallest_change) { smallest_change = change; } } if (!isFinite(smallest_change)) { return false; } this.addUnitToDate(date, 'day_of_week', smallest_change); return true; }); /** * Modify dates using Last-days * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date The date to modify in-place * @param {Object} config The parsed Cron field expression * @param {string} unit The unit of time. Should always be day_of_week * * @return {boolean|null} True if the date was modified, false if it failed, null if it wasn't modified */ CronExpression.setMethod(function modifyDateUsingLastDays(date, config, unit) { let current_date = date.getDate(); let smallest_change = Infinity; // First see if the current unit is allowed by any of the steps for (let day_of_week of config.lastDays) { let wanted_date = this.getDayOfLastWantedDayOfWeek(date, day_of_week); if (current_date == wanted_date) { return null; } let change; if (wanted_date > current_date) { change = wanted_date - current_date; } else { continue; } if (change != null && change < smallest_change) { smallest_change = change; } } if (!isFinite(smallest_change)) { smallest_change = 1; } this.addUnitToDate(date, 'day_of_week', smallest_change); return true; }); /** * Modify dates using Last-weekday * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date The date to modify in-place * @param {Object} config The parsed Cron field expression * @param {string} unit The unit of time. Should always be day_of_week * * @return {boolean|null} True if the date was modified, false if it failed, null if it wasn't modified */ CronExpression.setMethod(function modifyDateUsingLastWeekday(date, config, unit) { let last_weekday = this.getDayOfLastWeekday(date), current_day = date.getDate(); if (last_weekday == current_day) { return null; } this.addUnitToDate(date, 'day', last_weekday - current_day); return true; }); /** * Modify dates using Last-day * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date The date to modify in-place * @param {Object} config The parsed Cron field expression * @param {string} unit The unit of time. Should always be day_of_week * * @return {boolean|null} True if the date was modified, false if it failed, null if it wasn't modified */ CronExpression.setMethod(function modifyDateUsingLastDay(date, config, unit) { let last_day = this.getUnitMax('day', date), current_day = date.getDate(); if (last_day == current_day) { return null; } this.addUnitToDate(date, 'day', last_day - current_day); return true; }); /** * Add the given amount of units to the date. * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date * @param {Object} config * @param {string} unit */ CronExpression.setMethod(function addUnitToDate(date, unit, amount) { if (unit == 'day_of_month') { unit = 'day'; } if (unit == 'day_of_week') { unit = 'day'; } // Now add the smallest change to the date date.add(amount, unit); // And go to the start of this unit date.startOf(unit); }); /** * Get the date of the last weekday of the current month * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date * * @return {number} The date of the last weekday */ CronExpression.setMethod(function getDayOfLastWeekday(date) { let last_date = this.getUnitMax('day', date); // Create a new date that is the last day of the month of the current month let end_of_month = new Date(date.getFullYear(), date.getMonth(), last_date, 23, 59, 0); // Day of week in JavaScript Date are from 0-6 // Sunday is 0, Monday is 1, ..., and Saturday is 6 let last_day = end_of_month.getDay(); if (last_day === 6) { // If it's Saturday (6), go back 1 day last_date -= 1; } else if (last_day === 0) { // If it's Sunday (0), go back 2 days last_date -= 2; } return last_date; }); /** * Get the date of the last type of weekday of the current month * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date * @param {number} day_of_week * * @return {number} The date of the last weekday */ CronExpression.setMethod(function getDayOfLastWantedDayOfWeek(date, wanted_day_of_week) { let last_date = this.getUnitMax('day', date); // Create a new date that is the last day of the month of the current month let end_of_month = new Date(date.getFullYear(), date.getMonth(), last_date, 23, 59, 0); // Get the last day_of_week of the month let last_day_of_week = end_of_month.getDay(); // Calculate difference let diff = last_day_of_week - wanted_day_of_week; if (diff < 0) { diff += 7; } // Calculate the date of the last wanted day_of_week let result = end_of_month.getDate() - diff; return result; }); /** * Get a specific unit of the given date * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {Date} date * @param {string} unit */ CronExpression.setMethod(function getUnit(date, unit) { switch (unit) { case 'year': return date.getFullYear(); case 'month': // In our cron system, months start at 1. // Because they start at 0 in JavaScript, we add 1. return date.getMonth() + 1; case 'day_of_month': case 'day': return date.getDate(); case 'hour': return date.getHours(); case 'minute': return date.getMinutes(); case 'second': return date.getSeconds(); case 'day_of_week': return date.getDay(); } }); /** * Get the maximum allowed value of the given unit * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {string} unit */ CronExpression.setMethod(function getUnitMax(unit, of_date) { // The `FIELD_INFO` object says the max amount of days in a week is 7, // but the `Date` object says it's 6, and that's what's important here. if (unit == 'day_of_week') { return 6; } // The max amount of dates in a month depend on the month if (of_date && (unit == 'day_of_month' || unit == 'day')) { let month = of_date.getMonth() + 1; let days_in_month; switch (month) { case 2: days_in_month = 28; break; case 4: case 6: case 9: case 11: days_in_month = 30; break; default: days_in_month = 31; } return days_in_month; } const info = FIELD_INFO[unit]; return info?.max; }); /** * Get the minimum allowed value of the given unit * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 * * @param {string} unit */ CronExpression.setMethod(function getUnitMin(unit, of_date) { const info = FIELD_INFO[unit]; return info?.min; }); /** * Convert any aliases to their expected value * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ function unalias(field, value) { if (!field) { return value; } const info = FIELD_INFO[field]; const unaliased = (info.alias || {})[value]; return unaliased === undefined ? value : unaliased.toString(); } /** * Create a new error for the current expression * * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ CronExpression.setMethod(function createInvalidExpressionError(msg) { return new Error(`Invalid cron expression [${this.expression}]. ${msg}`); }); /** * Deduplicate an array * * @author Santhosh Kumar <brsanthu@gmail.com> * @author Jelle De Loecker <jelle@elevenways.be> * @since 1.3.17 * @version 1.3.17 */ function dedupe(inArray, keySupplier = (it) => it) { const seen = new Set(); const deduped = []; inArray.forEach((x) => { const keyValue = keySupplier(x); if (!seen.has(keyValue)) { seen.add(keyValue); deduped.push(x); } }); return deduped; }