/** * This is the trigger used to decode the cronTimer and calculate the next excution time of the cron Trigger. */ var logger = require('log4js').getLogger(__filename); var SECOND = 0; var MIN = 1; var HOUR = 2; var DOM = 3; var MONTH = 4; var DOW = 5; var Limit = [[0,59],[0,59],[0,24],[1,31],[0,11],[0,6]]; /** * The constructor of the CronTrigger * @param trigger The trigger str used to build the cronTrigger instance */ var CronTrigger = function(trigger, job){ this.trigger = this.decodeTrigger(trigger); this.nextTime = this.nextExcuteTime(Date.now()); this.job = job; }; var pro = CronTrigger.prototype; /** * Get the current excuteTime of trigger */ pro.excuteTime = function(){ return this.nextTime; }; /** * Caculate the next valid cronTime after the given time * @param The given time point * @return The nearest valid time after the given time point */ pro.nextExcuteTime = function(time){ //add 1s to the time so it must be the next time time = !!time?time:this.nextTime; time += 1000; var cronTrigger = this.trigger; var date = new Date(time); date.setMilliseconds(0); outmost: while(true){ if(date.getFullYear() > 2999){ logger.error("Can't compute the next time, exceed the limit"); return null; } if(!timeMatch(date.getMonth(), cronTrigger[MONTH])){ var nextMonth = nextCronTime(date.getMonth(), cronTrigger[MONTH]); if(nextMonth == null) return null; if(nextMonth <= date.getMonth()){ date.setYear(date.getFullYear() + 1); date.setMonth(0); date.setDate(1); date.setHours(0); date.setMinutes(0); date.setSeconds(0); continue; } date.setMonth(nextMonth); date.setDate(1); date.setHours(0); date.setMinutes(0); date.setSeconds(0); } if(!timeMatch(date.getDate(), cronTrigger[DOM]) || !timeMatch(date.getDay(), cronTrigger[DOW])){ var domLimit = getDomLimit(date.getFullYear(), date.getMonth()); do{ var nextDom = nextCronTime(date.getDate(), cronTrigger[DOM]); if(nextDom == null) return null; //If the date is in the next month, add month if(nextDom <= date.getDate() || nextDom > domLimit){ date.setMonth(date.getMonth() + 1); date.setDate(1); date.setHours(0); date.setMinutes(0); date.setSeconds(0); continue outmost; } date.setDate(nextDom); }while(!timeMatch(date.getDay(), cronTrigger[DOW])); date.setHours(0); date.setMinutes(0); date.setSeconds(0); } if(!timeMatch(date.getHours(), cronTrigger[HOUR])){ var nextHour = nextCronTime(date.getHours(), cronTrigger[HOUR]); if(nextHour <= date.getHours()){ date.setDate(date.getDate() + 1); date.setHours(nextHour); date.setMinutes(0); date.setSeconds(0); continue; } date.setHours(nextHour); date.setMinutes(0); date.setSeconds(0); } if(!timeMatch(date.getMinutes(), cronTrigger[MIN])){ var nextMinute = nextCronTime(date.getMinutes(), cronTrigger[MIN]); if(nextMinute <= date.getMinutes()){ date.setHours(date.getHours() + 1); date.setMinutes(nextMinute); date.setSeconds(0); continue; } date.setMinutes(nextMinute); date.setSeconds(0); } if(!timeMatch(date.getSeconds(), cronTrigger[SECOND])){ var nextSecond = nextCronTime(date.getSeconds(), cronTrigger[SECOND]); if(nextSecond <= date.getSeconds()){ date.setMinutes(date.getMinutes() + 1); date.setSeconds(nextSecond); continue; } date.setSeconds(nextSecond); } break; } this.nextTime = date.getTime(); return this.nextTime; }; /** * return the next match time of the given value * @param value The time value * @param cronTime The cronTime need to match * @return The match value or null if unmatch(it offten means an error occur). */ function nextCronTime(value, cronTime){ value += 1; if(typeof(cronTime) == 'number'){ if(cronTime == -1) return value; else return cronTime; }else if(typeof(cronTime) == 'object' && cronTime instanceof Array){ if(value <= cronTime[0] || value > cronTime[cronTime.length -1]) return cronTime[0]; for(var i = 0; i < cronTime.length; i++) if(value <= cronTime[i]) return cronTime[i]; } logger.warn('Compute next Time error! value :' + value + ' cronTime : ' + cronTime); return null; } /** * Match the given value to the cronTime * @param value The given value * @param cronTime The cronTime * @return The match result */ function timeMatch(value, cronTime){ if(typeof(cronTime) == 'number'){ if(cronTime == -1) return true; if(value == cronTime) return true; return false; }else if(typeof(cronTime) == 'object' && cronTime instanceof Array){ if(value < cronTime[0] || value > cronTime[cronTime.length -1]) return false; for(var i = 0; i < cronTime.length; i++) if(value == cronTime[i]) return true; return false; } return null; } /** * Decude the cronTrigger string to arrays * @param cronTimeStr The cronTimeStr need to decode, like "0 12 * * * 3" * @return The array to represent the cronTimer */ pro.decodeTrigger = function(cronTimeStr){ var cronTimes = cronTimeStr.split(/\s+/); if(cronTimes.length != 6){ console.log('error'); return null; } for(var i = 0; i < cronTimes.length; i++){ cronTimes[i] = (this.decodeTimeStr(cronTimes[i], i)); if(!checkNum(cronTimes[i], Limit[i][0], Limit[i][1])){ logger.error('Decode crontime error, value exceed limit!' + JSON.stringify({cronTime: cronTimes[i], limit:Limit[i]})); return null; } } return cronTimes; } /** * Decode the cron Time string * @param timeStr The cron time string, like: 1,2 or 1-3 * @return A sorted array, like [1,2,3] */ pro.decodeTimeStr = function(timeStr, type){ var result = {}; var arr = []; if(timeStr=='*'){ return -1; }else if(timeStr.search(',')>0){ var timeArr = timeStr.split(','); for(var i = 0; i < timeArr.length; i++){ var time = timeArr[i]; if(time.match(/^\d+-\d+$/)){ decodeRangeTime(result, time); }else if(time.match(/^\d+\/\d+/)){ decodePeriodTime(result, time, type); }else if(!isNaN(time)){ var num = Number(time); result[num] = num; }else return null; } }else if(timeStr.match(/^\d+-\d+$/)){ decodeRangeTime(result, timeStr); }else if(timeStr.match(/^\d+\/\d+/)){ decodePeriodTime(result, timeStr, type); }else if(!isNaN(timeStr)){ var num = Number(timeStr); result[num] = num; }else{ return null; } for(var key in result){ arr.push(result[key]); } arr.sort(function(a, b){ return a - b; }); return arr; } /** * Decode time range * @param map The decode map * @param timeStr The range string, like 2-5 */ function decodeRangeTime(map, timeStr){ var times = timeStr.split('-'); times[0] = Number(times[0]); times[1] = Number(times[1]); if(times[0] > times[1]){ console.log("Error time range"); return null; } for(var i = times[0]; i <= times[1]; i++){ map[i] = i; } } /** * Compute the period timer */ function decodePeriodTime(map, timeStr, type){ var times = timeStr.split('/'); var min = Limit[type][0]; var max = Limit[type][1]; var remind = Number(times[0]); var period = Number(times[1]); if(period==0) return; for(var i = min; i <= max; i++){ if(i%period == remind) map[i] = i; } } /** * Check if the numbers are valid * @param nums The numbers array need to check * @param min Minimus value * @param max Maximam value * @return If all the numbers are in the data range */ function checkNum(nums, min, max){ if(nums == null) return false; if(nums == -1) return true; for(var i = 0; i < nums.length; i++){ if(nums[i]max) return false; } return true; } /** * Get the date limit of given month * @param The given year * @month The given month * @return The date count of given month */ function getDomLimit(year, month){ var date = new Date(year, month+1, 0); return date.getDate(); } /** * Create cronTrigger * @param trigger The Cron Trigger string * @return The Cron trigger */ function createTrigger(trigger, job){ return new CronTrigger(trigger, job); } module.exports.createTrigger = createTrigger;