Skip to content

Commit 1a8906b

Browse files
committed
Support macros in CronExpression
This commit introduces supports for macros like "@Yearly", "@monthly", etc. in CronExpression. Closes gh-25471
1 parent ea52627 commit 1a8906b

File tree

2 files changed

+153
-0
lines changed

2 files changed

+153
-0
lines changed

spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ public final class CronExpression {
4141

4242
static final int MAX_ATTEMPTS = 366;
4343

44+
private static final String[] MACROS = new String[] {
45+
"@yearly", "0 0 0 1 1 *",
46+
"@annually", "0 0 0 1 1 *",
47+
"@monthly", "0 0 0 1 * *",
48+
"@weekly", "0 0 0 * * 0",
49+
"@daily", "0 0 0 * * *",
50+
"@midnight", "0 0 0 * * *",
51+
"@hourly", "0 0 * * * *"
52+
};
53+
4454

4555
private final CronField[] fields;
4656

@@ -111,6 +121,15 @@ private CronExpression(
111121
* <li>{@code "0 0 0 25 12 ?"} = every Christmas Day at midnight</li>
112122
* </ul>
113123
*
124+
* <p>The following macros are also supported:
125+
* <ul>
126+
* <li>{@code "@yearly"} (or {@code "@annually"}) to run un once a year, i.e. {@code "0 0 0 1 1 *"},</li>
127+
* <li>{@code "@monthly"} to run once a month, i.e. {@code "0 0 0 1 * *"},</li>
128+
* <li>{@code "@weekly"} to run once a week, i.e. {@code "0 0 0 * * 0"},</li>
129+
* <li>{@code "@daily"} (or {@code "@midnight"}) to run once a day, i.e. {@code "0 0 0 * * *"},</li>
130+
* <li>{@code "@hourly"} to run once an hour, i.e. {@code "0 0 * * * *"}.</li>
131+
* </ul>
132+
*
114133
* @param expression the expression string to parse
115134
* @return the parsed {@code CronExpression} object
116135
* @throws IllegalArgumentException in the expression does not conform to
@@ -119,6 +138,8 @@ private CronExpression(
119138
public static CronExpression parse(String expression) {
120139
Assert.hasLength(expression, "Expression string must not be empty");
121140

141+
expression = resolveMacros(expression);
142+
122143
String[] fields = StringUtils.tokenizeToStringArray(expression, " ");
123144
if (fields.length != 6) {
124145
throw new IllegalArgumentException(String.format(
@@ -141,6 +162,17 @@ public static CronExpression parse(String expression) {
141162
}
142163

143164

165+
private static String resolveMacros(String expression) {
166+
expression = expression.trim();
167+
for (int i = 0; i < MACROS.length; i = i + 2) {
168+
if (MACROS[i].equalsIgnoreCase(expression)) {
169+
return MACROS[i + 1];
170+
}
171+
}
172+
return expression;
173+
}
174+
175+
144176
/**
145177
* Calculate the next {@link Temporal} that matches this expression.
146178
* @param temporal the seed value

spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
import static java.time.DayOfWeek.FRIDAY;
2929
import static java.time.DayOfWeek.MONDAY;
30+
import static java.time.DayOfWeek.SUNDAY;
3031
import static java.time.DayOfWeek.TUESDAY;
3132
import static java.time.DayOfWeek.WEDNESDAY;
3233
import static java.time.temporal.TemporalAdjusters.next;
@@ -462,4 +463,124 @@ void friday13th() {
462463
assertThat(actual.getDayOfMonth()).isEqualTo(13);
463464
}
464465

466+
@Test
467+
void yearly() {
468+
CronExpression expression = CronExpression.parse("@yearly");
469+
assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 1 1 *"));
470+
471+
LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(10);
472+
LocalDateTime expected = LocalDateTime.of(last.getYear() + 1, 1, 1, 0, 0);
473+
474+
LocalDateTime actual = expression.next(last);
475+
assertThat(actual).isEqualTo(expected);
476+
477+
last = actual;
478+
expected = expected.plusYears(1);
479+
actual = expression.next(last);
480+
assertThat(actual).isEqualTo(expected);
481+
482+
last = actual;
483+
expected = expected.plusYears(1);
484+
assertThat(expression.next(last)).isEqualTo(expected);
485+
}
486+
487+
@Test
488+
void annually() {
489+
CronExpression expression = CronExpression.parse("@annually");
490+
assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 1 1 *"));
491+
assertThat(expression).isEqualTo(CronExpression.parse("@yearly"));
492+
}
493+
494+
@Test
495+
void monthly() {
496+
CronExpression expression = CronExpression.parse("@monthly");
497+
assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 1 * *"));
498+
499+
LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(10);
500+
LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 1, 0, 0);
501+
502+
LocalDateTime actual = expression.next(last);
503+
assertThat(actual).isEqualTo(expected);
504+
505+
last = actual;
506+
expected = expected.plusMonths(1);
507+
actual = expression.next(last);
508+
assertThat(actual).isEqualTo(expected);
509+
510+
last = actual;
511+
expected = expected.plusMonths(1);
512+
assertThat(expression.next(last)).isEqualTo(expected);
513+
}
514+
515+
@Test
516+
void weekly() {
517+
CronExpression expression = CronExpression.parse("@weekly");
518+
assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 * * 0"));
519+
520+
LocalDateTime last = LocalDateTime.now();
521+
LocalDateTime expected = last.with(next(SUNDAY)).withHour(0).withMinute(0).withSecond(0).withNano(0);
522+
523+
LocalDateTime actual = expression.next(last);
524+
assertThat(actual).isEqualTo(expected);
525+
526+
last = actual;
527+
expected = expected.plusWeeks(1);
528+
actual = expression.next(last);
529+
assertThat(actual).isEqualTo(expected);
530+
531+
last = actual;
532+
expected = expected.plusWeeks(1);
533+
assertThat(expression.next(last)).isEqualTo(expected);
534+
}
535+
536+
@Test
537+
void daily() {
538+
CronExpression expression = CronExpression.parse("@daily");
539+
assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 * * *"));
540+
541+
LocalDateTime last = LocalDateTime.now();
542+
LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0);
543+
544+
LocalDateTime actual = expression.next(last);
545+
assertThat(actual).isEqualTo(expected);
546+
547+
last = actual;
548+
expected = expected.plusDays(1);
549+
actual = expression.next(last);
550+
assertThat(actual).isEqualTo(expected);
551+
552+
last = actual;
553+
expected = expected.plusDays(1);
554+
assertThat(expression.next(last)).isEqualTo(expected);
555+
}
556+
557+
@Test
558+
void midnight() {
559+
CronExpression expression = CronExpression.parse("@midnight");
560+
assertThat(expression).isEqualTo(CronExpression.parse("0 0 0 * * *"));
561+
assertThat(expression).isEqualTo(CronExpression.parse("@daily"));
562+
}
563+
564+
@Test
565+
void hourly() {
566+
CronExpression expression = CronExpression.parse("@hourly");
567+
assertThat(expression).isEqualTo(CronExpression.parse("0 0 * * * *"));
568+
569+
LocalDateTime last = LocalDateTime.now();
570+
LocalDateTime expected = last.plusHours(1).withMinute(0).withSecond(0).withNano(0);
571+
572+
LocalDateTime actual = expression.next(last);
573+
assertThat(actual).isEqualTo(expected);
574+
575+
last = actual;
576+
expected = expected.plusHours(1);
577+
actual = expression.next(last);
578+
assertThat(actual).isEqualTo(expected);
579+
580+
last = actual;
581+
expected = expected.plusHours(1);
582+
assertThat(expression.next(last)).isEqualTo(expected);
583+
}
584+
585+
465586
}

0 commit comments

Comments
 (0)