Skip to content

Commit 87c3bb5

Browse files
committed
Introduce CronExpression
This commit introduces CronExpression, a new for representing cron expressions, and a direct replacement for CronSequenceGenerator.
1 parent c17f204 commit 87c3bb5

File tree

8 files changed

+1227
-15
lines changed

8 files changed

+1227
-15
lines changed
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/*
2+
* Copyright 2002-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.scheduling.support;
18+
19+
import java.time.temporal.ChronoUnit;
20+
import java.time.temporal.Temporal;
21+
import java.util.Arrays;
22+
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.util.Assert;
25+
import org.springframework.util.StringUtils;
26+
27+
/**
28+
* Representation of a
29+
* <a href="https://www.manpagez.com/man/5/crontab/">crontab expression</a>
30+
* that can calculate the next time it matches.
31+
*
32+
* <p>{@code CronExpression} instances are created through
33+
* {@link #parse(String)}; the next match is determined with
34+
* {@link #next(Temporal)}.
35+
*
36+
* @author Arjen Poutsma
37+
* @since 5.3
38+
* @see CronTrigger
39+
*/
40+
public final class CronExpression {
41+
42+
static final int MAX_ATTEMPTS = 366;
43+
44+
45+
private final CronField[] fields;
46+
47+
private final String expression;
48+
49+
50+
private CronExpression(
51+
CronField seconds,
52+
CronField minutes,
53+
CronField hours,
54+
CronField daysOfMonth,
55+
CronField months,
56+
CronField daysOfWeek,
57+
String expression) {
58+
59+
// to make sure we end up at 0 nanos, we add an extra field
60+
this.fields = new CronField[]{daysOfWeek, months, daysOfMonth, hours, minutes, seconds, CronField.zeroNanos()};
61+
this.expression = expression;
62+
}
63+
64+
65+
/**
66+
* Parse the given
67+
* <a href="https://www.manpagez.com/man/5/crontab/">crontab expression</a>
68+
* string into a {@code CronExpression}.
69+
* The string has six single space-separated time and date fields:
70+
* <pre>
71+
* &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; second (0-59)
72+
* &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; minute (0 - 59)
73+
* &#9474; &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; hour (0 - 23)
74+
* &#9474; &#9474; &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; day of the month (1 - 31)
75+
* &#9474; &#9474; &#9474; &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; month (1 - 12) (or JAN-DEC)
76+
* &#9474; &#9474; &#9474; &#9474; &#9474; &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472; day of the week (0 - 7)
77+
* &#9474; &#9474; &#9474; &#9474; &#9474; &#9474; (0 or 7 is Sunday, or MON-SUN)
78+
* &#9474; &#9474; &#9474; &#9474; &#9474; &#9474;
79+
* &#42; &#42; &#42; &#42; &#42; &#42;
80+
* </pre>
81+
*
82+
* <p>The following rules apply:
83+
* <ul>
84+
* <li>
85+
* A field may be an asterisk ({@code *}), which always stands for
86+
* "first-last". For the "day of the month" or "day of the week" fields, a
87+
* question mark ({@code ?}) may be used instead of an asterisk.
88+
* </li>
89+
* <li>
90+
* Ranges of numbers are expressed by two numbers separated with a hyphen
91+
* ({@code -}). The specified range is inclusive.
92+
* </li>
93+
* <li>Following a range (or {@code *}) with {@code "/n"} specifies
94+
* skips of the number's value through the range.
95+
* </li>
96+
* <li>
97+
* English names can also be used for the "month" and "day of week" fields.
98+
* Use the first three letters of the particular day or month (case does not
99+
* matter).
100+
* </li>
101+
* </ul>
102+
*
103+
* <p>Example expressions:
104+
* <ul>
105+
* <li>{@code "0 0 * * * *"} = the top of every hour of every day.</li>
106+
* <li><code>"*&#47;10 * * * * *"</code> = every ten seconds.</li>
107+
* <li>{@code "0 0 8-10 * * *"} = 8, 9 and 10 o'clock of every day.</li>
108+
* <li>{@code "0 0 6,19 * * *"} = 6:00 AM and 7:00 PM every day.</li>
109+
* <li>{@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.</li>
110+
* <li>{@code "0 0 9-17 * * MON-FRI"} = on the hour nine-to-five weekdays</li>
111+
* <li>{@code "0 0 0 25 12 ?"} = every Christmas Day at midnight</li>
112+
* </ul>
113+
*
114+
* @param expression the expression string to parse
115+
* @return the parsed {@code CronExpression} object
116+
* @throws IllegalArgumentException in the expression does not conform to
117+
* the cron format
118+
*/
119+
public static CronExpression parse(String expression) {
120+
Assert.hasLength(expression, "Expression string must not be empty");
121+
122+
String[] fields = StringUtils.tokenizeToStringArray(expression, " ");
123+
if (fields.length != 6) {
124+
throw new IllegalArgumentException(String.format(
125+
"Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression));
126+
}
127+
try {
128+
CronField seconds = CronField.parseSeconds(fields[0]);
129+
CronField minutes = CronField.parseMinutes(fields[1]);
130+
CronField hours = CronField.parseHours(fields[2]);
131+
CronField daysOfMonth = CronField.parseDaysOfMonth(fields[3]);
132+
CronField months = CronField.parseMonth(fields[4]);
133+
CronField daysOfWeek = CronField.parseDaysOfWeek(fields[5]);
134+
135+
return new CronExpression(seconds, minutes, hours, daysOfMonth, months, daysOfWeek, expression);
136+
}
137+
catch (IllegalArgumentException ex) {
138+
String msg = ex.getMessage() + " in cron expression \"" + expression + "\"";
139+
throw new IllegalArgumentException(msg, ex);
140+
}
141+
}
142+
143+
144+
/**
145+
* Calculate the next {@link Temporal} that matches this expression.
146+
* @param temporal the seed value
147+
* @param <T> the type of temporal
148+
* @return the next temporal that matches this expression, or {@code null}
149+
* if no such temporal can be found
150+
*/
151+
@Nullable
152+
public <T extends Temporal> T next(T temporal) {
153+
return nextOrSame(ChronoUnit.NANOS.addTo(temporal, 1));
154+
}
155+
156+
157+
@Nullable
158+
private <T extends Temporal> T nextOrSame(T temporal) {
159+
for (int i = 0; i < MAX_ATTEMPTS; i++) {
160+
T result = nextOrSameInternal(temporal);
161+
if (result == null || result.equals(temporal)) {
162+
return result;
163+
}
164+
temporal = result;
165+
}
166+
return null;
167+
}
168+
169+
@Nullable
170+
private <T extends Temporal> T nextOrSameInternal(T temporal) {
171+
for (CronField field : this.fields) {
172+
temporal = field.nextOrSame(temporal);
173+
if (temporal == null) {
174+
return null;
175+
}
176+
}
177+
return temporal;
178+
}
179+
180+
181+
@Override
182+
public int hashCode() {
183+
return Arrays.hashCode(this.fields);
184+
}
185+
186+
@Override
187+
public boolean equals(Object o) {
188+
if (this == o) {
189+
return true;
190+
}
191+
if (o instanceof CronExpression) {
192+
CronExpression other = (CronExpression) o;
193+
return Arrays.equals(this.fields, other.fields);
194+
}
195+
else {
196+
return false;
197+
}
198+
}
199+
200+
/**
201+
* Return the expression string used to create this {@code CronExpression}.
202+
* @return the expression string
203+
*/
204+
@Override
205+
public String toString() {
206+
return this.expression;
207+
}
208+
209+
}

0 commit comments

Comments
 (0)