您的位置:

Cron表达式解析器详解

一、Cron表达式生成器

Cron表达式是一种在定时任务中经常使用的语法,它可以用来指定被执行的时间,像“每小时执行一次”或“每周星期五晚上10点执行”等等。由于Cron表达式的语法比较复杂,因此通常需要使用Cron表达式生成器来帮助用户快速生成想要的表达式。

下面是一个简单的Cron表达式生成器的示例,它可以让用户选择任务执行的时间单位、时间点和重复频率,然后根据用户的选择生成对应的Cron表达式:

function generateCronExpression() {
  // 获取用户的选择
  let timeUnit = document.getElementById("time-unit-select").value;
  let timePoint = document.getElementById("time-point-input").value;
  let repeatFrequency = document.getElementById("repeat-frequency-input").value;
  
  // 根据用户选择生成Cron表达式
  let cronExp = "";
  if (timeUnit === "hourly") {
    cronExp = `0 ${timePoint} 0/${repeatFrequency} * * *`;
  } else if (timeUnit === "daily") {
    cronExp = `0 ${timePoint} * * * *`;
  } else if (timeUnit === "weekly") {
    cronExp = `0 ${timePoint} * * ${repeatFrequency} *`;
  } else if (timeUnit === "monthly") {
    cronExp = `0 ${timePoint} ${repeatFrequency} * * *`;
  }
  
  document.getElementById("result").innerHTML = cronExp;
}

二、Cron表达式解析成时间

除了生成Cron表达式之外,我们还需要将Cron表达式解析成对应的时间点,以便于查看任务执行的计划。下面是一个简单的解析器,它可以将Cron表达式解析成下一次执行的时间点:

function getNextExecutionTime(cronExp) {
  let cronArr = cronExp.split(" ");
  let now = Date.now();
  
  // 解析分钟表达式
  let minuteExp = cronArr[0];
  let nextMinute = getNextValidValue(minuteExp, new Date(now).getMinutes());
  
  // 解析小时表达式
  let hourExp = cronArr[1];
  let nextHour = getNextValidValue(hourExp, new Date(now).getHours());
  
  // 解析日表达式
  let dayExp = cronArr[2];
  let nextDay = getNextValidValue(dayExp, new Date(now).getDate());
  
  // 解析月表达式
  let monthExp = cronArr[3];
  let nextMonth = getNextValidValue(monthExp, new Date(now).getMonth() + 1);
  
  // 解析星期表达式
  let weekExp = cronArr[4];
  let nextWeekDay = getNextValidValue(weekExp, new Date(now).getDay());
  
  // 解析年表达式
  let yearExp = cronArr[5];
  let nextYear = getNextValidValue(yearExp, new Date(now).getFullYear());
  
  // 计算下一次执行的时间点(注意要考虑到月份和年份的变化)
  let nextDate = new Date(nextYear, nextMonth - 1, nextDay, nextHour, nextMinute);
  while (nextDate.getTime() < now) {
    nextMonth++;
    if (nextMonth > 12) {
      nextMonth = 1;
      nextYear++;
    }
    nextDate.setFullYear(nextYear, nextMonth - 1, nextDay);
  }
  
  return nextDate;
}

function getNextValidValue(exp, currentValue) {
  // TODO:根据表达式解析出下一个合法的值
  // 略去具体实现
  return 0;
}

三、Cron表达式在线解析

为了方便用户查询指定Cron表达式的下一次执行时间点,我们可以提供一个在线的解析工具,用户只需要将表达式粘贴到输入框中,即可得到下一次的执行时间:

function parseCronExpression() {
  let cronExp = document.getElementById("cron-exp-input").value;
  let nextExecutionTime = getNextExecutionTime(cronExp);
  document.getElementById("next-execution-time").innerHTML = nextExecutionTime;
}

四、Cron表达式每5分钟

有时候我们需要将任务设置成每5分钟一次执行,这时候就需要使用*/5来指定:

0 */5 * * * *

五、Cron表达式解析

对于Cron表达式的解析,其实就是将表达式的各个部分解析出来,并根据这些部分计算下一次的执行时间点。下面是一个完整的Cron表达式解析器的实现,它可以解析所有Cron表达式,并计算出下一次的执行时间点:

function parseCronExpression(cronExp) {
  let cronArr = cronExp.split(" ");
  let now = Date.now();
  
  // 解析分钟表达式
  let minuteExp = cronArr[0];
  let nextMinute = getNextValidValues(minuteExp, new Date(now).getMinutes());
  
  // 解析小时表达式
  let hourExp = cronArr[1];
  let nextHour = getNextValidValues(hourExp, new Date(now).getHours());
  
  // 解析日表达式
  let dayExp = cronArr[2];
  let nextDay = getNextValidValues(dayExp, new Date(now).getDate());
  
  // 解析月表达式
  let monthExp = cronArr[3];
  let nextMonth = getNextValidValues(monthExp, new Date(now).getMonth() + 1);
  
  // 解析星期表达式
  let weekExp = cronArr[4];
  let nextWeekDay = getNextValidValues(weekExp, new Date(now).getDay());
  
  // 解析年表达式
  let yearExp = cronArr[5];
  let nextYear = getNextValidValues(yearExp, new Date(now).getFullYear());
  
  let nextExecutionTimes = [];
  
  for (let i = 0; i < nextMinute.length; i++) {
    for (let j = 0; j < nextHour.length; j++) {
      for (let k = 0; k < nextDay.length; k++) {
        for (let l = 0; l < nextMonth.length; l++) {
          let date = new Date(nextYear[0], nextMonth[l] - 1, nextDay[k], nextHour[j], nextMinute[i]);
          while (date.getTime() < now) {
            // 如果时间已经过去了,则需要加上一个周期
            if (monthExp === "*" && weekExp === "*") {
              // 每天执行的任务
              date.setDate(date.getDate() + 1);
            } else if (monthExp === "*" && weekExp !== "*") {
              // 每周某一天执行的任务
              date.setDate(date.getDate() + 7);
            } else if (monthExp !== "*" && weekExp === "*") {
              // 每月某一天执行的任务
              let tempDate = new Date(date.getFullYear(), date.getMonth() + 1, 0);
              date.setDate(tempDate.getDate() + 1);
              if (date.getMonth() + 1 !== nextMonth[l]) {
                // 某个月没有对应日期,需要调整到下一个合法日期
                date.setDate(1);
                date.setMonth(date.getMonth() + 1);
              }
            } else {
              // 每年某一天执行的任务
              let tempDate = new Date(date.getFullYear(), nextMonth[l], 0);
              date.setDate(tempDate.getDate() + 1);
              if (date.getMonth() + 1 !== nextMonth[l] || date.getDay() !== nextWeekDay[0]) {
                // 某个月没有对应的星期,或者对应的星期不是指定的星期,需要调整到下一个合法日期
                date.setDate(1);
                date.setMonth(date.getMonth() + 1);
                while (date.getDay() !== nextWeekDay[0]) {
                  date.setDate(date.getDate() + 1);
                }
              }
            }
          }
          
          nextExecutionTimes.push(date);
        }
      }
    }
  }
  
  return nextExecutionTimes;
}

function getNextValidValues(exp, currentValue) {
  let result = [];
  if (exp === "*") {
    // 如果表达式为*,则返回对应取值范围内的所有值
    for (let i = getMinValue(exp); i <= getMaxValue(exp); i++) {
      result.push(i);
    }
  } else if (exp.indexOf("/") !== -1) {
    // 如果表达式中包含/,则按照步长来获取合法值
    let arr = exp.split("/");
    let startValue = getMinValue(arr[0]);
    let step = parseInt(arr[1]);
    for (let i = startValue; i <= getMaxValue(exp); i += step) {
      result.push(i);
    }
  } else if (exp.indexOf(",") !== -1) {
    // 如果表达式中包含,,则将其拆分成多个子表达式
    let arr = exp.split(",");
    for (let i = 0; i < arr.length; i++) {
      result = result.concat(getNextValidValues(arr[i], currentValue));
    }
    result = Array.from(new Set(result)); // 去重
    result.sort(function(a, b) { return a - b; }); // 排序
  } else if (exp.indexOf("-") !== -1) {
    // 如果表达式中包含-,则按照范围来获取合法值
    let arr = exp.split("-");
    let minValue = parseInt(arr[0]);
    let maxValue = parseInt(arr[1]);
    for (let i = minValue; i <= maxValue; i++) {
      result.push(i);
    }
  } else {
    // 如果表达式为单个数值,则直接返回该值
    result.push(parseInt(exp));
  }
  
  // 对于星期表达式,还需要特殊处理,因为星期的取值范围是0~6,但是Date对象中的星期是从0开始的
  // 因此需要将所有星期的值+1,如果越界了则取模
  if (exp.startsWith("0") && result.indexOf(7) !== -1) {
    result.push(0);
  } else if (exp.startsWith("7") && result.indexOf(0) !== -1) {
    result.push(7);
  }
  result = result.map(function(value) { return (value === 7 ? 0 : value + 1); });
  for (let i = 0; i < result.length; i++) {
    result[i] %= 7;
  }
  
  // 如果当前值不在合法值的范围内,需要找到比它大的第一个合法值
  let index = result.indexOf(currentValue);
  if (index === -1) {
    for (let i = 0; i < result.length; i++) {
      if (result[i] > currentValue) {
        index = i;
        break;
      }
    }
  }
  
  // 如果当前值在合法值的范围内,直接从它开始向后找到下一个合法值
  if (index !== -1) {
    for (let i = index; i < result.length; i++) {
      if (result[i] >= currentValue) {
        result = result.slice(i);
        break;
      }
    }
  }
  
  // 如果找不到合法值,说明下一次执行时间已经超过了指定的时间范围,这时候需要将结果清空
  if (result.length === 0) {
    result.push(getMinValue(exp));
  }
  
  return result;
}

function getMinValue(exp) {
  // TODO:根据表达式获取取值范围的最小值
  // 略去具体实现
  return 0;
}

function getMaxValue(exp) {
  // TODO:根据表达式获取取值范围的最大值
  // 略去具体实现
  return 59;
}

六、Cron表达式在线生成器

除了简单的Cron表达式生成器之外,我们还可以提供一个功能更加强大的在线生成器,让用户可以通过界面来选择更加复杂的任务执行计划:

// 界面部分
// 略...

// 事件处理部分
function generateCronExpression() {
  let minuteExp = getCronExp("minute");
  let hourExp = getCronExp("hour");
  let dayExp = getCronExp("day");
  let monthExp = getCronExp("month");
  let weekExp = getCronExp("week");
  
  let