Introduce CronExpression

This commit introduces CronExpression, a new for representing cron
expressions, and a direct replacement for CronSequenceGenerator.
This commit is contained in:
Arjen Poutsma
2020-07-15 12:23:26 +02:00
parent c17f2047f6
commit 87c3bb5797
8 changed files with 1227 additions and 15 deletions

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -557,7 +557,7 @@ class CronTriggerTests {
this.calendar.set(Calendar.MONTH, 2);
Date localDate = this.calendar.getTime();
TriggerContext context1 = getTriggerContext(localDate);
assertThatIllegalArgumentException().isThrownBy(() -> trigger.nextExecutionTime(context1));
assertThat(trigger.nextExecutionTime(context1)).isNull();
}
@ParameterizedCronTriggerTest