mirror of
https://gitee.com/dromara/hutool.git
synced 2025-11-24 16:43:24 +08:00
修复CronPatternUtil.nextDateAfter当日为L时计算错误问题。(issue#4056@Github)
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
# 🚀Changelog
|
# 🚀Changelog
|
||||||
|
|
||||||
-------------------------------------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------------------------------------
|
||||||
# 5.8.41(2025-09-04)
|
# 5.8.41(2025-09-06)
|
||||||
|
|
||||||
### 🐣新特性
|
### 🐣新特性
|
||||||
* 【core 】 增加`WeakKeyValueConcurrentMap`及其关联类,同时废弃`WeakConcurrentMap`并替换(issue#4039@Github)
|
* 【core 】 增加`WeakKeyValueConcurrentMap`及其关联类,同时废弃`WeakConcurrentMap`并替换(issue#4039@Github)
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
* 【db 】 修复`Condition`的`Condition("discount_end_time", "!=", (String) null)`方法生成SQL时,生成SQL不符合预期要求的错误(pr#4042@Github)
|
* 【db 】 修复`Condition`的`Condition("discount_end_time", "!=", (String) null)`方法生成SQL时,生成SQL不符合预期要求的错误(pr#4042@Github)
|
||||||
* 【core 】 修复`IoUtil`的`closeIfPosible`拼写错误,新建一个`closeIfPossible`方法,原方法标记deprecated(issue#4047@Github)
|
* 【core 】 修复`IoUtil`的`closeIfPosible`拼写错误,新建一个`closeIfPossible`方法,原方法标记deprecated(issue#4047@Github)
|
||||||
* 【http 】 修复`HttpRequest.sendRedirectIfPossible`未对308做判断问题。(issue#4053@Github)
|
* 【http 】 修复`HttpRequest.sendRedirectIfPossible`未对308做判断问题。(issue#4053@Github)
|
||||||
|
* 【cron 】 修复`CronPatternUtil.nextDateAfter`当日为L时计算错误问题。(issue#4056@Github)
|
||||||
|
|
||||||
-------------------------------------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------------------------------------
|
||||||
# 5.8.40(2025-08-26)
|
# 5.8.40(2025-08-26)
|
||||||
|
|||||||
@@ -36,6 +36,12 @@
|
|||||||
<artifactId>hutool-setting</artifactId>
|
<artifactId>hutool-setting</artifactId>
|
||||||
<version>${project.parent.version}</version>
|
<version>${project.parent.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.quartz-scheduler</groupId>
|
||||||
|
<artifactId>quartz</artifactId>
|
||||||
|
<version>2.4.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ import cn.hutool.cron.pattern.matcher.PatternMatcher;
|
|||||||
import cn.hutool.cron.pattern.parser.PatternParser;
|
import cn.hutool.cron.pattern.parser.PatternParser;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.*;
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.GregorianCalendar;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定时任务表达式<br>
|
* 定时任务表达式<br>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public enum Part {
|
|||||||
SECOND(Calendar.SECOND, 0, 59),
|
SECOND(Calendar.SECOND, 0, 59),
|
||||||
MINUTE(Calendar.MINUTE, 0, 59),
|
MINUTE(Calendar.MINUTE, 0, 59),
|
||||||
HOUR(Calendar.HOUR_OF_DAY, 0, 23),
|
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()),
|
MONTH(Calendar.MONTH, Month.JANUARY.getValueBaseOne(), Month.DECEMBER.getValueBaseOne()),
|
||||||
DAY_OF_WEEK(Calendar.DAY_OF_WEEK, Week.SUNDAY.ordinal(), Week.SATURDAY.ordinal()),
|
DAY_OF_WEEK(Calendar.DAY_OF_WEEK, Week.SUNDAY.ordinal(), Week.SATURDAY.ordinal()),
|
||||||
YEAR(Calendar.YEAR, 1970, 2099);
|
YEAR(Calendar.YEAR, 1970, 2099);
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ public class BoolArrayMatcher implements PartMatcher {
|
|||||||
* 用户定义此字段的最小值
|
* 用户定义此字段的最小值
|
||||||
*/
|
*/
|
||||||
private final int minValue;
|
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!");
|
Assert.isTrue(CollUtil.isNotEmpty(intValueList), "Values must be not empty!");
|
||||||
bValues = new boolean[Collections.max(intValueList) + 1];
|
bValues = new boolean[Collections.max(intValueList) + 1];
|
||||||
int min = Integer.MAX_VALUE;
|
int min = Integer.MAX_VALUE;
|
||||||
|
int max = 0;
|
||||||
for (Integer value : intValueList) {
|
for (Integer value : intValueList) {
|
||||||
min = Math.min(min, value);
|
min = Math.min(min, value);
|
||||||
|
max = Math.max(max, value);
|
||||||
bValues[value] = true;
|
bValues[value] = true;
|
||||||
}
|
}
|
||||||
this.minValue = min;
|
this.minValue = min;
|
||||||
|
this.maxValue = max;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean match(Integer value) {
|
public boolean match(Integer value) {
|
||||||
if (null == value || value >= bValues.length) {
|
if(null != value && value >= minValue && value <= maxValue){
|
||||||
return false;
|
return bValues[value];
|
||||||
}
|
}
|
||||||
return bValues[value];
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int nextAfter(int value) {
|
public int nextAfter(int value) {
|
||||||
if(value > minValue){
|
final int maxValue = this.maxValue;
|
||||||
while(value < bValues.length){
|
if(value == maxValue){
|
||||||
if(bValues[value]){
|
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;
|
return value;
|
||||||
}
|
}
|
||||||
value++;
|
value++;
|
||||||
@@ -70,6 +86,16 @@ public class BoolArrayMatcher implements PartMatcher {
|
|||||||
return this.minValue;
|
return this.minValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表达式定义的最大值
|
||||||
|
*
|
||||||
|
* @return 最大值
|
||||||
|
* @since 5.8.41
|
||||||
|
*/
|
||||||
|
public int getMaxValue() {
|
||||||
|
return this.maxValue;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return StrUtil.format("Matcher:{}", new Object[]{this.bValues});
|
return StrUtil.format("Matcher:{}", new Object[]{this.bValues});
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ import java.util.List;
|
|||||||
* @author Looly
|
* @author Looly
|
||||||
*/
|
*/
|
||||||
public class DayOfMonthMatcher extends BoolArrayMatcher {
|
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 month 实际的月份,从1开始
|
||||||
* @param isLeapYear 是否闰年
|
* @param isLeapYear 是否闰年
|
||||||
* @return 是否匹配
|
* @return 是否匹配
|
||||||
*/
|
*/
|
||||||
public boolean match(int value, int month, boolean isLeapYear) {
|
public boolean match(int dayValue, int month, boolean isLeapYear) {
|
||||||
return (super.match(value) // 在约定日范围内的某一天
|
return (super.match(dayValue) // 在约定日范围内的某一天
|
||||||
//匹配器中用户定义了最后一天(31表示最后一天)
|
//匹配器中用户定义了最后一天(32表示最后一天)
|
||||||
|| (value > 27 && match(31) && isLastDayOfMonth(value, month, isLeapYear)));
|
|| matchLastDay(dayValue, month, isLeapYear));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否为本月最后一天,规则如下:
|
* 获取指定日之后的匹配值,也可以是其本身<br>
|
||||||
* <pre>
|
* 如果表达式中存在最后一天(如使用"L"),则:
|
||||||
* 1、闰年2月匹配是否为29
|
* <ul>
|
||||||
* 2、其它月份是否匹配最后一天的日期(可能为30或者31)
|
* <li>4月、6月、9月、11月最多匹配到30日</li>
|
||||||
* </pre>
|
* <li>4月闰年匹配到29日,非闰年28日</li>
|
||||||
|
* </ul>
|
||||||
*
|
*
|
||||||
* @param value 被检查的值
|
* @param dayValue 指定的天值
|
||||||
* @param month 月份,从1开始
|
* @param month 月份,从1开始
|
||||||
* @param isLeapYear 是否闰年
|
* @param isLeapYear 是否为闰年
|
||||||
* @return 是否为本月最后一天
|
* @return 匹配到的值或之后的值
|
||||||
|
* @since 5.8.41
|
||||||
*/
|
*/
|
||||||
private static boolean isLastDayOfMonth(int value, int month, boolean isLeapYear) {
|
public int nextAfter(int dayValue, final int month, final boolean isLeapYear) {
|
||||||
return value == Month.getLastDay(month - 1, 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() {
|
public boolean isLast() {
|
||||||
return match(31);
|
return match(32);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查value是这个月的最后一天
|
* 检查value是这个月的最后一天
|
||||||
|
*
|
||||||
* @param value 被检查的值
|
* @param value 被检查的值
|
||||||
* @return
|
* @param month 月份,从1开始
|
||||||
|
* @param isLeapYear 是否闰年
|
||||||
|
* @return 是否是这个月的最后
|
||||||
*/
|
*/
|
||||||
public boolean isLastDay(Integer value,Integer month, boolean isLeapYear) {
|
public boolean isLastDay(Integer value, Integer month, boolean isLeapYear) {
|
||||||
if(isLastDayOfMonth(value, month, isLeapYear)) {
|
return matchLastDay(value, month, isLeapYear);
|
||||||
return match(31);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表达式定义中指定月的最小日的值
|
||||||
|
*
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取表达式定义中指定月的最大日的值<br>
|
||||||
|
* 首先获取表达式定义的最大值,如果这个值大于本月最后一天,则返回最后一天,否则返回用户定义的最大值<br>
|
||||||
|
* 注意最后一天可能不是表达式中定义的有效值
|
||||||
|
*
|
||||||
|
* @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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否匹配本月最后一天,规则如下:
|
||||||
|
* <pre>
|
||||||
|
* 1、闰年2月匹配是否为29
|
||||||
|
* 2、其它月份是否匹配最后一天的日期(可能为30或者31)
|
||||||
|
* 3、表达式包含最后一天(使用31表示)
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,8 +177,6 @@ public class PatternMatcher {
|
|||||||
* @return {@link Calendar},毫秒数为0
|
* @return {@link Calendar},毫秒数为0
|
||||||
*/
|
*/
|
||||||
private int[] nextMatchValuesAfter(int[] values) {
|
private int[] nextMatchValuesAfter(int[] values) {
|
||||||
final int[] newValues = values.clone();
|
|
||||||
|
|
||||||
int i = Part.YEAR.ordinal();
|
int i = Part.YEAR.ordinal();
|
||||||
// 新值,-1表示标识为回退
|
// 新值,-1表示标识为回退
|
||||||
int nextValue = 0;
|
int nextValue = 0;
|
||||||
@@ -189,30 +187,20 @@ public class PatternMatcher {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// pr#1189
|
nextValue = getNextMatch(values, i, 0);
|
||||||
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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextValue > values[i]) {
|
if (nextValue > values[i]) {
|
||||||
// 此部分正常获取新值,结束循环,后续的部分置最小值
|
// 此部分正常获取新值,结束循环,后续的部分置最小值
|
||||||
newValues[i] = nextValue;
|
values[i] = nextValue;
|
||||||
i--;
|
i--;
|
||||||
break;
|
break;
|
||||||
} else if (nextValue < values[i]) {
|
} else if (nextValue < values[i]) {
|
||||||
// 回退前保存最新值
|
|
||||||
newValues[i] = nextValue;
|
|
||||||
// 此部分下一个值获取到的值产生回退,回到上一个部分,继续获取新值
|
// 此部分下一个值获取到的值产生回退,回到上一个部分,继续获取新值
|
||||||
i++;
|
i++;
|
||||||
nextValue = -1;// 标记回退查找
|
nextValue = -1;// 标记回退查找
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 值不变,检查下一个部分
|
// 值不变,检查下一个部分
|
||||||
i--;
|
i--;
|
||||||
}
|
}
|
||||||
@@ -224,17 +212,12 @@ public class PatternMatcher {
|
|||||||
// 周不参与计算
|
// 周不参与计算
|
||||||
i++;
|
i++;
|
||||||
continue;
|
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]) {
|
if (nextValue > values[i]) {
|
||||||
newValues[i] = nextValue;
|
values[i] = nextValue;
|
||||||
i--;
|
i--;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -243,8 +226,32 @@ public class PatternMatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 修改值以下的字段全部归最小值
|
// 修改值以下的字段全部归最小值
|
||||||
setToMin(newValues, i);
|
setToMin(values, i);
|
||||||
return newValues;
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定部分的下一个匹配值,三种结果:
|
||||||
|
* <ul>
|
||||||
|
* <li>结果值大于原值:此部分已更新,后续部分取匹配的最小值。</li>
|
||||||
|
* <li>结果值小于原值:此部分获取到了最小值,上一个部分需要继续取下一个值。</li>
|
||||||
|
* <li>结果值等于原值:此部分匹配,获取下一个部分的next值</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @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 values 值数组
|
||||||
* @param toPart 截止的部分
|
* @param toPart 截止的部分
|
||||||
*/
|
*/
|
||||||
private void setToMin(int[] values, int toPart) {
|
private void setToMin(final int[] values, final int toPart) {
|
||||||
Part part;
|
Part part;
|
||||||
for (int i = 0; i <= toPart; i++) {
|
for (int i = toPart; i >= 0; i--) {
|
||||||
part = Part.of(i);
|
part = Part.of(i);
|
||||||
if (part == Part.DAY_OF_MONTH
|
if (part == Part.DAY_OF_MONTH) {
|
||||||
&& get(part) instanceof DayOfMonthMatcher
|
final boolean isLeapYear = DateUtil.isLeapYear(values[Part.YEAR.ordinal()]);
|
||||||
&& ((DayOfMonthMatcher) get(part)).isLast()) {
|
final int month = values[Part.MONTH.ordinal()];
|
||||||
int newMonth = values[Part.MONTH.ordinal()];
|
final PartMatcher partMatcher = get(part);
|
||||||
int newYear = values[Part.YEAR.ordinal()];
|
if (partMatcher instanceof DayOfMonthMatcher) {
|
||||||
values[i] = getLastDay(newMonth, newYear);
|
values[i] = ((DayOfMonthMatcher) partMatcher).getMinValue(month, isLeapYear);
|
||||||
} else {
|
continue;
|
||||||
values[i] = getMin(part);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
values[i] = getMin(part);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package cn.hutool.cron.pattern;
|
|||||||
import cn.hutool.core.date.DateField;
|
import cn.hutool.core.date.DateField;
|
||||||
import cn.hutool.core.date.DateTime;
|
import cn.hutool.core.date.DateTime;
|
||||||
import cn.hutool.core.date.DateUtil;
|
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 org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
public class CronPatternNextMatchTest {
|
public class CronPatternNextMatchTest {
|
||||||
|
|
||||||
@Test
|
@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
|
@Test
|
||||||
public void testEveryHour() {
|
public void testEveryHour() {
|
||||||
DateTime date = DateUtil.parse("2022-02-28 07:44:16");
|
DateTime date = DateUtil.parse("2022-02-28 07:44:16");
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package cn.hutool.cron.pattern;
|
package cn.hutool.cron.pattern;
|
||||||
|
|
||||||
|
import cn.hutool.core.date.DateTime;
|
||||||
import cn.hutool.core.date.DateUtil;
|
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 org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
public class CronPatternUtilTest {
|
public class CronPatternUtilTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -44,4 +47,37 @@ public class CronPatternUtilTest {
|
|||||||
assertEquals("2018-10-31 03:00:00", matchedDates.get(3).toString());
|
assertEquals("2018-10-31 03:00:00", matchedDates.get(3).toString());
|
||||||
assertEquals("2018-10-31 04:00:00", matchedDates.get(4).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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user