diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/date/CalendarUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/date/CalendarUtil.java index eb4c19e2f..9ff00adaa 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/date/CalendarUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/date/CalendarUtil.java @@ -797,6 +797,28 @@ public class CalendarUtil { } // endregion + /** + * 是否为本月第一天 + * + * @param calendar {@link Calendar} + * @return 是否为本月最后一天 + * @since 6.0.0 + */ + public static boolean isFirstDayOfMonth(final Calendar calendar) { + return 1 == calendar.get(Calendar.DAY_OF_MONTH); + } + + /** + * 是否为本月最后一天 + * + * @param calendar {@link Calendar} + * @return 是否为本月最后一天 + * @since 5.8.27 + */ + public static boolean isLastDayOfMonth(final Calendar calendar) { + return calendar.get(Calendar.DAY_OF_MONTH) == calendar.getActualMaximum(Calendar.DAY_OF_MONTH); + } + /** * 计算相对于dateToCompare的年龄(周岁),常用于计算指定生日在某年的年龄
* 按照《最高人民法院关于审理未成年人刑事案件具体应用法律若干问题的解释》第二条规定刑法第十七条规定的“周岁”,按照公历的年、月、日计算,从周岁生日的第二天起算。 @@ -844,26 +866,4 @@ public class CalendarUtil { return age; } - - /** - * 是否为本月第一天 - * - * @param calendar {@link Calendar} - * @return 是否为本月最后一天 - * @since 6.0.0 - */ - public static boolean isFirstDayOfMonth(final Calendar calendar) { - return 1 == calendar.get(Calendar.DAY_OF_MONTH); - } - - /** - * 是否为本月最后一天 - * - * @param calendar {@link Calendar} - * @return 是否为本月最后一天 - * @since 5.8.27 - */ - public static boolean isLastDayOfMonth(final Calendar calendar) { - return calendar.get(Calendar.DAY_OF_MONTH) == calendar.getActualMaximum(Calendar.DAY_OF_MONTH); - } } diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/date/DateTime.java b/hutool-core/src/main/java/cn/hutool/v7/core/date/DateTime.java index 4ba00f7df..64d2c11ce 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/date/DateTime.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/date/DateTime.java @@ -25,6 +25,7 @@ import cn.hutool.v7.core.lang.Assert; import cn.hutool.v7.core.text.StrUtil; import cn.hutool.v7.core.util.ObjUtil; +import java.io.Serial; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.Instant; @@ -51,6 +52,7 @@ import java.util.TimeZone; * @author Looly */ public class DateTime extends Date { + @Serial private static final long serialVersionUID = -5395712593979185936L; private static boolean useJdkToStringStyle = false; diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/date/DateUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/date/DateUtil.java index d1ad061e6..16d10b9f8 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/date/DateUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/date/DateUtil.java @@ -1985,22 +1985,15 @@ public class DateUtil { * @since 5.7.16 */ public static String getShortName(final TimeUnit unit) { - switch (unit) { - case NANOSECONDS: - return "ns"; - case MICROSECONDS: - return "μs"; - case MILLISECONDS: - return "ms"; - case SECONDS: - return "s"; - case MINUTES: - return "min"; - case HOURS: - return "h"; - default: - return unit.name().toLowerCase(); - } + return switch (unit) { + case NANOSECONDS -> "ns"; + case MICROSECONDS -> "μs"; + case MILLISECONDS -> "ms"; + case SECONDS -> "s"; + case MINUTES -> "min"; + case HOURS -> "h"; + default -> unit.name().toLowerCase(); + }; } /** diff --git a/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/CronPattern.java b/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/CronPattern.java index 484618813..846365401 100644 --- a/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/CronPattern.java +++ b/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/CronPattern.java @@ -18,6 +18,7 @@ package cn.hutool.v7.cron.pattern; import cn.hutool.v7.core.comparator.CompareUtil; import cn.hutool.v7.core.date.CalendarUtil; +import cn.hutool.v7.core.date.DateUtil; import cn.hutool.v7.core.lang.Console; import cn.hutool.v7.cron.pattern.matcher.PatternMatcher; import cn.hutool.v7.cron.pattern.parser.PatternParser; diff --git a/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/Part.java b/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/Part.java index 8aaff1722..79fa03d37 100644 --- a/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/Part.java +++ b/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/Part.java @@ -49,9 +49,9 @@ public enum Part { */ HOUR(Calendar.HOUR_OF_DAY, 0, 23), /** - * 日[1-31] + * 日[1-31],32表示最后一天 */ - DAY_OF_MONTH(Calendar.DAY_OF_MONTH, 1, 31), + DAY_OF_MONTH(Calendar.DAY_OF_MONTH, 1, 32), /** * 月[1-12] */ diff --git a/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/matcher/BoolArrayMatcher.java b/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/matcher/BoolArrayMatcher.java index 930f4e41b..517d5614a 100644 --- a/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/matcher/BoolArrayMatcher.java +++ b/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/matcher/BoolArrayMatcher.java @@ -33,7 +33,12 @@ public class BoolArrayMatcher implements PartMatcher { /** * 用户定义此字段的最小值 */ - protected final int minValue; + private final int minValue; + /** + * 用户定义此字段的最大值 + */ + private final int maxValue; + /** * 匹配值列表 */ @@ -48,29 +53,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 (final 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 test(final Integer value) { - final boolean[] bValues = this.bValues; - 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) { + final int maxValue = this.maxValue; + if(value == maxValue){ + return value; + } final int minValue = this.minValue; - if(value > minValue){ + if(value > minValue && value < maxValue){ final boolean[] bValues = this.bValues; - while(value < bValues.length){ - if(bValues[value]){ + // 最大值永远小于数组长度,只需判断最大值边界 + while(value <= maxValue){ + if(value == maxValue || bValues[value]){ + // 达到最大值或达到第一个匹配值 return value; } value++; @@ -92,6 +105,15 @@ public class BoolArrayMatcher implements PartMatcher { return this.minValue; } + /** + * 获取表达式定义的最大值 + * + * @return 最大值 + */ + 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/v7/cron/pattern/matcher/DayOfMonthMatcher.java b/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/matcher/DayOfMonthMatcher.java index 6a0b33fab..99cc98638 100644 --- a/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/matcher/DayOfMonthMatcher.java +++ b/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/matcher/DayOfMonthMatcher.java @@ -28,6 +28,11 @@ import java.util.List; */ public class DayOfMonthMatcher extends BoolArrayMatcher { + /** + * 最后一天 + */ + private static final int LAST_DAY = 32; + /** * 构造 * @@ -38,45 +43,50 @@ public class DayOfMonthMatcher extends BoolArrayMatcher { } /** - * 给定的日期是否匹配当前匹配器 + * 给定的日是否匹配当前匹配器,匹配分为两种情况: + * * - * @param value 被检查的值,此处为日,从1开始 + * @param dayValue 被检查的值,此处为日,从1开始 * @param month 实际的月份,从1开始 * @param isLeapYear 是否闰年 * @return 是否匹配 */ - public boolean match(final int value, final int month, final boolean isLeapYear) { - return (super.test(value) // 在约定日范围内的某一天 - //匹配器中用户定义了最后一天(31表示最后一天) - || matchLastDay(value, getLastDay(month, isLeapYear))); + public boolean match(final int dayValue, final int month, final boolean isLeapYear) { + return (super.test(dayValue) // 在约定日范围内的某一天 + //匹配器中用户定义了最后一天(32表示最后一天) + || matchLastDay(dayValue, month, isLeapYear)); } /** - * 获取指定值之后的匹配值,也可以是指定值本身
+ * 获取指定日之后的匹配值,也可以是其本身
* 如果表达式中存在最后一天(如使用"L"),则: * * - * @param value 指定的值 + * @param dayValue 指定的天值 * @param month 月份,从1开始 * @param isLeapYear 是否为闰年 * @return 匹配到的值或之后的值 */ - public int nextAfter(int value, final int month, final boolean 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 (value > minValue) { + if (dayValue > minValue) { final boolean[] bValues = this.bValues; - while (value < bValues.length) { - if (bValues[value]) { - if(31 == value){ - // value == lastDay - return getLastDay(month, isLeapYear); - } - return value; + // 最大值永远小于数组长度,只需判断最大值边界 + while (dayValue <= maxValue) { + // 匹配到有效值 + if (bValues[dayValue] || + // 如果最大值不在有效值中,这个最大值表示最后一天,则在包含了最后一天的情况下返回最后一天 + (dayValue == maxValue && test(LAST_DAY))) { + return dayValue; } - value++; + dayValue++; } } @@ -87,7 +97,7 @@ public class DayOfMonthMatcher extends BoolArrayMatcher { } /** - * 获取匹配的最小值 + * 获取表达式定义中指定月的最小日的值 * * @param month 月,base1 * @param isLeapYear 是否闰年 @@ -95,13 +105,26 @@ public class DayOfMonthMatcher extends BoolArrayMatcher { */ public int getMinValue(final int month, final boolean isLeapYear) { final int minValue = super.getMinValue(); - if (31 == minValue) { + if (LAST_DAY == minValue) { // 用户指定了 L 等表示最后一天 return getLastDay(month, isLeapYear); } return minValue; } + /** + * 获取表达式定义中指定月的最大日的值
+ * 首先获取表达式定义的最大值,如果这个值大于本月最后一天,则返回最后一天,否则返回用户定义的最大值
+ * 注意最后一天可能不是表达式中定义的有效值 + * + * @param month 月,base1 + * @param isLeapYear 是否闰年 + * @return 匹配的最大值 + */ + public int getMaxValue(final int month, final boolean isLeapYear) { + return Math.min(super.getMaxValue(), getLastDay(month, isLeapYear)); + } + /** * 是否匹配本月最后一天,规则如下: *
@@ -110,12 +133,17 @@ public class DayOfMonthMatcher extends BoolArrayMatcher {
 	 * 3、表达式包含最后一天(使用31表示)
 	 * 
* - * @param value 被检查的值 - * @param lastDay 月份的最后一天 + * @param value 被检查的值 + * @param month 月,base1 + * @param isLeapYear 是否闰年 * @return 是否为本月最后一天 */ - private boolean matchLastDay(final int value, final int lastDay) { - return value > 27 && test(31) && value == lastDay; + private boolean matchLastDay(final int value, final int month, final boolean isLeapYear) { + return value > 27 + // 表达式中定义包含了最后一天 + && test(LAST_DAY) + // 用户指定的日正好是最后一天 + && value == getLastDay(month, isLeapYear); } /** diff --git a/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/matcher/PatternMatcher.java b/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/matcher/PatternMatcher.java index e4e84f977..ccaa65653 100644 --- a/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/matcher/PatternMatcher.java +++ b/hutool-cron/src/main/java/cn/hutool/v7/cron/pattern/matcher/PatternMatcher.java @@ -178,14 +178,14 @@ public class PatternMatcher { @Override public String toString() { return StrUtil.format(""" - SECOND : {} - MINUTE : {} - HOUR : {} - DAY_OF_MONTH: {} - MONTH : {} - DAY_OF_WEEK : {} - YEAR : {} - """, (Object[]) this.matchers); + SECOND : {} + MINUTE : {} + HOUR : {} + DAY_OF_MONTH: {} + MONTH : {} + DAY_OF_WEEK : {} + YEAR : {} + """, (Object[]) this.matchers); } /** @@ -269,10 +269,12 @@ public class PatternMatcher { * * @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); diff --git a/hutool-cron/src/test/java/cn/hutool/v7/cron/pattern/CronPatternUtilTest.java b/hutool-cron/src/test/java/cn/hutool/v7/cron/pattern/CronPatternUtilTest.java index b5b7594df..d89774024 100644 --- a/hutool-cron/src/test/java/cn/hutool/v7/cron/pattern/CronPatternUtilTest.java +++ b/hutool-cron/src/test/java/cn/hutool/v7/cron/pattern/CronPatternUtilTest.java @@ -66,11 +66,19 @@ public class CronPatternUtilTest { 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-02-28不应该在匹配之列 + // 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); - final boolean match = cronPattern.match(DateUtil.parse("2025-02-28 00:00:00").toCalendar(), true); + + // 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 @@ -82,6 +90,7 @@ public class CronPatternUtilTest { 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/v7/cron/pattern/Issue4056Test.java b/hutool-cron/src/test/java/cn/hutool/v7/cron/pattern/Issue4056Test.java new file mode 100644 index 000000000..ca2df4f73 --- /dev/null +++ b/hutool-cron/src/test/java/cn/hutool/v7/cron/pattern/Issue4056Test.java @@ -0,0 +1,132 @@ +package cn.hutool.v7.cron.pattern; + +import cn.hutool.v7.core.date.DateTime; +import cn.hutool.v7.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); + } + } + } +}