fix cron bug

This commit is contained in:
Looly 2025-09-06 00:58:58 +08:00
parent b6e7287287
commit 5e966fdfc6
10 changed files with 271 additions and 82 deletions

View File

@ -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的年龄周岁常用于计算指定生日在某年的年龄<br>
* 按照最高人民法院关于审理未成年人刑事案件具体应用法律若干问题的解释第二条规定刑法第十七条规定的周岁按照公历的年日计算从周岁生日的第二天起算
@ -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);
}
}

View File

@ -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;

View File

@ -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();
};
}
/**

View File

@ -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;

View File

@ -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]
*/

View File

@ -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});

View File

@ -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 {
}
/**
* 给定的日期是否匹配当前匹配器
* 给定的日是否匹配当前匹配器匹配分为两种情况
* <ul>
* <li>如果指定日在表达式中有定义则直接返回</li>
* <li>如果指定日不在表达式中但是表达式包含最后一天匹配当月最后一天</li>
* </ul>
*
* @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));
}
/**
* 获取指定值之后的匹配值也可以是指定值本身<br>
* 获取指定日之后的匹配值也可以是其本身<br>
* 如果表达式中存在最后一天如使用"L"
* <ul>
* <li>4月6月9月11月最多匹配到30日</li>
* <li>4月闰年匹配到29日非闰年28日</li>
* </ul>
*
* @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;
}
/**
* 获取表达式定义中指定月的最大日的值<br>
* 首先获取表达式定义的最大值如果这个值大于本月最后一天则返回最后一天否则返回用户定义的最大值<br>
* 注意最后一天可能不是表达式中定义的有效值
*
* @param month base1
* @param isLeapYear 是否闰年
* @return 匹配的最大值
*/
public int getMaxValue(final int month, final boolean isLeapYear) {
return Math.min(super.getMaxValue(), getLastDay(month, isLeapYear));
}
/**
* 是否匹配本月最后一天规则如下
* <pre>
@ -110,12 +133,17 @@ public class DayOfMonthMatcher extends BoolArrayMatcher {
* 3表达式包含最后一天使用31表示
* </pre>
*
* @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);
}
/**

View File

@ -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);

View File

@ -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());
}

View File

@ -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<String> cronsList = new ArrayList<>();
final ArrayList<DateTime> 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);
}
}
}
}