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 {
}
/**
- * 给定的日期是否匹配当前匹配器
+ * 给定的日是否匹配当前匹配器,匹配分为两种情况:
+ *
@@ -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