Last active
September 1, 2024 10:59
-
-
Save zcweng/d68cc59ff7de3900d41194d6b6f35499 to your computer and use it in GitHub Desktop.
Cron的简单实现
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package org.example; | |
import lombok.RequiredArgsConstructor; | |
import java.time.LocalDateTime; | |
import java.time.temporal.TemporalAdjusters; | |
import java.util.Arrays; | |
import java.util.Objects; | |
import java.util.OptionalInt; | |
import java.util.stream.Collectors; | |
import java.util.stream.Stream; | |
import static java.time.DayOfWeek.SUNDAY; | |
import static java.time.Month.DECEMBER; | |
public class Cron { | |
public static void main(String[] args) { | |
System.out.println("Hello world!"); | |
final String text = "0 1/7 * * * *"; | |
final Cron cron = new Cron(text); | |
final LocalDateTime now = LocalDateTime.now(); | |
System.out.println(now); | |
System.out.println(text); | |
LocalDateTime next = now; | |
for (int i = 1; i <= 10; i++) { | |
next = cron.getNext(next); | |
System.out.println((i <= 9 ? "0":"")+i+":\t"+next); | |
next = next.plusSeconds(1); | |
} | |
} | |
private final Seconds seconds; | |
private final Minutes minutes; | |
private final Hours hours; | |
private final DayOfMonth dayOfMonth; | |
private final Month month; | |
private final DayOfWeek dayOfWeek; | |
// TODO not impl... | |
private final Holidays holidays; | |
private final Year year; | |
Cron(String text) { | |
holidays = new Holidays(); | |
final String[] splits = text.split(" "); | |
final Field[] fields = new Field[]{ | |
seconds = new Seconds(), | |
minutes = new Minutes(), | |
hours = new Hours(), | |
dayOfMonth = new DayOfMonth(), | |
month = new Month(), | |
dayOfWeek = new DayOfWeek(), | |
year = new Year(), | |
}; | |
for (int i = 0; i < splits.length && i < fields.length; i++) { | |
fields[i].text = splits[i]; | |
} | |
} | |
LocalDateTime getNext(LocalDateTime start) { | |
LocalDateTime time = start; | |
final Field[] fields = new Field[]{holidays,dayOfWeek, month, dayOfMonth, hours, minutes, seconds}; | |
while (time != null) { | |
time = year.getNext(time); | |
boolean match = true; | |
for (Field field : fields) { | |
final LocalDateTime _time = field.getNext(time); | |
if (_time != time) { | |
time = field.resetTail(_time); | |
match = false; | |
break; | |
} | |
} | |
if (! match) { | |
continue; | |
} | |
return time; | |
} | |
return time; | |
} | |
@Override | |
public String toString() { | |
final Field[] fields = new Field[]{ | |
seconds, | |
minutes, | |
hours, | |
dayOfMonth, | |
month, | |
dayOfWeek, | |
year, | |
}; | |
return Stream.of(fields).map(f -> f.text) | |
.map(t -> null == t ? "" : t) | |
.collect(Collectors.joining(" ")); | |
} | |
static class Express {} | |
static class ExpressBlank extends Express {} | |
/** | |
* 星期几字段可以使用“#”,后面必须跟一个介于1和5之间的数字。例如,5#3表示每个月的第三个星期五。 | |
* “day of month”字段可以使用“W”字符。指定最接近给定日期的工作日(星期一-星期五)。 | |
* 例如,15W,意思是:“最接近该月15日的工作日。”;所以,如果15号是星期六,触发器在14号星期五触发。如果15日是星期天,触发器在16日星期一触发。如果15号是星期二,那么它在15号星期二触发。“1W”,如果这个月的第一天是星期六,不会跨到上个月,触发器会在这个月的第三天(也就是星期一)触发。只有指定一天(不能是范围或列表)的时候,才能指定“W”字符。 | |
**/ | |
enum ExpressType { | |
BLANK, | |
/** | |
* 【*】:每的意思。在不同的字段上,就代表每秒,每分,每小时等。 | |
* */ | |
EVERY, | |
/** | |
* 【?】:仅用于【日】和【周】字段。因为在指定某日和周几的时候,这两个值实际上是冲突的,所以需要用【?】标识不生效的字段。比如【0 1 * * * ?】就代表每年每月每日每小时的1分0秒触发任务。这里的周就没有效果了。 | |
* */ | |
ANY, | |
/** | |
* 【-】:指定值的范围。比如[1-10],在秒字段里就是每分钟的第1到10秒,在分就是每小时的第1到10分钟,以此类推。 | |
* */ | |
RANGE, | |
/** | |
* 【,】:指定某几个值。比如[2,4,5],在秒字段里就是每分钟的第2,第4,第5秒,以此类推。 | |
* */ | |
OPTIONAL, | |
/** | |
* 【/】:指定值的起始和增加幅度。比如[3/5],在秒字段就是每分钟的第3秒开始,每隔5秒生效一次,也就是第3秒、8秒、13秒,以此类推。 | |
* */ | |
INCREMENT, | |
EXPR, | |
// L:即last,用于【日】【周】字段。这里需要注意的是,在不同的字段的不同使用方式,其含义有所差别。 | |
//用于日字段:直接使用L代表每个月的最后一天。也支持偏移量的方式,配置[L-1]则代表每月的倒数第二天。 | |
//用于周字段:直接使用L代表每周的最后一天,也就是等效于[7]或[SAT],但是如果配合上数字,比如[7L],则代表每个月最后一个周六,等效于[SATL]。目前Quartz支持。 | |
// LAST, | |
// FIRST; | |
; | |
boolean match(Field field) { | |
final String text = field.text; | |
return match(text); | |
} | |
static ExpressType getType(Field field) { | |
return getType(field.text); | |
} | |
boolean match(String text) { | |
switch (this) { | |
// case LAST: return text.contains("L"); | |
// case FIRST: return text.contains("F"); | |
case BLANK: return text == null || Objects.equals(text, ""); | |
case EVERY: return "*".equals(text); | |
case ANY: return "?".equals(text); | |
case RANGE: return text.matches("\\d+-\\d+") && ! (text.contains("L") || text.contains("F")); | |
case OPTIONAL: return text.matches("\\d+,\\d+(,\\d+)?"); | |
case INCREMENT: return text.matches("\\d+/\\d+"); | |
case EXPR: return true; | |
} | |
throw new IllegalStateException(); | |
} | |
static ExpressType getType(String text) { | |
for (ExpressType type : ExpressType.values()) { | |
if (type.match(text)) { | |
return type; | |
} | |
} | |
throw new IllegalArgumentException(); | |
} | |
} | |
static class SimpleOptional { | |
final Integer[] vs; | |
final Integer first, last; | |
SimpleOptional(String text) { | |
final String[] splits = text.split(","); | |
vs = new Integer[splits.length]; | |
for (int i = 0; i < splits.length; i++) { | |
vs[i] = Integer.parseInt(splits[i]); | |
} | |
Arrays.sort(vs); | |
first = vs[0]; | |
last = vs[vs.length-1]; | |
} | |
int getOffset(int value, int max) { | |
if (value < first) { | |
return (first-value); | |
} | |
if (last < value) { | |
return (max - value + 1); | |
} | |
for (int i = 0; i < vs.length-1; i++) { | |
if (value == vs[i]) { | |
return 0; | |
} | |
if (vs[i] < value && value < vs[i+1]) { | |
return (vs[i+1] - value); | |
} | |
} | |
return 0; | |
} | |
} | |
@RequiredArgsConstructor | |
static class SimpleRange { | |
final int from, to; | |
int getOffset(int value, int max) { | |
if (value == from || value == to) { | |
return 0; | |
} | |
if(from == to) { | |
if (value < from) { | |
return (from - value + 1); | |
} | |
return (max - value); | |
} | |
if (from < to) { | |
if (value < from) { | |
return (from-value); | |
} | |
if (value > to) { | |
return (max - value + 1); | |
} | |
return 0; | |
} | |
// to < from | |
if (to < value && value < from) { | |
return (from - value); | |
} | |
return 0; | |
} | |
} | |
static abstract class Field { | |
String text; | |
// abstract boolean illegal(); | |
abstract LocalDateTime getNext(LocalDateTime start); | |
ExpressType getType() { | |
return ExpressType.getType(this); | |
} | |
abstract LocalDateTime resetTail(LocalDateTime time); | |
} | |
class Year extends Field { | |
@Override | |
LocalDateTime getNext(LocalDateTime start) { | |
if (start == null) { | |
return null; | |
} | |
final ExpressType type = getType(); | |
final int year = start.getYear(); | |
switch (type) { | |
case EXPR: { | |
final int value = Integer.parseInt(text); | |
if (year < value) { | |
return start.withYear(value); | |
} | |
return start; | |
} | |
case BLANK: return start; | |
case EVERY: return start; | |
case RANGE: { | |
final String[] splits = text.split("-"); | |
final int from = Integer.parseInt(splits[0]); | |
final int to = Integer.parseInt(splits[1]); | |
if (year >= from && year <= to) { | |
return start; | |
} | |
return null; | |
} | |
case OPTIONAL: { | |
final String[] splits = text.split(","); | |
final OptionalInt minYear = Stream.of(splits) | |
.mapToInt(Integer::parseInt) | |
.filter(y -> y >= year) | |
.min(); | |
if (minYear.isPresent()) { | |
return start.withYear(minYear.getAsInt()); | |
} | |
return null; | |
} | |
case INCREMENT: { | |
final String[] splits = text.split("/"); | |
final int sy = Integer.parseInt(splits[0]); | |
final int incr = Integer.parseInt(splits[1]); | |
assert sy >= 1970 && sy <= 9999; | |
assert incr >= 0; | |
for (int y = sy; ; y+=incr) { | |
if (y >= year) { | |
return start.withYear(y); | |
} | |
} | |
} | |
} | |
throw new IllegalArgumentException("illegal "+text+" for year"); | |
} | |
@Override | |
LocalDateTime resetTail(LocalDateTime time) { | |
return month.resetTail(time.withMonth(1)); | |
} | |
} | |
/** | |
* *,-?L# | |
* ?L#只有部分软件实现了 | |
* Linux和Spring的允许值为0-7,0和7为周日 | |
* Quartz的允许值为1-7,1为周日 | |
*/ | |
class DayOfWeek extends Field { | |
@Override | |
LocalDateTime getNext(LocalDateTime start) { | |
if (start == null) { | |
return null; | |
} | |
final ExpressType type = getType(); | |
// 1-7 mon-sun | |
final int dw = start.getDayOfWeek().getValue(); | |
switch (type) { | |
case EXPR: { | |
final int value = Integer.parseInt(text); | |
if (dw == value) { | |
return start; | |
} | |
if (dw < value) { | |
return start.plusDays(value - dw); | |
} | |
return start.plusDays(SUNDAY.getValue() - dw + value); | |
} | |
case EVERY: return start; | |
case ANY: return start; | |
case RANGE: { | |
final String[] splits = text.split("-"); | |
final int from = Integer.parseInt(splits[0]); | |
final int to = Integer.parseInt(splits[1]); | |
final SimpleRange simpleRange = new SimpleRange(from, to); | |
final int offset = simpleRange.getOffset(dw, SUNDAY.getValue()); | |
if (offset == 0) { | |
return start; | |
} | |
return start.plusDays(offset); | |
} | |
case OPTIONAL: { | |
final SimpleOptional simpleOptional = new SimpleOptional(text); | |
final int offset = simpleOptional.getOffset(dw, SUNDAY.getValue()); | |
if (offset == 0) { | |
return start; | |
} | |
return start.plusDays(offset); | |
} | |
case INCREMENT: { | |
final String[] splits = text.split("/"); | |
final int sy = Integer.parseInt(splits[0]); | |
final int incr = Math.min(Integer.parseInt(splits[1]), SUNDAY.getValue()-sy); | |
for (int d = sy; ; d+=incr) { | |
if (d >= dw) { | |
return start.plusDays(d-dw); | |
} | |
} | |
} | |
} | |
throw new IllegalArgumentException("illegal "+text+" for day of week"); | |
} | |
@Override | |
LocalDateTime resetTail(LocalDateTime time) { | |
return hours.resetTail(time.withHour(0)); | |
} | |
} | |
class Holidays extends Field { | |
@Override | |
LocalDateTime getNext(LocalDateTime start) { | |
return start; | |
} | |
@Override | |
LocalDateTime resetTail(LocalDateTime time) { | |
return time; | |
} | |
} | |
class Month extends Field { | |
@Override | |
LocalDateTime getNext(LocalDateTime start) { | |
if (start == null) { | |
return null; | |
} | |
final ExpressType type = getType(); | |
final int month = start.getMonth().getValue(); | |
switch (type) { | |
case EXPR: { | |
final int value = Integer.parseInt(text); | |
if (month == value) { | |
return start; | |
} | |
if (month < value) { | |
return start.plusMonths(value - month); | |
} | |
return start.plusMonths(DECEMBER.getValue() - month + value); | |
} | |
case EVERY: return start; | |
case RANGE: { | |
final String[] splits = text.split("-"); | |
final int from = Integer.parseInt(splits[0]); | |
final int to = Integer.parseInt(splits[1]); | |
final SimpleRange simpleRange = new SimpleRange(from, to); | |
final int offset = simpleRange.getOffset(month, DECEMBER.getValue()); | |
if (offset == 0) { | |
return start; | |
} | |
return start.plusMonths(offset); | |
} | |
case OPTIONAL: { | |
final SimpleOptional simpleOptional = new SimpleOptional(text); | |
final int offset = simpleOptional.getOffset(month, DECEMBER.getValue()); | |
if (offset == 0) { | |
return start; | |
} | |
return start.plusMonths(offset); | |
} | |
case INCREMENT: { | |
final String[] splits = text.split("/"); | |
final int sy = Integer.parseInt(splits[0]); | |
final int incr = Math.min(Integer.parseInt(splits[1]), DECEMBER.getValue()-sy); | |
for (int d = sy; ; d+=incr) { | |
if (d >= month) { | |
return start.plusMonths(d-month); | |
} | |
} | |
} | |
} | |
throw new IllegalArgumentException("illegal "+text+" for month"); | |
} | |
@Override | |
LocalDateTime resetTail(LocalDateTime time) { | |
return dayOfMonth.resetTail(time.withDayOfMonth(1)); | |
} | |
} | |
/** | |
* *,-?LW | |
* ?LW只有部分软件实现了 | |
*/ | |
class DayOfMonth extends Field { | |
@Override | |
LocalDateTime getNext(LocalDateTime start) { | |
if (start == null) { | |
return null; | |
} | |
final ExpressType type = getType(); | |
final int dayOfMonth = start.getDayOfMonth(); | |
switch (type) { | |
case EXPR: { | |
final int value = Integer.parseInt(text); | |
if (dayOfMonth == value) { | |
return start; | |
} | |
if (dayOfMonth < value) { | |
return start.plusDays(value - dayOfMonth); | |
} | |
final int lastDayOfMonth = start.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth(); | |
return start.plusDays(lastDayOfMonth - dayOfMonth + value); | |
} | |
case ANY: return start; | |
case EVERY: return start; | |
case RANGE: { | |
final String[] splits = text.split("-"); | |
final int from = Integer.parseInt(splits[0]); | |
final int to = Integer.parseInt(splits[1]); | |
final SimpleRange simpleRange = new SimpleRange(from, to); | |
final int lastDayOfMonth = start.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth(); | |
final int offset = simpleRange.getOffset(dayOfMonth, lastDayOfMonth); | |
if (offset == 0) { | |
return start; | |
} | |
return start.plusDays(offset); | |
} | |
case OPTIONAL: { | |
final int lastDayOfMonth = start.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth(); | |
final SimpleOptional simpleOptional = new SimpleOptional(text); | |
final int offset = simpleOptional.getOffset(dayOfMonth, lastDayOfMonth); | |
if (offset == 0) { | |
return start; | |
} | |
return start.plusDays(offset); | |
} | |
case INCREMENT: { | |
final int lastDayOfMonth = start.with(TemporalAdjusters.lastDayOfMonth()).getDayOfMonth(); | |
final String[] splits = text.split("/"); | |
final int sy = Integer.parseInt(splits[0]); | |
final int incr = Math.min(Integer.parseInt(splits[1]), lastDayOfMonth-sy); | |
for (int d = sy; ; d+=incr) { | |
if (d >= dayOfMonth) { | |
return start.plusDays(d-dayOfMonth); | |
} | |
} | |
} | |
} | |
throw new IllegalArgumentException("illegal "+text+" for day of month"); | |
} | |
@Override | |
LocalDateTime resetTail(LocalDateTime time) { | |
return hours.resetTail(time.withHour(0)); | |
} | |
} | |
class Hours extends Field { | |
@Override | |
LocalDateTime getNext(LocalDateTime start) { | |
if (start == null) { | |
return null; | |
} | |
final ExpressType type = getType(); | |
final int hour = start.getHour(); | |
switch (type) { | |
case EXPR: { | |
final int value = Integer.parseInt(text); | |
if (hour == value) { | |
return start; | |
} | |
if (hour < value) { | |
return start.plusHours(value - hour); | |
} | |
return start.plusHours(24 - hour + value); | |
} | |
case EVERY: return start; | |
case RANGE: { | |
final String[] splits = text.split("-"); | |
final int from = Integer.parseInt(splits[0]); | |
final int to = Integer.parseInt(splits[1]); | |
final SimpleRange simpleRange = new SimpleRange(from, to); | |
final int offset = simpleRange.getOffset(hour, 24); | |
if (offset == 0) { | |
return start; | |
} | |
return start.plusHours(offset); | |
} | |
case OPTIONAL: { | |
final SimpleOptional simpleOptional = new SimpleOptional(text); | |
final int offset = simpleOptional.getOffset(hour, 24); | |
if (offset == 0) { | |
return start; | |
} | |
return start.plusHours(offset); | |
} | |
case INCREMENT: { | |
final String[] splits = text.split("/"); | |
final int sy = Integer.parseInt(splits[0]); | |
final int incr = Math.min(Integer.parseInt(splits[1]), 24-sy); | |
for (int d = sy; ; d+=incr) { | |
if (d >= hour) { | |
return start.plusHours(d-hour); | |
} | |
} | |
} | |
} | |
throw new IllegalArgumentException("illegal "+text+" for hour"); | |
} | |
@Override | |
LocalDateTime resetTail(LocalDateTime time) { | |
return minutes.resetTail(time.withMinute(0)); | |
} | |
} | |
class Minutes extends Field { | |
@Override | |
LocalDateTime getNext(LocalDateTime start) { | |
if (start == null) { | |
return null; | |
} | |
final ExpressType type = getType(); | |
final int minute = start.getMinute(); | |
switch (type) { | |
case EXPR: { | |
final int value = Integer.parseInt(text); | |
if (minute == value) { | |
return start; | |
} | |
if (minute < value) { | |
return start.plusMinutes(value - minute); | |
} | |
return start.plusMinutes(60 - minute + value); | |
} | |
case EVERY: return start; | |
case RANGE: { | |
final String[] splits = text.split("-"); | |
final int from = Integer.parseInt(splits[0]); | |
final int to = Integer.parseInt(splits[1]); | |
final SimpleRange simpleRange = new SimpleRange(from, to); | |
final int offset = simpleRange.getOffset(minute, 60); | |
if (offset == 0) { | |
return start; | |
} | |
return start.plusMinutes(offset); | |
} | |
case OPTIONAL: { | |
final SimpleOptional simpleOptional = new SimpleOptional(text); | |
final int offset = simpleOptional.getOffset(minute, 60); | |
if (offset == 0) { | |
return start; | |
} | |
return start.plusMinutes(offset); | |
} | |
case INCREMENT: { | |
final String[] splits = text.split("/"); | |
final int sy = Integer.parseInt(splits[0]); | |
final int incr = Math.min(Integer.parseInt(splits[1]), 60-sy); | |
for (int d = sy; ; d+=incr) { | |
if (d >= minute) { | |
return start.plusMinutes(d-minute); | |
} | |
} | |
} | |
} | |
throw new IllegalArgumentException("illegal "+text+" for minute"); | |
} | |
@Override | |
LocalDateTime resetTail(LocalDateTime time) { | |
return seconds.resetTail(time.withSecond(0)); | |
} | |
} | |
class Seconds extends Field { | |
@Override | |
LocalDateTime getNext(LocalDateTime start) { | |
if (start == null) { | |
return null; | |
} | |
final ExpressType type = getType(); | |
final int second = start.getSecond(); | |
switch (type) { | |
case EXPR: { | |
final int value = Integer.parseInt(text); | |
if (second == value) { | |
return start; | |
} | |
if (second < value) { | |
return start.plusSeconds(value - second); | |
} | |
return start.plusSeconds(60 - second + value); | |
} | |
case EVERY: return start; | |
case RANGE: { | |
final String[] splits = text.split("-"); | |
final int from = Integer.parseInt(splits[0]); | |
final int to = Integer.parseInt(splits[1]); | |
final SimpleRange simpleRange = new SimpleRange(from, to); | |
final int offset = simpleRange.getOffset(second, 60); | |
if (offset == 0) { | |
return start; | |
} | |
return start.plusSeconds(offset); | |
} | |
case OPTIONAL: { | |
final SimpleOptional simpleOptional = new SimpleOptional(text); | |
final int offset = simpleOptional.getOffset(second, 60); | |
if (offset == 0) { | |
return start; | |
} | |
return start.plusSeconds(offset); | |
} | |
case INCREMENT: { | |
final String[] splits = text.split("/"); | |
final int sy = Integer.parseInt(splits[0]); | |
final int incr = Math.min(Integer.parseInt(splits[1]), 60-sy); | |
for (int d = sy; ; d+=incr) { | |
if (d >= second) { | |
return start.plusSeconds(d-second); | |
} | |
} | |
} | |
} | |
throw new IllegalArgumentException("illegal "+text+" for second"); | |
} | |
@Override | |
LocalDateTime resetTail(LocalDateTime time) { | |
return time.withNano(0); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment