Skip to content

Instantly share code, notes, and snippets.

@zcweng
Last active September 1, 2024 10:59
Show Gist options
  • Save zcweng/d68cc59ff7de3900d41194d6b6f35499 to your computer and use it in GitHub Desktop.
Save zcweng/d68cc59ff7de3900d41194d6b6f35499 to your computer and use it in GitHub Desktop.
Cron的简单实现
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