Skip to content

Commit

Permalink
!1189 尝试修复cron中在小月时使用“L”的计算bug
Browse files Browse the repository at this point in the history
Merge pull request !1189 from emptypoint/fix-cron-bug
  • Loading branch information
looly authored and gitee-org committed Mar 29, 2024
2 parents 339b886 + ac1246d commit bdaa89f
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@
* @author Looly
*/
public class DayOfMonthMatcher extends BoolArrayMatcher {
/**
* 是否是查询最后一天,即“L”
*/
private final boolean isLast;

/**
* 构造
*
* @param intValueList 匹配的日值
*/
public DayOfMonthMatcher(List<Integer> intValueList) {
public DayOfMonthMatcher(List<Integer> intValueList, boolean isLast) {
super(intValueList);
this.isLast = isLast;
}

/**
Expand Down Expand Up @@ -50,4 +55,9 @@ public boolean match(int value, int month, boolean isLeapYear) {
private static boolean isLastDayOfMonth(int value, int month, boolean isLeapYear) {
return value == Month.getLastDay(month - 1, isLeapYear);
}

public boolean isLast() {
return isLast;
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package cn.hutool.cron.pattern.matcher;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Month;
import cn.hutool.cron.pattern.Part;

import java.time.Year;
Expand Down Expand Up @@ -184,14 +186,23 @@ private int[] nextMatchValuesAfter(int[] values) {
// 周不参与计算
i--;
continue;
} else if (i == Part.DAY_OF_MONTH.ordinal()
&& matchers[i] instanceof DayOfMonthMatcher
&& ((DayOfMonthMatcher) matchers[i]).isLast()) {
int newMonth = newValues[Part.MONTH.ordinal()];
int newYear = newValues[Part.YEAR.ordinal()];
nextValue = Month.of(newMonth - 1).getLastDay(DateUtil.isLeapYear(newYear));
} else {
nextValue = matchers[i].nextAfter(values[i]);
}
nextValue = matchers[i].nextAfter(values[i]);
if (nextValue > values[i]) {
// 此部分正常获取新值,结束循环,后续的部分置最小值
newValues[i] = nextValue;
i--;
break;
} else if (nextValue < values[i]) {
// 回退前保存最新值
newValues[i] = nextValue;
// 此部分下一个值获取到的值产生回退,回到上一个部分,继续获取新值
i++;
nextValue = -1;// 标记回退查找
Expand All @@ -208,8 +219,15 @@ private int[] nextMatchValuesAfter(int[] values) {
// 周不参与计算
i++;
continue;
} else if (i == Part.DAY_OF_MONTH.ordinal()
&& matchers[i] instanceof DayOfMonthMatcher
&& ((DayOfMonthMatcher) matchers[i]).isLast()) {
int newMonth = newValues[Part.MONTH.ordinal()];
int newYear = newValues[Part.YEAR.ordinal()];
nextValue = Month.of(newMonth - 1).getLastDay(DateUtil.isLeapYear(newYear));
} else {
nextValue = matchers[i].nextAfter(values[i] + 1);
}
nextValue = matchers[i].nextAfter(values[i] + 1);
if (nextValue > values[i]) {
newValues[i] = nextValue;
i--;
Expand All @@ -234,7 +252,15 @@ private void setToMin(int[] values, int toPart) {
Part part;
for (int i = 0; i <= toPart; i++) {
part = Part.of(i);
values[i] = getMin(part);
if (part == Part.DAY_OF_MONTH
&& get(part) instanceof DayOfMonthMatcher
&& ((DayOfMonthMatcher) get(part)).isLast()) {
int newMonth = values[Part.MONTH.ordinal()];
int newYear = values[Part.YEAR.ordinal()];
values[i] = Month.of(newMonth - 1).getLastDay(DateUtil.isLeapYear(newYear));
} else {
values[i] = getMin(part);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,13 @@ public PartParser(Part part) {
* @return {@link PartMatcher}
*/
public PartMatcher parse(String value) {
// 是否是查询最后一天
boolean isLastDay = false;
if (isMatchAllStr(value)) {
//兼容Quartz的"?"表达式,不会出现互斥情况,与"*"作用相同
return new AlwaysTrueMatcher();
} else if ("L".equalsIgnoreCase(value)) {
isLastDay = true;
}

final List<Integer> values = parseArray(value);
Expand All @@ -76,7 +80,7 @@ public PartMatcher parse(String value) {

switch (this.part) {
case DAY_OF_MONTH:
return new DayOfMonthMatcher(values);
return new DayOfMonthMatcher(values, isLastDay);
case YEAR:
return new YearValueMatcher(values);
default:
Expand Down Expand Up @@ -226,7 +230,7 @@ private static boolean isMatchAllStr(String value) {
/**
* 解析单个int值,支持别名
*
* @param value 被解析的值
* @param value 被解析的值
* @param checkValue 是否检查值在有效范围内
* @return 解析结果
* @throws CronException 当无效数字或无效别名时抛出
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,135 @@ public void nextMatchAfterByWeekTest(){
final Calendar calendar = pattern.nextMatchAfter(time.toCalendar());
Assert.assertEquals("2022-04-09 01:01:01", DateUtil.date(calendar).toString());
}

@Test
public void testLastDayOfMonthForEveryMonth1() {
DateTime date = DateUtil.parse("2023-01-08 07:44:16");
DateTime result = DateUtil.parse("2023-01-31 03:02:01");
// 匹配所有月,生成每个月的最后一天
CronPattern pattern = new CronPattern("1 2 3 L * ?");
for (int i = 0; i < 30; i++) {
//noinspection ConstantConditions
Calendar calendar = pattern.nextMatchAfter(date.toCalendar());
date = DateUtil.date(calendar);
Assert.assertEquals(date, result);
// 加一秒
date = date.offset(DateField.SECOND, 1);

// 移动到下一个月的最后一天
result = result.offset(DateField.DAY_OF_MONTH, 1);
int lastDayOfMonth = DateUtil.getLastDayOfMonth(result);
result.setField(DateField.DAY_OF_MONTH, lastDayOfMonth);
}
}

@Test
public void testLastDayOfMonthForEveryMonth2() {
DateTime date = DateUtil.parse("2023-03-08 07:44:16");
DateTime result = DateUtil.parse("2023-03-31 03:02:01");
// 匹配所有月,生成每个月的最后一天
CronPattern pattern = new CronPattern("1 2 3 L * ?");
for (int i = 0; i < 30; i++) {
//noinspection ConstantConditions
Calendar calendar = pattern.nextMatchAfter(date.toCalendar());
date = DateUtil.date(calendar);
Assert.assertEquals(date, result);
// 加一秒
date = date.offset(DateField.SECOND, 1);

// 移动到下一个月的最后一天
result = result.offset(DateField.DAY_OF_MONTH, 1);
int lastDayOfMonth = DateUtil.getLastDayOfMonth(result);
result.setField(DateField.DAY_OF_MONTH, lastDayOfMonth);
}
}

@Test
public void testLastDayOfMonthForEveryYear1() {
DateTime date = DateUtil.parse("2023-01-08 07:44:16");
DateTime result = DateUtil.parse("2023-02-28 03:02:01");
// 匹配每一年2月的最后一天
CronPattern pattern = new CronPattern("1 2 3 L 2 ?");
for (int i = 0; i < 10; i++) {
//noinspection ConstantConditions
Calendar calendar = pattern.nextMatchAfter(date.toCalendar());
date = DateUtil.date(calendar);
Assert.assertEquals(date, result);
// 加一秒
date = date.offset(DateField.SECOND, 1);

// 移动到下一年的最后一天
result = result.offset(DateField.YEAR, 1);
int lastDayOfMonth = DateUtil.getLastDayOfMonth(result);
result.setField(DateField.DAY_OF_MONTH, lastDayOfMonth);
}
}

@Test
public void testLastDayOfMonthForEveryYear2() {
DateTime date = DateUtil.parse("2022-03-08 07:44:16");
DateTime result = DateUtil.parse("2023-02-28 03:02:01");
// 匹配每一年2月的最后一天
CronPattern pattern = new CronPattern("1 2 3 L 2 ?");
for (int i = 0; i < 30; i++) {
//noinspection ConstantConditions
Calendar calendar = pattern.nextMatchAfter(date.toCalendar());
date = DateUtil.date(calendar);
Assert.assertEquals(date, result);
// 加一秒
date = date.offset(DateField.SECOND, 1);

// 移动到下一年的最后一天
result = result.offset(DateField.YEAR, 1);
int lastDayOfMonth = DateUtil.getLastDayOfMonth(result);
result.setField(DateField.DAY_OF_MONTH, lastDayOfMonth);
}
}

@Test
public void testEveryHour() {
DateTime date = DateUtil.parse("2022-02-28 07:44:16");
DateTime result = DateUtil.parse("2022-02-28 08:02:01");
// 匹配每一年2月的最后一天
CronPattern pattern = new CronPattern("1 2 */1 * * ?");
for (int i = 0; i < 30; i++) {
//noinspection ConstantConditions
Calendar calendar = pattern.nextMatchAfter(date.toCalendar());
date = DateUtil.date(calendar);
Assert.assertEquals(date, result);
// 加一秒
date = date.offset(DateField.SECOND, 1);

// 移动到下一个小时
result = result.offset(DateField.HOUR_OF_DAY, 1);
}
}

@Test
public void testLastDayOfMonthForEveryHour() {
DateTime date = DateUtil.parse("2023-01-28 07:44:16");
DateTime result = DateUtil.parse("2023-01-31 00:00:00");
// 匹配每一年2月的最后一天
CronPattern pattern = new CronPattern("0 0 */1 L * ?");
for (int i = 0; i < 400; i++) {
//noinspection ConstantConditions
Calendar calendar = pattern.nextMatchAfter(date.toCalendar());
date = DateUtil.date(calendar);
Assert.assertEquals(date, result);
// 加一秒
date = date.offset(DateField.SECOND, 1);

// 移动到下一个小时
DateTime t = result.setMutable(false).offset(DateField.HOUR_OF_DAY, 1);
if (t.dayOfMonth() != result.dayOfMonth()) {
// 移动到下个月最后一天的开始
result = result.offset(DateField.DAY_OF_MONTH, 1);
int lastDayOfMonth = DateUtil.getLastDayOfMonth(result);
result = result.setField(DateField.DAY_OF_MONTH, lastDayOfMonth);
result = DateUtil.beginOfDay(result);
} else {
result = t;
}
}
}
}

0 comments on commit bdaa89f

Please sign in to comment.