Cron 表达式解析,crontab表达式解析
前言
将类似
30 8 * * 1,4,6
这种的表达式解析为执行时间,获取下一次或者多次的执行时间。
一、代码展示
代码如下(示例):
import cn.hutool.core.date.DateUtil;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Set;
import java.util.TreeSet;
import static java.util.Calendar.DATE;
import static java.util.Calendar.DAY_OF_YEAR;
/**
* @author ******
* Created on 2022/3/15.
*
* Cron 表达式解析
*/
public class CronParser {
private String cronExp;
public CronParser(String exp) {
this.cronExp = exp;
}
public Date nextDate(Date start) {
Calendar lastCal = Calendar.getInstance();
lastCal.setTime(start);
//上一次执行时间字段
int lastYear = lastCal.get(Calendar.YEAR);
int lastMonth = lastCal.get(Calendar.MONTH) + 1;
int lastDay = lastCal.get(Calendar.DAY_OF_MONTH);
int lastMonthDay = this.encodeMonthday(lastMonth, lastDay);
int lastHour = lastCal.get(Calendar.HOUR_OF_DAY);
int lastMinute = lastCal.get(Calendar.MINUTE);
int lastSecond = lastCal.get(Calendar.SECOND);
//下一次执行时间字段
Integer newMonthDay = null;
Integer newHour = null;
Integer newMinute = null;
Integer newYear = lastYear;
//解析cron表达式
String[] exps = cronExp.split("\\s+");
CronRange minute = parseRange(exps[0], 0, 59);
CronRange hour = parseRange(exps[1], 0, 23);
CronRange day = parseRange(exps[2], 1, 31);
CronRange month = parseRange(exps[3], 1, 12);
CronRange week = parseRange(exps[4], 1, 7);
CronRange monthDay = this.calMonthDay(month, day, week);
if (monthDay.isEmpty()) {
return null;
}
boolean isNotFound = false;
if (monthDay.inRange(lastMonthDay)) {
if (hour.inRange(lastHour)) {
if (minute.inRange(lastMinute)) {
newMinute = minute.getNextValue(lastMinute);
}
if (newMinute == null) {
//如果分钟找不到,需要对小时进行递增
newHour = hour.getNextValue(lastHour);
isNotFound = newHour == null;
newMinute = minute.getMin();
} else {
newHour = lastHour;
}
}
if (newHour == null) {
if (isNotFound) {
//如果小时找不到,需要对天数进行递增
if (monthDay.isAll()) {
Calendar c = Calendar.getInstance();
c.setTime(start);
c.add(DATE, 1);
newMonthDay = this.encodeMonthday(c.get(Calendar.MONTH) + 1, c.get(Calendar.DAY_OF_MONTH));
} else {
//如果跨年了就找不到
newMonthDay = monthDay.getNextValue(lastMonthDay);
}
} else {
newMonthDay = lastMonthDay;
}
newHour = hour.getMin();
newMinute = minute.getMin();
} else {
newMonthDay = lastMonthDay;
}
} else {
//天如果不在范围内,需要对天进行递增
newMonthDay = monthDay.getNextValue(lastMonthDay);
newHour = hour.getMin();
newMinute = minute.getMin();
}
if (newMonthDay == null) {
//跨年
newYear = newYear + 1;
if (monthDay.isAll()) {
//1月1日
newMonthDay = 0x0101;
} else {
newMonthDay = monthDay.getMin();
}
newHour = hour.getMin();
newMinute = minute.getMin();
}
Calendar newCal = Calendar.getInstance();
newCal.set(Calendar.MONTH, this.decodeMonth(newMonthDay) - 1);
newCal.set(Calendar.DAY_OF_MONTH, decodeDay(newMonthDay));
newCal.set(Calendar.HOUR_OF_DAY, newHour);
newCal.set(Calendar.MINUTE, newMinute);
newCal.set(Calendar.SECOND, lastSecond);
newCal.set(Calendar.YEAR, newYear);
return newCal.getTime();
}
/**
* 将月和日合并成一个int型整数
* @param month
* @param day
* @return
*/
public int encodeMonthday(int month, int day) {
return (day & 0x000000FF) | (month << 8 & 0x0000FF00);
}
/**
* 解码月
* @param monthDay
* @return
*/
public int decodeMonth(int monthDay) {
return (monthDay & 0x0000FF00) >> 8;
}
/**
* 解码日
* @param monthDay
* @return
*/
public int decodeDay(int monthDay) {
return (monthDay & 0x000000FF);
}
private CronRange calMonthDay(CronRange month, CronRange day, CronRange week) {
CronRange monthDay = new CronRange();
if (month.isAll() && day.isAll() && week.isAll()) {
//如果都是全范围的就不进行计算
monthDay.setReturnAll(true);
return monthDay;
}
int[] monthDays = {31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
//如果是闰年就是29天
monthDays[1] = Calendar.getInstance().getActualMaximum(DAY_OF_YEAR) > 365 ? 29 : 28;
Set<Integer> rangeMonth = month.getRange();
for (Integer m : rangeMonth) {
for (int d = 1; d <= monthDays[m - 1]; ++d) {
if (day.inRange(d)) {
//判断周的逻辑
if (!week.isAll()) {
Calendar cal = Calendar.getInstance();
cal.set(Calendar.MONTH, m - 1);
cal.set(Calendar.DAY_OF_MONTH, d);
int w = cal.get(Calendar.DAY_OF_WEEK) - 1;
//周日-周六==>1-7
w = w == 0 ? 7 : w;
if (!week.inRange(w)) {
continue;
}
}
monthDay.addRange(this.encodeMonthday(m, d));
}
}
}
return monthDay;
}
/**
* 解析表达式的取值范围和循环周期
*
* @param exp
* @param start
* @param end
* @return
*/
public CronRange parseRange(String exp, int start, int end) {
String[] exps = exp.trim().split("/");
CronRange range = new CronRange();
if (exps.length > 1) {
range.setCycle(Integer.parseInt(exps[1]));
}
if (exps[0].trim().length() == 0) {
range.range(start, end);
} else if ("*".equals(exps[0])) {
range.range(start, end);
range.setReturnAll(exps.length == 1);
} else if (exps[0].contains("-")) {
String[] ss = exps[0].split("-");
range.range(Integer.parseInt(ss[0]), Integer.parseInt(ss[1]));
} else if (exps[0].contains(",")) {
String[] ss = exps[0].split(",");
for (String s : ss) {
range.addRange(Integer.parseInt(s));
}
} else {
range.addRange(Integer.parseInt(exps[0]));
}
return range;
}
public static void main(String[] args) throws ParseException {
String cronExp = "30 8 * * 1,4,6";
CronParser parser = new CronParser(cronExp);
String lastExecuteDateStr = "2022-03-12 22:23:22";
Date date = DateUtil.date();
SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm");
Date lastExecuteDate = fmt.parse(lastExecuteDateStr);
for (int i = 0; ; ++i) {
lastExecuteDate = parser.nextDate(lastExecuteDate);
if (DateUtil.compare(lastExecuteDate,date)>0) {
System.out.println(fmt.format(lastExecuteDate));
break;
}
System.out.println(fmt.format(lastExecuteDate));
}
}
}
class CronRange {
private Set<Integer> range = new TreeSet<>();
private Integer cycle;
private Integer max = null;
private Integer min = null;
private Boolean returnAll = false;
public CronRange range(int start, int end) {
for (int i = start; i <= end; ++i) {
this.addRange(i);
}
return this;
}
public CronRange addRange(int value) {
max = (max == null || value > max) ? value : max;
min = (min == null || value < min) ? value : min;
this.range.add(value);
return this;
}
public Set<Integer> getRange() {
return range;
}
public void setCycle(Integer cycle) {
this.cycle = cycle;
}
public boolean inRange(int value) {
return returnAll ? true : range.contains(value);
}
public boolean isEmpty() {
return !returnAll && range.isEmpty();
}
public Integer getNextValue(int lastValue) {
Integer value = null;
if (this.cycle != null) {
value = this.cycle + lastValue;
while (!inRange(value)) {
value = value + this.cycle;
if (value > max) {
value = null;
break;
}
}
} else {
value = this.getNextMin(lastValue);
}
return value;
}
private Integer getNextMin(int value) {
Integer[] integers = range.toArray(new Integer[range.size()]);
Integer minValue = null;
for (int i = 0; i < integers.length; ++i) {
if (integers[i] > value) {
minValue = integers[i];
break;
}
}
return minValue;
}
public Boolean isAll() {
return returnAll;
}
public void setReturnAll(Boolean returnAll) {
this.returnAll = returnAll;
}
public Integer getMin() {
return min;
}
}