diff --git a/CHANGELOG.md b/CHANGELOG.md
index feba12b43..5c4d709cc 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,7 @@
# 🚀Changelog
-------------------------------------------------------------------------------------------------------------
-# 5.8.41(2025-09-04)
+# 5.8.41(2025-09-06)
### 🐣新特性
* 【core 】 增加`WeakKeyValueConcurrentMap`及其关联类,同时废弃`WeakConcurrentMap`并替换(issue#4039@Github)
@@ -20,6 +20,7 @@
* 【db 】 修复`Condition`的`Condition("discount_end_time", "!=", (String) null)`方法生成SQL时,生成SQL不符合预期要求的错误(pr#4042@Github)
* 【core 】 修复`IoUtil`的`closeIfPosible`拼写错误,新建一个`closeIfPossible`方法,原方法标记deprecated(issue#4047@Github)
* 【http 】 修复`HttpRequest.sendRedirectIfPossible`未对308做判断问题。(issue#4053@Github)
+* 【cron 】 修复`CronPatternUtil.nextDateAfter`当日为L时计算错误问题。(issue#4056@Github)
-------------------------------------------------------------------------------------------------------------
# 5.8.40(2025-08-26)
diff --git a/hutool-cron/pom.xml b/hutool-cron/pom.xml
index 1fde97010..bf5b55aea 100755
--- a/hutool-cron/pom.xml
+++ b/hutool-cron/pom.xml
@@ -36,6 +36,12 @@
hutool-setting
${project.parent.version}
+
+ org.quartz-scheduler
+ quartz
+ 2.4.0
+ test
+
diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java
index 1c80205d3..05eb6bfff 100755
--- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java
+++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/CronPattern.java
@@ -6,11 +6,7 @@ import cn.hutool.cron.pattern.matcher.PatternMatcher;
import cn.hutool.cron.pattern.parser.PatternParser;
import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.GregorianCalendar;
-import java.util.List;
-import java.util.TimeZone;
+import java.util.*;
/**
* 定时任务表达式
diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/Part.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/Part.java
index 7178aab32..0c75db25a 100644
--- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/Part.java
+++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/Part.java
@@ -23,7 +23,7 @@ public enum Part {
SECOND(Calendar.SECOND, 0, 59),
MINUTE(Calendar.MINUTE, 0, 59),
HOUR(Calendar.HOUR_OF_DAY, 0, 23),
- DAY_OF_MONTH(Calendar.DAY_OF_MONTH, 1, 31),
+ DAY_OF_MONTH(Calendar.DAY_OF_MONTH, 1, 32),
MONTH(Calendar.MONTH, Month.JANUARY.getValueBaseOne(), Month.DECEMBER.getValueBaseOne()),
DAY_OF_WEEK(Calendar.DAY_OF_WEEK, Week.SUNDAY.ordinal(), Week.SATURDAY.ordinal()),
YEAR(Calendar.YEAR, 1970, 2099);
diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/BoolArrayMatcher.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/BoolArrayMatcher.java
index d60909ec5..46f20adeb 100644
--- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/BoolArrayMatcher.java
+++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/BoolArrayMatcher.java
@@ -18,7 +18,12 @@ public class BoolArrayMatcher implements PartMatcher {
* 用户定义此字段的最小值
*/
private final int minValue;
- private final boolean[] bValues;
+ /**
+ * 用户定义此字段的最大值
+ * @since 5.8.41
+ */
+ private final int maxValue;
+ protected final boolean[] bValues;
/**
* 构造
@@ -29,26 +34,37 @@ public class BoolArrayMatcher implements PartMatcher {
Assert.isTrue(CollUtil.isNotEmpty(intValueList), "Values must be not empty!");
bValues = new boolean[Collections.max(intValueList) + 1];
int min = Integer.MAX_VALUE;
+ int max = 0;
for (Integer value : intValueList) {
min = Math.min(min, value);
+ max = Math.max(max, value);
bValues[value] = true;
}
this.minValue = min;
+ this.maxValue = max;
}
@Override
public boolean match(Integer value) {
- if (null == value || value >= bValues.length) {
- return false;
+ if(null != value && value >= minValue && value <= maxValue){
+ return bValues[value];
}
- return bValues[value];
+ return false;
}
@Override
public int nextAfter(int value) {
- if(value > minValue){
- while(value < bValues.length){
- if(bValues[value]){
+ final int maxValue = this.maxValue;
+ if(value == maxValue){
+ return value;
+ }
+ final int minValue = this.minValue;
+ if(value > minValue && value < maxValue){
+ final boolean[] bValues = this.bValues;
+ // 最大值永远小于数组长度,只需判断最大值边界
+ while(value <= maxValue){
+ if(value == maxValue || bValues[value]){
+ // 达到最大值或达到第一个匹配值
return value;
}
value++;
@@ -70,6 +86,16 @@ public class BoolArrayMatcher implements PartMatcher {
return this.minValue;
}
+ /**
+ * 获取表达式定义的最大值
+ *
+ * @return 最大值
+ * @since 5.8.41
+ */
+ public int getMaxValue() {
+ return this.maxValue;
+ }
+
@Override
public String toString() {
return StrUtil.format("Matcher:{}", new Object[]{this.bValues});
diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/DayOfMonthMatcher.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/DayOfMonthMatcher.java
index c1deb0f1c..cfe240a84 100644
--- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/DayOfMonthMatcher.java
+++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/DayOfMonthMatcher.java
@@ -11,6 +11,12 @@ import java.util.List;
* @author Looly
*/
public class DayOfMonthMatcher extends BoolArrayMatcher {
+
+ /**
+ * 最后一天
+ */
+ private static final int LAST_DAY = 32;
+
/**
* 构造
*
@@ -23,47 +29,135 @@ public class DayOfMonthMatcher extends BoolArrayMatcher {
/**
* 给定的日期是否匹配当前匹配器
*
- * @param value 被检查的值,此处为日
+ * @param dayValue 被检查的值,此处为日
* @param month 实际的月份,从1开始
* @param isLeapYear 是否闰年
* @return 是否匹配
*/
- public boolean match(int value, int month, boolean isLeapYear) {
- return (super.match(value) // 在约定日范围内的某一天
- //匹配器中用户定义了最后一天(31表示最后一天)
- || (value > 27 && match(31) && isLastDayOfMonth(value, month, isLeapYear)));
+ public boolean match(int dayValue, int month, boolean isLeapYear) {
+ return (super.match(dayValue) // 在约定日范围内的某一天
+ //匹配器中用户定义了最后一天(32表示最后一天)
+ || matchLastDay(dayValue, month, isLeapYear));
}
/**
- * 是否为本月最后一天,规则如下:
- *
- * 1、闰年2月匹配是否为29
- * 2、其它月份是否匹配最后一天的日期(可能为30或者31)
- *
+ * 获取指定日之后的匹配值,也可以是其本身
+ * 如果表达式中存在最后一天(如使用"L"),则:
+ *
+ * - 4月、6月、9月、11月最多匹配到30日
+ * - 4月闰年匹配到29日,非闰年28日
+ *
*
- * @param value 被检查的值
+ * @param dayValue 指定的天值
* @param month 月份,从1开始
- * @param isLeapYear 是否闰年
- * @return 是否为本月最后一天
+ * @param isLeapYear 是否为闰年
+ * @return 匹配到的值或之后的值
+ * @since 5.8.41
*/
- private static boolean isLastDayOfMonth(int value, int month, boolean isLeapYear) {
- return value == Month.getLastDay(month - 1, isLeapYear);
+ public int nextAfter(int dayValue, final int month, final boolean isLeapYear) {
+ final int maxValue = getMaxValue(month, isLeapYear);
+ final int minValue = getMinValue(month, isLeapYear);
+ if (dayValue > minValue) {
+ final boolean[] bValues = this.bValues;
+ // 最大值永远小于数组长度,只需判断最大值边界
+ while (dayValue <= maxValue) {
+ // 匹配到有效值
+ if (bValues[dayValue] ||
+ // 如果最大值不在有效值中,这个最大值表示最后一天,则在包含了最后一天的情况下返回最后一天
+ (dayValue == maxValue && match(LAST_DAY))) {
+ return dayValue;
+ }
+ dayValue++;
+ }
+ }
+
+ // 两种情况返回最小值
+ // 一是给定值小于最小值,那下一个匹配值就是最小值
+ // 二是给定值大于最大值,那下一个匹配值也是下一轮的最小值
+ return minValue;
}
+ /**
+ * 是否包含最后一天
+ *
+ * @return 包含最后一天
+ */
public boolean isLast() {
- return match(31);
+ return match(32);
}
/**
* 检查value是这个月的最后一天
+ *
* @param value 被检查的值
- * @return
+ * @param month 月份,从1开始
+ * @param isLeapYear 是否闰年
+ * @return 是否是这个月的最后
*/
- public boolean isLastDay(Integer value,Integer month, boolean isLeapYear) {
- if(isLastDayOfMonth(value, month, isLeapYear)) {
- return match(31);
- }
- return false;
+ public boolean isLastDay(Integer value, Integer month, boolean isLeapYear) {
+ return matchLastDay(value, month, isLeapYear);
}
+ /**
+ * 获取表达式定义中指定月的最小日的值
+ *
+ * @param month 月,base1
+ * @param isLeapYear 是否闰年
+ * @return 匹配的最小值
+ * @since 5.8.41
+ */
+ public int getMinValue(final int month, final boolean isLeapYear) {
+ final int minValue = super.getMinValue();
+ if (LAST_DAY == minValue) {
+ // 用户指定了 L 等表示最后一天
+ return getLastDay(month, isLeapYear);
+ }
+ return minValue;
+ }
+
+ /**
+ * 获取表达式定义中指定月的最大日的值
+ * 首先获取表达式定义的最大值,如果这个值大于本月最后一天,则返回最后一天,否则返回用户定义的最大值
+ * 注意最后一天可能不是表达式中定义的有效值
+ *
+ * @param month 月,base1
+ * @param isLeapYear 是否闰年
+ * @return 匹配的最大值
+ * @since 5.8.41
+ */
+ public int getMaxValue(final int month, final boolean isLeapYear) {
+ return Math.min(super.getMaxValue(), getLastDay(month, isLeapYear));
+ }
+
+ /**
+ * 是否匹配本月最后一天,规则如下:
+ *
+ * 1、闰年2月匹配是否为29
+ * 2、其它月份是否匹配最后一天的日期(可能为30或者31)
+ * 3、表达式包含最后一天(使用31表示)
+ *
+ *
+ * @param dayValue 被检查的值
+ * @param month 月,base1
+ * @param isLeapYear 是否闰年
+ * @return 是否为本月最后一天
+ */
+ private boolean matchLastDay(final int dayValue, final int month, final boolean isLeapYear) {
+ return dayValue > 27
+ // 表达式中定义包含了最后一天
+ && match(LAST_DAY)
+ // 用户指定的日正好是最后一天
+ && dayValue == getLastDay(month, isLeapYear);
+ }
+
+ /**
+ * 获取最后一天
+ *
+ * @param month 月,base1
+ * @param isLeapYear 是否闰年
+ * @return 最后一天
+ */
+ private static int getLastDay(final int month, final boolean isLeapYear) {
+ return Month.getLastDay(month - 1, isLeapYear);
+ }
}
diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/PatternMatcher.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/PatternMatcher.java
index 146beec0b..e73c7d3bf 100755
--- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/PatternMatcher.java
+++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/matcher/PatternMatcher.java
@@ -177,8 +177,6 @@ public class PatternMatcher {
* @return {@link Calendar},毫秒数为0
*/
private int[] nextMatchValuesAfter(int[] values) {
- final int[] newValues = values.clone();
-
int i = Part.YEAR.ordinal();
// 新值,-1表示标识为回退
int nextValue = 0;
@@ -189,30 +187,20 @@ public class PatternMatcher {
continue;
}
- // pr#1189
- if (i == Part.DAY_OF_MONTH.ordinal()
- && matchers[i] instanceof DayOfMonthMatcher
- && ((DayOfMonthMatcher) matchers[i]).isLastDay(values[i],values[i+1],DateUtil.isLeapYear(values[Part.YEAR.ordinal()]))) {
- int newMonth = newValues[Part.MONTH.ordinal()];
- int newYear = newValues[Part.YEAR.ordinal()];
- nextValue = getLastDay(newMonth, newYear);
- } else {
- nextValue = matchers[i].nextAfter(values[i]);
- }
+ nextValue = getNextMatch(values, i, 0);
if (nextValue > values[i]) {
// 此部分正常获取新值,结束循环,后续的部分置最小值
- newValues[i] = nextValue;
+ values[i] = nextValue;
i--;
break;
} else if (nextValue < values[i]) {
- // 回退前保存最新值
- newValues[i] = nextValue;
// 此部分下一个值获取到的值产生回退,回到上一个部分,继续获取新值
i++;
nextValue = -1;// 标记回退查找
break;
}
+
// 值不变,检查下一个部分
i--;
}
@@ -224,17 +212,12 @@ public class PatternMatcher {
// 周不参与计算
i++;
continue;
- } else if (i == Part.DAY_OF_MONTH.ordinal()
- && matchers[i] instanceof DayOfMonthMatcher
- && ((DayOfMonthMatcher) matchers[i]).isLastDay(values[i],values[i+1],DateUtil.isLeapYear(values[Part.YEAR.ordinal()]))) {
- int newMonth = newValues[Part.MONTH.ordinal()];
- int newYear = newValues[Part.YEAR.ordinal()];
- nextValue = getLastDay(newMonth, newYear);
- } else {
- nextValue = matchers[i].nextAfter(values[i] + 1);
}
+
+ nextValue = getNextMatch(values, i, 1);
+
if (nextValue > values[i]) {
- newValues[i] = nextValue;
+ values[i] = nextValue;
i--;
break;
}
@@ -243,8 +226,32 @@ public class PatternMatcher {
}
// 修改值以下的字段全部归最小值
- setToMin(newValues, i);
- return newValues;
+ setToMin(values, i);
+ return values;
+ }
+
+ /**
+ * 获取指定部分的下一个匹配值,三种结果:
+ *
+ * - 结果值大于原值:此部分已更新,后续部分取匹配的最小值。
+ * - 结果值小于原值:此部分获取到了最小值,上一个部分需要继续取下一个值。
+ * - 结果值等于原值:此部分匹配,获取下一个部分的next值
+ *
+ *
+ * @param newValues 时间字段值,{second, minute, hour, dayOfMonth, monthBase1, dayOfWeekBase0, year}
+ * @param partOrdinal 序号
+ * @param plusValue 获取的偏移值
+ * @return 下一个值
+ */
+ private int getNextMatch(final int[] newValues, final int partOrdinal, final int plusValue) {
+ if (partOrdinal == Part.DAY_OF_MONTH.ordinal() && matchers[partOrdinal] instanceof DayOfMonthMatcher) {
+ // 对于日需要考虑月份和闰年,单独处理
+ final boolean isLeapYear = DateUtil.isLeapYear(newValues[Part.YEAR.ordinal()]);
+ final int month = newValues[Part.MONTH.ordinal()];
+ return ((DayOfMonthMatcher) matchers[partOrdinal]).nextAfter(newValues[partOrdinal] + plusValue, month, isLeapYear);
+ }
+
+ return matchers[partOrdinal].nextAfter(newValues[partOrdinal] + plusValue);
}
/**
@@ -253,19 +260,21 @@ public class PatternMatcher {
* @param values 值数组
* @param toPart 截止的部分
*/
- private void setToMin(int[] values, int toPart) {
+ private void setToMin(final int[] values, final int toPart) {
Part part;
- for (int i = 0; i <= toPart; i++) {
+ for (int i = toPart; i >= 0; i--) {
part = Part.of(i);
- 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] = getLastDay(newMonth, newYear);
- } else {
- values[i] = getMin(part);
+ if (part == Part.DAY_OF_MONTH) {
+ final boolean isLeapYear = DateUtil.isLeapYear(values[Part.YEAR.ordinal()]);
+ final int month = values[Part.MONTH.ordinal()];
+ final PartMatcher partMatcher = get(part);
+ if (partMatcher instanceof DayOfMonthMatcher) {
+ values[i] = ((DayOfMonthMatcher) partMatcher).getMinValue(month, isLeapYear);
+ continue;
+ }
}
+
+ values[i] = getMin(part);
}
}
diff --git a/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternNextMatchTest.java b/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternNextMatchTest.java
index 04b0b985e..643e9d187 100644
--- a/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternNextMatchTest.java
+++ b/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternNextMatchTest.java
@@ -3,11 +3,14 @@ package cn.hutool.cron.pattern;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
-import static org.junit.jupiter.api.Assertions.*;
+import cn.hutool.core.lang.Console;
import org.junit.jupiter.api.Test;
import java.util.Calendar;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
public class CronPatternNextMatchTest {
@Test
@@ -182,6 +185,16 @@ public class CronPatternNextMatchTest {
}
}
+ @Test
+ public void testLastDayOfMonthForEveryYear3() {
+ 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 ?");
+ Calendar calendar = pattern.nextMatchAfter(date.toCalendar());
+ Console.log(DateUtil.date(calendar));
+ }
+
@Test
public void testEveryHour() {
DateTime date = DateUtil.parse("2022-02-28 07:44:16");
diff --git a/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternUtilTest.java b/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternUtilTest.java
index 9921e7500..e08bb9564 100644
--- a/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternUtilTest.java
+++ b/hutool-cron/src/test/java/cn/hutool/cron/pattern/CronPatternUtilTest.java
@@ -1,12 +1,15 @@
package cn.hutool.cron.pattern;
+import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
-import static org.junit.jupiter.api.Assertions.*;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.Date;
import java.util.List;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
public class CronPatternUtilTest {
@Test
@@ -44,4 +47,37 @@ public class CronPatternUtilTest {
assertEquals("2018-10-31 03:00:00", matchedDates.get(3).toString());
assertEquals("2018-10-31 04:00:00", matchedDates.get(4).toString());
}
+
+ @Test
+ public void issue4056Test() {
+ // "*/5"和"1/5"意义相同,从1号开始,每5天一个匹配,则匹配的天为:
+ // 2025-02-01, 2025-02-06, 2025-02-11, 2025-02-16, 2025-02-21, 2025-02-26
+ // 2025-03-01, 2025-03-06, 2025-03-11, 2025-03-16, 2025-03-21, 2025-03-26, 2025-03-31
+ final String cron = "0 0 0 */5 * ? *";
+ final CronPattern cronPattern = new CronPattern(cron);
+
+ // 2025-02-28不应该在匹配之列
+ boolean match = cronPattern.match(DateUtil.parse("2025-02-28 00:00:00").toCalendar(), true);
+ Assertions.assertFalse( match);
+
+ match = cronPattern.match(DateUtil.parse("2025-03-01 00:00:00").toCalendar(), true);
+ Assertions.assertTrue( match);
+
+ match = cronPattern.match(DateUtil.parse("2025-03-31 00:00:00").toCalendar(), true);
+ Assertions.assertTrue( match);
+ }
+
+ @Test
+ public void issue4056Test2() {
+ final String cron = "0 0 0 */5 * ? *";
+ final CronPattern cronPattern = new CronPattern(cron);
+
+ final DateTime judgeTime = DateUtil.parse("2025-02-27 23:59:59");
+ final Date nextDate = CronPatternUtil.nextDateAfter(cronPattern, judgeTime);
+ // "*/5"和"1/5"意义相同,从1号开始,每5天一个匹配,则匹配的天为:
+ // 2025-02-01, 2025-02-06, 2025-02-11, 2025-02-16, 2025-02-21, 2025-02-26
+ // 2025-03-01, 2025-03-06, 2025-03-11, 2025-03-16, 2025-03-21, 2025-03-26, 2025-03-31
+ // 下一个匹配日期应为2025-03-01
+ Assertions.assertEquals("2025-03-01 00:00:00", nextDate.toString());
+ }
}
diff --git a/hutool-cron/src/test/java/cn/hutool/cron/pattern/Issue4056Test.java b/hutool-cron/src/test/java/cn/hutool/cron/pattern/Issue4056Test.java
new file mode 100644
index 000000000..e0b95ae30
--- /dev/null
+++ b/hutool-cron/src/test/java/cn/hutool/cron/pattern/Issue4056Test.java
@@ -0,0 +1,140 @@
+package cn.hutool.cron.pattern;
+
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUtil;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.quartz.CronExpression;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Date;
+
+public class Issue4056Test {
+
+ /**
+ * 见:https://github.com/quartz-scheduler/quartz/issues/1298
+ * Quartz-2.5.0这块有bug,只能使用2.4.0测认
+ *
+ * @throws ParseException 解析错误
+ */
+ @Test
+ void testCronAll() throws ParseException {
+ final ArrayList cronsList = new ArrayList<>();
+ final ArrayList judgeTimes = new ArrayList<>();
+
+ // 1. Cron 表达式(40个)
+ cronsList.add("0 0 0 * * ? *"); // 每天00:00
+ cronsList.add("0 0 12 * * ? *"); // 每天中午12:00
+ cronsList.add("0 0 18 * * ? *"); // 每天傍晚18:00
+ cronsList.add("0 0 6,12,18 * * ? *"); // 每天6点、12点、18点
+ cronsList.add("0 0 */6 * * ? *"); // 每6小时
+ cronsList.add("0 30 */8 * * ? *"); // 每8小时的30分
+ cronsList.add("0 */15 * * * ? *"); // 每15分钟
+ cronsList.add("0 */5 9-17 * * ? *"); // 工作时间内每5分钟
+ cronsList.add("0 0 0-23/2 * * ? *"); // 每2小时
+ cronsList.add("0 0 0 */8 * ? *"); // 每8天的00:00
+ cronsList.add("0 0 12 15 * ? *"); // 每月15日12:00
+ cronsList.add("0 0 0 L * ? *"); // 每月最后一天00:00
+ cronsList.add("0 0 0 29 2 ? *"); // 2月29日00:00(闰年)
+ cronsList.add("0 0 0 1 1 ? *"); // 每年1月1日00:00
+ cronsList.add("0 0/30 * * * ? *"); // 每小时0分和30分
+ cronsList.add("0 0 */4 * * ? *"); // 每4小时
+ cronsList.add("0 0 0 1/3 * ? *"); // 每3天00:00
+ cronsList.add("0 0 2 28-31 * ? *"); // 每月最后几天2:00
+ cronsList.add("0 0 0 1,15 * ? *"); // 每月1日和15日00:00
+ cronsList.add("0 0 0 1/5 * ? *"); // 每5天00:00
+ cronsList.add("0 0 0 1/10 * ? *"); // 每10天00:00
+ cronsList.add("0 0 0 1 */3 ? *"); // 每3个月的第1天00:00
+ cronsList.add("0 0 0 25 12 ? *"); // 圣诞节00:00
+ cronsList.add("0 0 12 31 12 ? *"); // 新年前夜12:00
+ cronsList.add("0 0 0 14 2 ? *"); // 情人节00:00
+ cronsList.add("0 0 10 1 5 ? *"); // 劳动节10:00
+ cronsList.add("0 0 9 8 3 ? *"); // 妇女节09:00
+ cronsList.add("0 0 0 1 4 ? *"); // 愚人节00:00
+ cronsList.add("0 0 12 4 7 ? *"); // 美国独立日12:00
+ cronsList.add("0 0 0 31 10 ? *"); // 万圣节00:00
+ cronsList.add("0 7,19,31,43,55 * * * ? *"); // 特定分钟
+ cronsList.add("0 */7 * * * ? *"); // 每7分钟
+ cronsList.add("0 15-45/5 * * * ? *"); // 每小时的15-45分之间每5分钟
+ cronsList.add("0 0-30/2 * * * ? *"); // 每小时前30分钟每2分钟
+ cronsList.add("0 45 23 * * ? *"); // 每天23:45
+ cronsList.add("0 59 23 * * ? *"); // 每天23:59
+ cronsList.add("0 0 */3 * * ? *"); // 每3小时
+ cronsList.add("0 0 9-18/2 * * ? *"); // 9点到18点每2小时
+ cronsList.add("0 0 22-2 * * ? *"); // 22点到次日2点每小时
+ cronsList.add("0 30 16 L * ? *"); // 每月最后一天16:30
+
+
+ // 2. 测试时间 (50个)
+ judgeTimes.add(DateUtil.parse("2025-02-01 18:20:10"));
+ judgeTimes.add(DateUtil.parse("2024-02-29 10:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-12-31 23:59:59"));
+ judgeTimes.add(DateUtil.parse("2025-01-01 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-06-15 12:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-03-30 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-02-28 23:59:59"));
+ judgeTimes.add(DateUtil.parse("2025-03-01 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-01-31 23:59:59"));
+ judgeTimes.add(DateUtil.parse("2025-04-30 23:59:59"));
+ judgeTimes.add(DateUtil.parse("2025-06-30 23:59:59"));
+ judgeTimes.add(DateUtil.parse("2025-09-30 23:59:59"));
+ judgeTimes.add(DateUtil.parse("2026-01-01 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2024-02-28 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2024-02-29 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2024-02-29 23:59:59"));
+ judgeTimes.add(DateUtil.parse("2023-02-28 23:59:59"));
+ judgeTimes.add(DateUtil.parse("2028-02-29 12:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-06-15 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-06-15 23:59:59"));
+ judgeTimes.add(DateUtil.parse("2025-03-31 23:59:59"));
+ judgeTimes.add(DateUtil.parse("2025-04-01 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-07-01 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-10-01 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-01-06 09:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-01-10 17:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-01-11 12:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-01-12 12:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-03-09 01:59:59"));
+ judgeTimes.add(DateUtil.parse("2025-03-09 03:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-11-02 01:59:59"));
+ judgeTimes.add(DateUtil.parse("2025-11-02 01:00:00"));
+ judgeTimes.add(DateUtil.parse("2024-12-31 23:59:59"));
+ judgeTimes.add(DateUtil.parse("2024-01-01 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2026-12-31 23:59:59"));
+ judgeTimes.add(DateUtil.parse("2026-01-01 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-05-15 08:45:30"));
+ judgeTimes.add(DateUtil.parse("2025-08-22 14:20:15"));
+ judgeTimes.add(DateUtil.parse("2025-11-03 19:10:45"));
+ judgeTimes.add(DateUtil.parse("2025-02-14 09:30:00"));
+ judgeTimes.add(DateUtil.parse("2025-07-07 07:07:07"));
+ judgeTimes.add(DateUtil.parse("2025-09-09 09:09:09"));
+ judgeTimes.add(DateUtil.parse("2025-10-10 10:10:10"));
+ judgeTimes.add(DateUtil.parse("2025-12-12 12:12:12"));
+ judgeTimes.add(DateUtil.parse("2025-03-03 03:03:03"));
+ judgeTimes.add(DateUtil.parse("2025-06-06 06:06:06"));
+ judgeTimes.add(DateUtil.parse("2025-04-16 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-04-30 23:59:59"));
+ judgeTimes.add(DateUtil.parse("2025-05-01 00:00:00"));
+ judgeTimes.add(DateUtil.parse("2025-05-01 00:00:01"));
+
+ // 3. 计算并比对结果
+ for (final String cron : cronsList) {
+ final CronPattern hutoolCorn = new CronPattern(cron);
+ final CronExpression quartzCorn = new CronExpression(cron);
+ for (final DateTime judgeTime : judgeTimes) {
+ final Date quartzDate = quartzCorn.getNextValidTimeAfter(judgeTime);
+ final Date hutoolDate = CronPatternUtil.nextDateAfter(hutoolCorn, judgeTime);
+ Assertions.assertEquals(quartzDate, hutoolDate);
+ }
+ }
+ }
+
+ @Test
+ void issue4056Test() {
+ final String cron = "0 0 0 1/3 * ? *";
+ final CronPattern hutoolCorn = new CronPattern(cron);
+ final Date hutoolDate = CronPatternUtil.nextDateAfter(hutoolCorn, DateUtil.parse("2025-02-28 00:00:00"));
+ System.out.println(DateUtil.formatDateTime(hutoolDate));
+ }
+}