From b270bc1420687ed8088c78e6f17f4df1cdeb7725 Mon Sep 17 00:00:00 2001 From: emptypoiint <1215582715@qq.com> Date: Sat, 16 Mar 2024 23:22:57 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dcron=E6=97=A0=E6=B3=95?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=B0=8F=E6=9C=88=E7=9A=84=E6=9C=80=E5=90=8E?= =?UTF-8?q?=E4=B8=80=E5=A4=A9=E7=9A=84bug=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pattern/matcher/DayOfMonthMatcher.java | 12 +- .../cron/pattern/matcher/PatternMatcher.java | 32 ++++- .../cron/pattern/parser/PartParser.java | 10 +- .../pattern/CronPatternNextMatchTest.java | 131 ++++++++++++++++++ 4 files changed, 179 insertions(+), 6 deletions(-) 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 61a395d77..eeb631830 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,14 +11,19 @@ import java.util.List; * @author Looly */ public class DayOfMonthMatcher extends BoolArrayMatcher { + /** + * 是否是查询最后一天,即“L” + */ + private final boolean isLast; /** * 构造 * * @param intValueList 匹配的日值 */ - public DayOfMonthMatcher(List intValueList) { + public DayOfMonthMatcher(List intValueList, boolean isLast) { super(intValueList); + this.isLast = isLast; } /** @@ -50,4 +55,9 @@ public class DayOfMonthMatcher extends BoolArrayMatcher { private static boolean isLastDayOfMonth(int value, int month, boolean isLeapYear) { return value == Month.getLastDay(month - 1, isLeapYear); } + + public boolean isLast() { + return isLast; + } + } 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 bfc5712a1..6a31ed319 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 @@ -1,5 +1,7 @@ package cn.hutool.cron.pattern.matcher; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.Month; import cn.hutool.cron.pattern.Part; import java.time.Year; @@ -184,14 +186,23 @@ public class PatternMatcher { // 周不参与计算 i--; continue; + } else if (i == Part.DAY_OF_MONTH.ordinal() + && matchers[i] instanceof DayOfMonthMatcher + && ((DayOfMonthMatcher) matchers[i]).isLast()) { + int newMonth = newValues[Part.MONTH.ordinal()]; + int newYear = newValues[Part.YEAR.ordinal()]; + nextValue = Month.of(newMonth - 1).getLastDay(DateUtil.isLeapYear(newYear)); + } else { + nextValue = matchers[i].nextAfter(values[i]); } - nextValue = matchers[i].nextAfter(values[i]); if (nextValue > values[i]) { // 此部分正常获取新值,结束循环,后续的部分置最小值 newValues[i] = nextValue; i--; break; } else if (nextValue < values[i]) { + // 回退前保存最新值 + newValues[i] = nextValue; // 此部分下一个值获取到的值产生回退,回到上一个部分,继续获取新值 i++; nextValue = -1;// 标记回退查找 @@ -208,8 +219,15 @@ public class PatternMatcher { // 周不参与计算 i++; continue; + } else if (i == Part.DAY_OF_MONTH.ordinal() + && matchers[i] instanceof DayOfMonthMatcher + && ((DayOfMonthMatcher) matchers[i]).isLast()) { + int newMonth = newValues[Part.MONTH.ordinal()]; + int newYear = newValues[Part.YEAR.ordinal()]; + nextValue = Month.of(newMonth - 1).getLastDay(DateUtil.isLeapYear(newYear)); + } else { + nextValue = matchers[i].nextAfter(values[i] + 1); } - nextValue = matchers[i].nextAfter(values[i] + 1); if (nextValue > values[i]) { newValues[i] = nextValue; i--; @@ -234,7 +252,15 @@ public class PatternMatcher { Part part; for (int i = 0; i <= toPart; i++) { part = Part.of(i); - values[i] = getMin(part); + 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] = Month.of(newMonth - 1).getLastDay(DateUtil.isLeapYear(newYear)); + } else { + values[i] = getMin(part); + } } } diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/PartParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/PartParser.java index 7c33ed05c..c0d80fb5a 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/PartParser.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/PartParser.java @@ -32,6 +32,10 @@ import java.util.List; public class PartParser { private final Part part; + /** + * 是否是查询最后一天 + */ + private boolean isLastDay; /** * 创建解析器 @@ -69,6 +73,7 @@ public class PartParser { return new AlwaysTrueMatcher(); } + isLastDay = false; final List values = parseArray(value); if (values.isEmpty()) { throw new CronException("Invalid part value: [{}]", value); @@ -76,7 +81,7 @@ public class PartParser { switch (this.part) { case DAY_OF_MONTH: - return new DayOfMonthMatcher(values); + return new DayOfMonthMatcher(values, isLastDay); case YEAR: return new YearValueMatcher(values); default: @@ -226,7 +231,7 @@ public class PartParser { /** * 解析单个int值,支持别名 * - * @param value 被解析的值 + * @param value 被解析的值 * @param checkValue 是否检查值在有效范围内 * @return 解析结果 * @throws CronException 当无效数字或无效别名时抛出 @@ -265,6 +270,7 @@ public class PartParser { */ private int parseAlias(String name) throws CronException { if ("L".equalsIgnoreCase(name)) { + isLastDay = true; // L表示最大值 return part.getMax(); } 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 59f39cfbd..2a1753b1a 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 @@ -97,4 +97,135 @@ public class CronPatternNextMatchTest { final Calendar calendar = pattern.nextMatchAfter(time.toCalendar()); Assert.assertEquals("2022-04-09 01:01:01", DateUtil.date(calendar).toString()); } + + @Test + public void testLastDayOfMonthForEveryMonth1() { + DateTime date = DateUtil.parse("2023-01-08 07:44:16"); + DateTime result = DateUtil.parse("2023-01-31 03:02:01"); + // 匹配所有月,生成每个月的最后一天 + CronPattern pattern = new CronPattern("1 2 3 L * ?"); + for (int i = 0; i < 30; i++) { + //noinspection ConstantConditions + Calendar calendar = pattern.nextMatchAfter(date.toCalendar()); + date = DateUtil.date(calendar); + Assert.assertEquals(date, result); + // 加一秒 + date = date.offset(DateField.SECOND, 1); + + // 移动到下一个月的最后一天 + result = result.offset(DateField.DAY_OF_MONTH, 1); + int lastDayOfMonth = DateUtil.getLastDayOfMonth(result); + result.setField(DateField.DAY_OF_MONTH, lastDayOfMonth); + } + } + + @Test + public void testLastDayOfMonthForEveryMonth2() { + DateTime date = DateUtil.parse("2023-03-08 07:44:16"); + DateTime result = DateUtil.parse("2023-03-31 03:02:01"); + // 匹配所有月,生成每个月的最后一天 + CronPattern pattern = new CronPattern("1 2 3 L * ?"); + for (int i = 0; i < 30; i++) { + //noinspection ConstantConditions + Calendar calendar = pattern.nextMatchAfter(date.toCalendar()); + date = DateUtil.date(calendar); + Assert.assertEquals(date, result); + // 加一秒 + date = date.offset(DateField.SECOND, 1); + + // 移动到下一个月的最后一天 + result = result.offset(DateField.DAY_OF_MONTH, 1); + int lastDayOfMonth = DateUtil.getLastDayOfMonth(result); + result.setField(DateField.DAY_OF_MONTH, lastDayOfMonth); + } + } + + @Test + public void testLastDayOfMonthForEveryYear1() { + DateTime date = DateUtil.parse("2023-01-08 07:44:16"); + DateTime result = DateUtil.parse("2023-02-28 03:02:01"); + // 匹配每一年2月的最后一天 + CronPattern pattern = new CronPattern("1 2 3 L 2 ?"); + for (int i = 0; i < 10; i++) { + //noinspection ConstantConditions + Calendar calendar = pattern.nextMatchAfter(date.toCalendar()); + date = DateUtil.date(calendar); + Assert.assertEquals(date, result); + // 加一秒 + date = date.offset(DateField.SECOND, 1); + + // 移动到下一年的最后一天 + result = result.offset(DateField.YEAR, 1); + int lastDayOfMonth = DateUtil.getLastDayOfMonth(result); + result.setField(DateField.DAY_OF_MONTH, lastDayOfMonth); + } + } + + @Test + public void testLastDayOfMonthForEveryYear2() { + 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 ?"); + for (int i = 0; i < 30; i++) { + //noinspection ConstantConditions + Calendar calendar = pattern.nextMatchAfter(date.toCalendar()); + date = DateUtil.date(calendar); + Assert.assertEquals(date, result); + // 加一秒 + date = date.offset(DateField.SECOND, 1); + + // 移动到下一年的最后一天 + result = result.offset(DateField.YEAR, 1); + int lastDayOfMonth = DateUtil.getLastDayOfMonth(result); + result.setField(DateField.DAY_OF_MONTH, lastDayOfMonth); + } + } + + @Test + public void testEveryHour() { + DateTime date = DateUtil.parse("2022-02-28 07:44:16"); + DateTime result = DateUtil.parse("2022-02-28 08:02:01"); + // 匹配每一年2月的最后一天 + CronPattern pattern = new CronPattern("1 2 */1 * * ?"); + for (int i = 0; i < 30; i++) { + //noinspection ConstantConditions + Calendar calendar = pattern.nextMatchAfter(date.toCalendar()); + date = DateUtil.date(calendar); + Assert.assertEquals(date, result); + // 加一秒 + date = date.offset(DateField.SECOND, 1); + + // 移动到下一个小时 + result = result.offset(DateField.HOUR_OF_DAY, 1); + } + } + + @Test + public void testLastDayOfMonthForEveryHour() { + DateTime date = DateUtil.parse("2023-01-28 07:44:16"); + DateTime result = DateUtil.parse("2023-01-31 00:00:00"); + // 匹配每一年2月的最后一天 + CronPattern pattern = new CronPattern("0 0 */1 L * ?"); + for (int i = 0; i < 400; i++) { + //noinspection ConstantConditions + Calendar calendar = pattern.nextMatchAfter(date.toCalendar()); + date = DateUtil.date(calendar); + Assert.assertEquals(date, result); + // 加一秒 + date = date.offset(DateField.SECOND, 1); + + // 移动到下一个小时 + DateTime t = result.setMutable(false).offset(DateField.HOUR_OF_DAY, 1); + if (t.dayOfMonth() != result.dayOfMonth()) { + // 移动到下个月最后一天的开始 + result = result.offset(DateField.DAY_OF_MONTH, 1); + int lastDayOfMonth = DateUtil.getLastDayOfMonth(result); + result = result.setField(DateField.DAY_OF_MONTH, lastDayOfMonth); + result = DateUtil.beginOfDay(result); + } else { + result = t; + } + } + } } From ac1246d7d95a729b2f92c386415e2206a44ce9f2 Mon Sep 17 00:00:00 2001 From: emptypoiint <1215582715@qq.com> Date: Sun, 17 Mar 2024 14:36:02 +0800 Subject: [PATCH 2/2] =?UTF-8?q?update=20=E7=A1=AE=E4=BF=9D=E7=BA=BF?= =?UTF-8?q?=E7=A8=8B=E5=AE=89=E5=85=A8=EF=BC=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/cn/hutool/cron/pattern/parser/PartParser.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/PartParser.java b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/PartParser.java index c0d80fb5a..d80bf2736 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/PartParser.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/pattern/parser/PartParser.java @@ -32,10 +32,6 @@ import java.util.List; public class PartParser { private final Part part; - /** - * 是否是查询最后一天 - */ - private boolean isLastDay; /** * 创建解析器 @@ -68,12 +64,15 @@ public class PartParser { * @return {@link PartMatcher} */ public PartMatcher parse(String value) { + // 是否是查询最后一天 + boolean isLastDay = false; if (isMatchAllStr(value)) { //兼容Quartz的"?"表达式,不会出现互斥情况,与"*"作用相同 return new AlwaysTrueMatcher(); + } else if ("L".equalsIgnoreCase(value)) { + isLastDay = true; } - isLastDay = false; final List values = parseArray(value); if (values.isEmpty()) { throw new CronException("Invalid part value: [{}]", value); @@ -270,7 +269,6 @@ public class PartParser { */ private int parseAlias(String name) throws CronException { if ("L".equalsIgnoreCase(name)) { - isLastDay = true; // L表示最大值 return part.getMax(); }