Browse Source
This commit introduces CronExpression, a new for representing cron expressions, and a direct replacement for CronSequenceGenerator.pull/25464/head
8 changed files with 1227 additions and 15 deletions
@ -0,0 +1,209 @@
@@ -0,0 +1,209 @@
|
||||
/* |
||||
* Copyright 2002-2020 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.scheduling.support; |
||||
|
||||
import java.time.temporal.ChronoUnit; |
||||
import java.time.temporal.Temporal; |
||||
import java.util.Arrays; |
||||
|
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* Representation of a |
||||
* <a href="https://www.manpagez.com/man/5/crontab/">crontab expression</a> |
||||
* that can calculate the next time it matches. |
||||
* |
||||
* <p>{@code CronExpression} instances are created through |
||||
* {@link #parse(String)}; the next match is determined with |
||||
* {@link #next(Temporal)}. |
||||
* |
||||
* @author Arjen Poutsma |
||||
* @since 5.3 |
||||
* @see CronTrigger |
||||
*/ |
||||
public final class CronExpression { |
||||
|
||||
static final int MAX_ATTEMPTS = 366; |
||||
|
||||
|
||||
private final CronField[] fields; |
||||
|
||||
private final String expression; |
||||
|
||||
|
||||
private CronExpression( |
||||
CronField seconds, |
||||
CronField minutes, |
||||
CronField hours, |
||||
CronField daysOfMonth, |
||||
CronField months, |
||||
CronField daysOfWeek, |
||||
String expression) { |
||||
|
||||
// to make sure we end up at 0 nanos, we add an extra field
|
||||
this.fields = new CronField[]{daysOfWeek, months, daysOfMonth, hours, minutes, seconds, CronField.zeroNanos()}; |
||||
this.expression = expression; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Parse the given |
||||
* <a href="https://www.manpagez.com/man/5/crontab/">crontab expression</a> |
||||
* string into a {@code CronExpression}. |
||||
* The string has six single space-separated time and date fields: |
||||
* <pre> |
||||
* ┌───────────── second (0-59) |
||||
* │ ┌───────────── minute (0 - 59) |
||||
* │ │ ┌───────────── hour (0 - 23) |
||||
* │ │ │ ┌───────────── day of the month (1 - 31) |
||||
* │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC) |
||||
* │ │ │ │ │ ┌───────────── day of the week (0 - 7) |
||||
* │ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN) |
||||
* │ │ │ │ │ │ |
||||
* * * * * * * |
||||
* </pre> |
||||
* |
||||
* <p>The following rules apply: |
||||
* <ul> |
||||
* <li> |
||||
* A field may be an asterisk ({@code *}), which always stands for |
||||
* "first-last". For the "day of the month" or "day of the week" fields, a |
||||
* question mark ({@code ?}) may be used instead of an asterisk. |
||||
* </li> |
||||
* <li> |
||||
* Ranges of numbers are expressed by two numbers separated with a hyphen |
||||
* ({@code -}). The specified range is inclusive. |
||||
* </li> |
||||
* <li>Following a range (or {@code *}) with {@code "/n"} specifies |
||||
* skips of the number's value through the range. |
||||
* </li> |
||||
* <li> |
||||
* English names can also be used for the "month" and "day of week" fields. |
||||
* Use the first three letters of the particular day or month (case does not |
||||
* matter). |
||||
* </li> |
||||
* </ul> |
||||
* |
||||
* <p>Example expressions: |
||||
* <ul> |
||||
* <li>{@code "0 0 * * * *"} = the top of every hour of every day.</li> |
||||
* <li><code>"*/10 * * * * *"</code> = every ten seconds.</li> |
||||
* <li>{@code "0 0 8-10 * * *"} = 8, 9 and 10 o'clock of every day.</li> |
||||
* <li>{@code "0 0 6,19 * * *"} = 6:00 AM and 7:00 PM every day.</li> |
||||
* <li>{@code "0 0/30 8-10 * * *"} = 8:00, 8:30, 9:00, 9:30, 10:00 and 10:30 every day.</li> |
||||
* <li>{@code "0 0 9-17 * * MON-FRI"} = on the hour nine-to-five weekdays</li> |
||||
* <li>{@code "0 0 0 25 12 ?"} = every Christmas Day at midnight</li> |
||||
* </ul> |
||||
* |
||||
* @param expression the expression string to parse |
||||
* @return the parsed {@code CronExpression} object |
||||
* @throws IllegalArgumentException in the expression does not conform to |
||||
* the cron format |
||||
*/ |
||||
public static CronExpression parse(String expression) { |
||||
Assert.hasLength(expression, "Expression string must not be empty"); |
||||
|
||||
String[] fields = StringUtils.tokenizeToStringArray(expression, " "); |
||||
if (fields.length != 6) { |
||||
throw new IllegalArgumentException(String.format( |
||||
"Cron expression must consist of 6 fields (found %d in \"%s\")", fields.length, expression)); |
||||
} |
||||
try { |
||||
CronField seconds = CronField.parseSeconds(fields[0]); |
||||
CronField minutes = CronField.parseMinutes(fields[1]); |
||||
CronField hours = CronField.parseHours(fields[2]); |
||||
CronField daysOfMonth = CronField.parseDaysOfMonth(fields[3]); |
||||
CronField months = CronField.parseMonth(fields[4]); |
||||
CronField daysOfWeek = CronField.parseDaysOfWeek(fields[5]); |
||||
|
||||
return new CronExpression(seconds, minutes, hours, daysOfMonth, months, daysOfWeek, expression); |
||||
} |
||||
catch (IllegalArgumentException ex) { |
||||
String msg = ex.getMessage() + " in cron expression \"" + expression + "\""; |
||||
throw new IllegalArgumentException(msg, ex); |
||||
} |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Calculate the next {@link Temporal} that matches this expression. |
||||
* @param temporal the seed value |
||||
* @param <T> the type of temporal |
||||
* @return the next temporal that matches this expression, or {@code null} |
||||
* if no such temporal can be found |
||||
*/ |
||||
@Nullable |
||||
public <T extends Temporal> T next(T temporal) { |
||||
return nextOrSame(ChronoUnit.NANOS.addTo(temporal, 1)); |
||||
} |
||||
|
||||
|
||||
@Nullable |
||||
private <T extends Temporal> T nextOrSame(T temporal) { |
||||
for (int i = 0; i < MAX_ATTEMPTS; i++) { |
||||
T result = nextOrSameInternal(temporal); |
||||
if (result == null || result.equals(temporal)) { |
||||
return result; |
||||
} |
||||
temporal = result; |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
@Nullable |
||||
private <T extends Temporal> T nextOrSameInternal(T temporal) { |
||||
for (CronField field : this.fields) { |
||||
temporal = field.nextOrSame(temporal); |
||||
if (temporal == null) { |
||||
return null; |
||||
} |
||||
} |
||||
return temporal; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return Arrays.hashCode(this.fields); |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object o) { |
||||
if (this == o) { |
||||
return true; |
||||
} |
||||
if (o instanceof CronExpression) { |
||||
CronExpression other = (CronExpression) o; |
||||
return Arrays.equals(this.fields, other.fields); |
||||
} |
||||
else { |
||||
return false; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Return the expression string used to create this {@code CronExpression}. |
||||
* @return the expression string |
||||
*/ |
||||
@Override |
||||
public String toString() { |
||||
return this.expression; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,385 @@
@@ -0,0 +1,385 @@
|
||||
/* |
||||
* Copyright 2002-2020 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.scheduling.support; |
||||
|
||||
import java.time.DateTimeException; |
||||
import java.time.temporal.ChronoField; |
||||
import java.time.temporal.Temporal; |
||||
import java.time.temporal.ValueRange; |
||||
import java.util.BitSet; |
||||
|
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* A single field in a cron pattern. Created using the {@code parse*} methods, |
||||
* main and only entry point is {@link #nextOrSame(Temporal)}. |
||||
* |
||||
* @author Arjen Poutsma |
||||
* @since 5.3 |
||||
*/ |
||||
final class CronField { |
||||
|
||||
private static final String[] MONTHS = new String[]{"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", |
||||
"OCT", "NOV", "DEC"}; |
||||
|
||||
private static final String[] DAYS = new String[]{"MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"}; |
||||
|
||||
private static final CronField ZERO_NANOS; |
||||
|
||||
|
||||
static { |
||||
ZERO_NANOS = new CronField(Type.NANO); |
||||
ZERO_NANOS.bits.set(0); |
||||
} |
||||
|
||||
|
||||
private final Type type; |
||||
|
||||
private final BitSet bits; |
||||
|
||||
|
||||
private CronField(Type type) { |
||||
this.type = type; |
||||
this.bits = new BitSet((int) type.range().getMaximum()); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Return a {@code CronField} enabled for 0 nano seconds. |
||||
*/ |
||||
public static CronField zeroNanos() { |
||||
return ZERO_NANOS; |
||||
} |
||||
|
||||
/** |
||||
* Parse the given value into a seconds {@code CronField}, the first entry of a cron expression. |
||||
*/ |
||||
public static CronField parseSeconds(String value) { |
||||
return parseField(value, Type.SECOND); |
||||
} |
||||
|
||||
/** |
||||
* Parse the given value into a minutes {@code CronField}, the second entry of a cron expression. |
||||
*/ |
||||
public static CronField parseMinutes(String value) { |
||||
return parseField(value, Type.MINUTE); |
||||
} |
||||
|
||||
/** |
||||
* Parse the given value into a hours {@code CronField}, the third entry of a cron expression. |
||||
*/ |
||||
public static CronField parseHours(String value) { |
||||
return parseField(value, Type.HOUR); |
||||
} |
||||
|
||||
/** |
||||
* Parse the given value into a days of months {@code CronField}, the fourth entry of a cron expression. |
||||
*/ |
||||
public static CronField parseDaysOfMonth(String value) { |
||||
return parseDate(value, Type.DAY_OF_MONTH); |
||||
} |
||||
|
||||
/** |
||||
* Parse the given value into a month {@code CronField}, the fifth entry of a cron expression. |
||||
*/ |
||||
public static CronField parseMonth(String value) { |
||||
value = replaceOrdinals(value, MONTHS); |
||||
return parseField(value, Type.MONTH); |
||||
} |
||||
|
||||
/** |
||||
* Parse the given value into a days of week {@code CronField}, the sixth entry of a cron expression. |
||||
*/ |
||||
public static CronField parseDaysOfWeek(String value) { |
||||
value = replaceOrdinals(value, DAYS); |
||||
CronField result = parseDate(value, Type.DAY_OF_WEEK); |
||||
if (result.bits.get(0)) { |
||||
// cron supports 0 for Sunday; we use 7 like java.time
|
||||
result.bits.set(7); |
||||
result.bits.clear(0); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
|
||||
private static CronField parseDate(String value, Type type) { |
||||
if (value.indexOf('?') != -1) { |
||||
value = "*"; |
||||
} |
||||
return parseField(value, type); |
||||
} |
||||
|
||||
private static CronField parseField(String value, Type type) { |
||||
Assert.hasLength(value, "Value must not be empty"); |
||||
Assert.notNull(type, "Type must not be null"); |
||||
try { |
||||
CronField result = new CronField(type); |
||||
String[] fields = StringUtils.delimitedListToStringArray(value, ","); |
||||
for (String field : fields) { |
||||
int slashPos = field.indexOf('/'); |
||||
if (slashPos == -1) { |
||||
ValueRange range = parseRange(field, type); |
||||
result.setBits(range); |
||||
} |
||||
else { |
||||
String rangeStr = value.substring(0, slashPos); |
||||
String deltaStr = value.substring(slashPos + 1); |
||||
ValueRange range = parseRange(rangeStr, type); |
||||
if (rangeStr.indexOf('-') == -1) { |
||||
range = ValueRange.of(range.getMinimum(), type.range().getMaximum()); |
||||
} |
||||
int delta = Integer.parseInt(deltaStr); |
||||
if (delta <= 0) { |
||||
throw new IllegalArgumentException("Incrementer delta must be 1 or higher"); |
||||
} |
||||
result.setBits(range, delta); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
catch (DateTimeException | IllegalArgumentException ex) { |
||||
String msg = ex.getMessage() + " '" + value + "'"; |
||||
throw new IllegalArgumentException(msg, ex); |
||||
} |
||||
} |
||||
|
||||
private static ValueRange parseRange(String value, Type type) { |
||||
if (value.indexOf('*') != -1) { |
||||
return type.range(); |
||||
} |
||||
else { |
||||
int hyphenPos = value.indexOf('-'); |
||||
if (hyphenPos == -1) { |
||||
int result = type.checkValidValue(Integer.parseInt(value)); |
||||
return ValueRange.of(result, result); |
||||
} |
||||
else { |
||||
int min = Integer.parseInt(value.substring(0, hyphenPos)); |
||||
int max = Integer.parseInt(value.substring(hyphenPos + 1)); |
||||
min = type.checkValidValue(min); |
||||
max = type.checkValidValue(max); |
||||
return ValueRange.of(min, max); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static String replaceOrdinals(String value, String[] list) { |
||||
value = value.toUpperCase(); |
||||
for (int i = 0; i < list.length; i++) { |
||||
String replacement = Integer.toString(i + 1); |
||||
value = StringUtils.replace(value, list[i], replacement); |
||||
} |
||||
return value; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Get the next or same {@link Temporal} in the sequence matching this |
||||
* cron field. |
||||
* @param temporal the seed value |
||||
* @return the next or same temporal matching the pattern |
||||
*/ |
||||
@Nullable |
||||
public <T extends Temporal> T nextOrSame(T temporal) { |
||||
int current = this.type.get(temporal); |
||||
int next = this.bits.nextSetBit(current); |
||||
if (next == -1) { |
||||
temporal = this.type.rollForward(temporal); |
||||
next = this.bits.nextSetBit(0); |
||||
} |
||||
if (next == current) { |
||||
return temporal; |
||||
} |
||||
else { |
||||
int count = 0; |
||||
current = this.type.get(temporal); |
||||
while (current != next && count++ < CronExpression.MAX_ATTEMPTS) { |
||||
temporal = this.type.elapseUntil(temporal, next); |
||||
current = this.type.get(temporal); |
||||
} |
||||
if (count >= CronExpression.MAX_ATTEMPTS) { |
||||
return null; |
||||
} |
||||
return this.type.reset(temporal); |
||||
} |
||||
} |
||||
|
||||
|
||||
BitSet bits() { |
||||
return this.bits; |
||||
} |
||||
|
||||
private void setBits(ValueRange range) { |
||||
this.bits.set((int) range.getMinimum(), (int) range.getMaximum() + 1); |
||||
} |
||||
|
||||
private void setBits(ValueRange range, int delta) { |
||||
for (int i = (int) range.getMinimum(); i <= range.getMaximum(); i += delta) { |
||||
this.bits.set(i); |
||||
} |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public int hashCode() { |
||||
return this.bits.hashCode(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean equals(Object o) { |
||||
if (this == o) { |
||||
return true; |
||||
} |
||||
if (!(o instanceof CronField)) { |
||||
return false; |
||||
} |
||||
CronField other = (CronField) o; |
||||
return this.type == other.type && |
||||
this.bits.equals(other.bits); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return this.type + " " + this.bits; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Represents the type of cron field, i.e. seconds, minutes, hours, |
||||
* day-of-month, month, day-of-week. |
||||
*/ |
||||
private enum Type { |
||||
NANO(ChronoField.NANO_OF_SECOND), |
||||
SECOND(ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), |
||||
MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), |
||||
HOUR(ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), |
||||
DAY_OF_MONTH(ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), |
||||
MONTH(ChronoField.MONTH_OF_YEAR, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), |
||||
DAY_OF_WEEK(ChronoField.DAY_OF_WEEK, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND); |
||||
|
||||
|
||||
private final ChronoField field; |
||||
|
||||
private final ChronoField[] lowerOrders; |
||||
|
||||
|
||||
Type(ChronoField field, ChronoField... lowerOrders) { |
||||
this.field = field; |
||||
this.lowerOrders = lowerOrders; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Return the value of this type for the given temporal. |
||||
* @return the value of this type |
||||
*/ |
||||
public int get(Temporal date) { |
||||
return date.get(this.field); |
||||
} |
||||
|
||||
/** |
||||
* Return the general range of this type. For instance, this methods |
||||
* will return 0-31 for {@link #MONTH}. |
||||
* @return the range of this field |
||||
*/ |
||||
public ValueRange range() { |
||||
return this.field.range(); |
||||
} |
||||
|
||||
/** |
||||
* Check whether the given value is valid, i.e. whether it falls in |
||||
* {@linkplain #range() range}. |
||||
* @param value the value to check |
||||
* @return the value that was passed in |
||||
* @throws IllegalArgumentException if the given value is invalid |
||||
*/ |
||||
public int checkValidValue(int value) { |
||||
if (this == DAY_OF_WEEK && value == 0) { |
||||
return value; |
||||
} |
||||
else { |
||||
try { |
||||
return this.field.checkValidIntValue(value); |
||||
} |
||||
catch (DateTimeException ex) { |
||||
throw new IllegalArgumentException(ex.getMessage(), ex); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Elapse the given temporal for the difference between the current |
||||
* value of this field and the goal value. Typically, the returned |
||||
* temporal will have the given goal as the current value for this type, |
||||
* but this is not the case for {@link #DAY_OF_MONTH}. For instance, |
||||
* if {@code goal} is 31, and {@code temporal} is April 16th, |
||||
* this method returns May 1st, because April 31st does not exist. |
||||
* @param temporal the temporal to elapse |
||||
* @param goal the goal value |
||||
* @param <T> the type of temporal |
||||
* @return the elapsed temporal, typically with {@code goal} as value |
||||
* for this type. |
||||
*/ |
||||
public <T extends Temporal> T elapseUntil(T temporal, int goal) { |
||||
int current = get(temporal); |
||||
if (current < goal) { |
||||
return this.field.getBaseUnit().addTo(temporal, goal - current); |
||||
} |
||||
else { |
||||
ValueRange range = temporal.range(this.field); |
||||
long amount = goal + range.getMaximum() - current + 1 - range.getMinimum(); |
||||
return this.field.getBaseUnit().addTo(temporal, amount); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Roll forward the give temporal until it reaches the next higher |
||||
* order field. Calling this method is equivalent to calling |
||||
* {@link #elapseUntil(Temporal, int)} with goal set to the |
||||
* minimum value of this field's range. |
||||
* @param temporal the temporal to roll forward |
||||
* @param <T> the type of temporal |
||||
* @return the rolled forward temporal |
||||
*/ |
||||
public <T extends Temporal> T rollForward(T temporal) { |
||||
int current = get(temporal); |
||||
ValueRange range = temporal.range(this.field); |
||||
long amount = range.getMaximum() - current + 1; |
||||
return this.field.getBaseUnit().addTo(temporal, amount); |
||||
} |
||||
|
||||
/** |
||||
* Reset this and all lower order fields of the given temporal to their |
||||
* minimum value. For instance for {@link #MINUTE}, this method |
||||
* resets nanos, seconds, <strong>and</strong> minutes to 0. |
||||
* @param temporal the temporal to reset |
||||
* @param <T> the type of temporal |
||||
* @return the reset temporal |
||||
*/ |
||||
public <T extends Temporal> T reset(T temporal) { |
||||
for (ChronoField lowerOrder : this.lowerOrders) { |
||||
if (temporal.isSupported(lowerOrder)) { |
||||
temporal = lowerOrder.adjustInto(temporal, temporal.range(lowerOrder).getMinimum()); |
||||
} |
||||
} |
||||
return temporal; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,81 @@
@@ -0,0 +1,81 @@
|
||||
/* |
||||
* Copyright 2002-2020 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.scheduling.support; |
||||
|
||||
import java.util.BitSet; |
||||
|
||||
import org.assertj.core.api.AbstractAssert; |
||||
|
||||
/** |
||||
* @author Arjen Poutsma |
||||
*/ |
||||
public class BitSetAssert extends AbstractAssert<BitSetAssert, BitSet> { |
||||
|
||||
private BitSetAssert(BitSet bitSet) { |
||||
super(bitSet, BitSetAssert.class); |
||||
} |
||||
|
||||
public static BitSetAssert assertThat(BitSet actual) { |
||||
return new BitSetAssert(actual); |
||||
} |
||||
|
||||
public BitSetAssert hasSet(int... indices) { |
||||
isNotNull(); |
||||
|
||||
for (int index : indices) { |
||||
if (!this.actual.get(index)) { |
||||
failWithMessage("Invalid disabled bit at @%d", index); |
||||
} |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
public BitSetAssert hasSetRange(int min, int max) { |
||||
isNotNull(); |
||||
|
||||
for (int i = min; i < max; i++) { |
||||
if (!this.actual.get(i)) { |
||||
failWithMessage("Invalid disabled bit at @%d", i); |
||||
} |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
public BitSetAssert hasUnset(int... indices) { |
||||
isNotNull(); |
||||
|
||||
for (int index : indices) { |
||||
if (this.actual.get(index)) { |
||||
failWithMessage("Invalid enabled bit at @%d", index); |
||||
} |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
public BitSetAssert hasUnsetRange(int min, int max) { |
||||
isNotNull(); |
||||
|
||||
for (int i = min; i < max; i++) { |
||||
if (this.actual.get(i)) { |
||||
failWithMessage("Invalid enabled bit at @%d", i); |
||||
} |
||||
} |
||||
return this; |
||||
} |
||||
|
||||
} |
||||
|
||||
@ -0,0 +1,434 @@
@@ -0,0 +1,434 @@
|
||||
/* |
||||
* Copyright 2002-2020 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.scheduling.support; |
||||
|
||||
import java.time.LocalDate; |
||||
import java.time.LocalDateTime; |
||||
import java.time.LocalTime; |
||||
import java.time.Year; |
||||
import java.time.ZoneId; |
||||
import java.time.ZonedDateTime; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import static java.time.DayOfWeek.MONDAY; |
||||
import static java.time.DayOfWeek.TUESDAY; |
||||
import static java.time.DayOfWeek.WEDNESDAY; |
||||
import static java.time.temporal.TemporalAdjusters.next; |
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* @author Arjen Poutsma |
||||
*/ |
||||
class CronExpressionTests { |
||||
|
||||
@Test |
||||
void matchAll() { |
||||
CronExpression expression = CronExpression.parse("* * * * * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now(); |
||||
LocalDateTime expected = last.plusSeconds(1).withNano(0); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void matchLastSecond() { |
||||
CronExpression expression = CronExpression.parse("* * * * * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withSecond(58); |
||||
LocalDateTime expected = last.plusSeconds(1).withNano(0); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void matchSpecificSecond() { |
||||
CronExpression expression = CronExpression.parse("10 * * * * *"); |
||||
|
||||
LocalDateTime now = LocalDateTime.now(); |
||||
LocalDateTime last = now.withSecond(9); |
||||
LocalDateTime expected = last.withSecond(10).withNano(0); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void incrementSecondByOne() { |
||||
CronExpression expression = CronExpression.parse("11 * * * * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withSecond(10); |
||||
LocalDateTime expected = last.plusSeconds(1).withNano(0); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void incrementSecondAndRollover() { |
||||
CronExpression expression = CronExpression.parse("10 * * * * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withSecond(11); |
||||
LocalDateTime expected = last.plusMinutes(1).withSecond(10).withNano(0); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void secondRange() { |
||||
CronExpression expression = CronExpression.parse("10-15 * * * * *"); |
||||
LocalDateTime now = LocalDateTime.now(); |
||||
|
||||
for (int i = 9; i < 15; i++) { |
||||
LocalDateTime last = now.withSecond(i); |
||||
LocalDateTime expected = last.plusSeconds(1).withNano(0); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
void incrementMinute() { |
||||
CronExpression expression = CronExpression.parse("0 * * * * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withMinute(10); |
||||
LocalDateTime expected = last.plusMinutes(1).withSecond(0).withNano(0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.plusMinutes(1); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void incrementMinuteByOne() { |
||||
CronExpression expression = CronExpression.parse("0 11 * * * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withMinute(10); |
||||
LocalDateTime expected = last.plusMinutes(1).withSecond(0).withNano(0); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void incrementMinuteAndRollover() { |
||||
CronExpression expression = CronExpression.parse("0 10 * * * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withMinute(11).withSecond(0); |
||||
LocalDateTime expected = last.plusMinutes(59).withNano(0); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void incrementHour() { |
||||
CronExpression expression = CronExpression.parse("0 0 * * * *"); |
||||
|
||||
int year = Year.now().getValue(); |
||||
LocalDateTime last = LocalDateTime.of(year, 10, 30, 11, 1); |
||||
LocalDateTime expected = last.withHour(12).withMinute(0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.withHour(13); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void incrementHourAndRollover() { |
||||
CronExpression expression = CronExpression.parse("0 0 * * * *"); |
||||
|
||||
int year = Year.now().getValue(); |
||||
LocalDateTime last = LocalDateTime.of(year, 9, 10, 23, 1); |
||||
LocalDateTime expected = last.withDayOfMonth(11).withHour(0).withMinute(0); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void incrementDayOfMonth() { |
||||
CronExpression expression = CronExpression.parse("0 0 0 * * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withDayOfMonth(1); |
||||
LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.plusDays(1); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void incrementDayOfMonthByOne() { |
||||
CronExpression expression = CronExpression.parse("* * * 10 * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withDayOfMonth(9); |
||||
LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void incrementDayOfMonthAndRollover() { |
||||
CronExpression expression = CronExpression.parse("* * * 10 * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withDayOfMonth(11); |
||||
LocalDateTime expected = |
||||
last.plusMonths(1).withDayOfMonth(10).withHour(0).withMinute(0).withSecond(0).withNano(0); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void dailyTriggerInShortMonth() { |
||||
CronExpression expression = CronExpression.parse("0 0 0 * * *"); |
||||
|
||||
// September: 30 days
|
||||
LocalDateTime last = LocalDateTime.now().withMonth(9).withDayOfMonth(30); |
||||
LocalDateTime expected = LocalDateTime.of(last.getYear(), 10, 1, 0, 0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.withDayOfMonth(2); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void dailyTriggerInLongMonth() { |
||||
CronExpression expression = CronExpression.parse("0 0 0 * * *"); |
||||
|
||||
// August: 31 days and not a daylight saving boundary
|
||||
LocalDateTime last = LocalDateTime.now().withMonth(8).withDayOfMonth(30); |
||||
LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.plusDays(1); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void dailyTriggerOnDaylightSavingBoundary() { |
||||
CronExpression expression = CronExpression.parse("0 0 0 * * *"); |
||||
|
||||
// October: 31 days and a daylight saving boundary in CET
|
||||
ZonedDateTime last = ZonedDateTime.now(ZoneId.of("CET")).withMonth(10).withDayOfMonth(30); |
||||
ZonedDateTime expected = last.withDayOfMonth(31).withHour(0).withMinute(0).withSecond(0).withNano(0); |
||||
ZonedDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.withMonth(11).withDayOfMonth(1); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void incrementMonth() { |
||||
CronExpression expression = CronExpression.parse("0 0 0 1 * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(30); |
||||
LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 1, 0, 0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.withMonth(12); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void incrementMonthAndRollover() { |
||||
CronExpression expression = CronExpression.parse("0 0 0 1 * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withYear(2010).withMonth(12).withDayOfMonth(31); |
||||
LocalDateTime expected = LocalDateTime.of(2011, 1, 1, 0, 0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.plusMonths(1); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void monthlyTriggerInLongMonth() { |
||||
CronExpression expression = CronExpression.parse("0 0 0 31 * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(30); |
||||
LocalDateTime expected = last.withDayOfMonth(31).withHour(0).withMinute(0).withSecond(0).withNano(0); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void monthlyTriggerInShortMonth() { |
||||
CronExpression expression = CronExpression.parse("0 0 0 1 * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(30); |
||||
LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 1, 0, 0); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void incrementDayOfWeekByOne() { |
||||
CronExpression expression = CronExpression.parse("* * * * * 2"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().with(next(MONDAY)); |
||||
LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(0).withNano(0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
assertThat(actual.getDayOfWeek()).isEqualTo(TUESDAY); |
||||
} |
||||
|
||||
@Test |
||||
void incrementDayOfWeekAndRollover() { |
||||
CronExpression expression = CronExpression.parse("* * * * * 2"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().with(next(WEDNESDAY)); |
||||
LocalDateTime expected = last.plusDays(6).withHour(0).withMinute(0).withSecond(0).withNano(0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
assertThat(actual.getDayOfWeek()).isEqualTo(TUESDAY); |
||||
} |
||||
|
||||
@Test |
||||
void specificMinuteSecond() { |
||||
CronExpression expression = CronExpression.parse("55 5 * * * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withMinute(4).withSecond(54); |
||||
LocalDateTime expected = last.plusMinutes(1).withSecond(55).withNano(0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.plusHours(1); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void specificHourSecond() { |
||||
CronExpression expression = CronExpression.parse("55 * 10 * * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withHour(9).withSecond(54); |
||||
LocalDateTime expected = last.plusHours(1).withMinute(0).withSecond(55).withNano(0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.plusMinutes(1); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void specificMinuteHour() { |
||||
CronExpression expression = CronExpression.parse("* 5 10 * * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withHour(9).withMinute(4); |
||||
LocalDateTime expected = last.plusHours(1).plusMinutes(1).withSecond(0).withNano(0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
// next trigger is in one second because second is wildcard
|
||||
expected = expected.plusSeconds(1); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void specificDayOfMonthSecond() { |
||||
CronExpression expression = CronExpression.parse("55 * * 3 * *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withDayOfMonth(2).withSecond(54); |
||||
LocalDateTime expected = last.plusDays(1).withHour(0).withMinute(0).withSecond(55).withNano(0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.plusMinutes(1); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void specificDate() { |
||||
CronExpression expression = CronExpression.parse("* * * 3 11 *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withMonth(10).withDayOfMonth(2); |
||||
LocalDateTime expected = LocalDateTime.of(last.getYear(), 11, 3, 0, 0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.plusSeconds(1); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void nonExistentSpecificDate() { |
||||
CronExpression expression = CronExpression.parse("0 0 0 31 6 *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withMonth(3).withDayOfMonth(10); |
||||
assertThat(expression.next(last)).isNull(); |
||||
} |
||||
|
||||
@Test |
||||
void leapYearSpecificDate() { |
||||
CronExpression expression = CronExpression.parse("0 0 0 29 2 *"); |
||||
|
||||
LocalDateTime last = LocalDateTime.now().withYear(2007).withMonth(2).withDayOfMonth(10); |
||||
LocalDateTime expected = LocalDateTime.of(2008, 2, 29, 0, 0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.plusYears(4); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void weekDaySequence() { |
||||
CronExpression expression = CronExpression.parse("0 0 7 ? * MON-FRI"); |
||||
|
||||
// This is a Saturday
|
||||
LocalDateTime last = LocalDateTime.of(LocalDate.of(2009, 9, 26), LocalTime.now()); |
||||
LocalDateTime expected = last.plusDays(2).withHour(7).withMinute(0).withSecond(0).withNano(0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
// Next day is a week day so add one
|
||||
last = actual; |
||||
expected = expected.plusDays(1); |
||||
actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.plusDays(1); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
@Test |
||||
void monthSequence() { |
||||
CronExpression expression = CronExpression.parse("0 30 23 30 1/3 ?"); |
||||
|
||||
LocalDateTime last = LocalDateTime.of(LocalDate.of(2010, 12, 30), LocalTime.now()); |
||||
LocalDateTime expected = last.plusMonths(1).withHour(23).withMinute(30).withSecond(0).withSecond(0).withNano(0); |
||||
LocalDateTime actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
// Next trigger is 3 months later
|
||||
last = actual; |
||||
expected = expected.plusMonths(3); |
||||
actual = expression.next(last); |
||||
assertThat(actual).isEqualTo(expected); |
||||
|
||||
last = actual; |
||||
expected = expected.plusMonths(3); |
||||
assertThat(expression.next(last)).isEqualTo(expected); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
|
||||
/* |
||||
* Copyright 2002-2020 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.scheduling.support; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
import static org.springframework.scheduling.support.BitSetAssert.assertThat; |
||||
|
||||
/** |
||||
* @author Arjen Poutsma |
||||
*/ |
||||
public class CronFieldTests { |
||||
|
||||
@Test |
||||
void parse() { |
||||
assertThat(CronField.parseSeconds("42").bits()).hasUnsetRange(0, 41).hasSet(42).hasUnsetRange(43, 59); |
||||
assertThat(CronField.parseMinutes("1,2,5,9").bits()).hasUnset(0).hasSet(1, 2).hasUnset(3,4).hasSet(5).hasUnsetRange(6,8).hasSet(9).hasUnsetRange(10,59); |
||||
assertThat(CronField.parseSeconds("0-4,8-12").bits()).hasSetRange(0, 4).hasUnsetRange(5,7).hasSetRange(8, 12).hasUnsetRange(13,59); |
||||
assertThat(CronField.parseHours("0-23/2").bits()).hasSet(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22).hasUnset(1,3,5,7,9,11,13,15,17,19,21,23); |
||||
assertThat(CronField.parseDaysOfWeek("0").bits()).hasUnsetRange(0, 6).hasSet(7, 7); |
||||
assertThat(CronField.parseSeconds("57/2").bits()).hasUnsetRange(0, 56).hasSet(57).hasUnset(58).hasSet(59); |
||||
} |
||||
|
||||
@Test |
||||
void invalidRange() { |
||||
assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseSeconds("")); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseSeconds("0-12/0")); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseSeconds("60")); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseMinutes("60")); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseDaysOfMonth("0")); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseDaysOfMonth("32")); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseMonth("0")); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseMonth("13")); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseDaysOfWeek("8")); |
||||
assertThatIllegalArgumentException().isThrownBy(() -> CronField.parseSeconds("20-10")); |
||||
} |
||||
|
||||
@Test |
||||
void parseWildcards() { |
||||
assertThat(CronField.parseSeconds("*").bits()).hasSetRange(0, 60); |
||||
assertThat(CronField.parseMinutes("*").bits()).hasSetRange(0, 60); |
||||
assertThat(CronField.parseHours("*").bits()).hasSetRange(0, 23); |
||||
assertThat(CronField.parseDaysOfMonth("*").bits()).hasUnset(0).hasSetRange(1, 31); |
||||
assertThat(CronField.parseDaysOfMonth("?").bits()).hasUnset(0).hasSetRange(1, 31); |
||||
assertThat(CronField.parseMonth("*").bits()).hasUnset(0).hasSetRange(1, 12); |
||||
assertThat(CronField.parseDaysOfWeek("*").bits()).hasUnset(0).hasSetRange(1, 7); |
||||
assertThat(CronField.parseDaysOfWeek("?").bits()).hasUnset(0).hasSetRange(1, 7); |
||||
} |
||||
|
||||
@Test |
||||
void names() { |
||||
assertThat(CronField.parseMonth("JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC").bits()) |
||||
.hasUnset(0).hasSetRange(1, 12); |
||||
assertThat(CronField.parseDaysOfWeek("SUN,MON,TUE,WED,THU,FRI,SAT").bits()) |
||||
.hasUnset(0).hasSetRange(1, 7); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue