Skip to content

Commit 65561b9

Browse files
committed
Add support for LW, W and L to unix cron tab expressions
1 parent a520ae4 commit 65561b9

File tree

5 files changed

+119
-106
lines changed

5 files changed

+119
-106
lines changed

pom.xml

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,13 +92,31 @@
9292
</dependency>
9393
<dependency>
9494
<groupId>org.junit.jupiter</groupId>
95-
<artifactId>junit-jupiter</artifactId>
95+
<artifactId>junit-jupiter-api</artifactId>
9696
<version>${junit.version}</version>
9797
<scope>test</scope>
9898
</dependency>
99+
<dependency>
100+
<groupId>org.junit.jupiter</groupId>
101+
<artifactId>junit-jupiter-engine</artifactId>
102+
<version>${junit.version}</version>
103+
<scope>test</scope>
104+
</dependency>
105+
<dependency>
106+
<groupId>org.junit.jupiter</groupId>
107+
<artifactId>junit-jupiter-params</artifactId>
108+
<version>${junit.version}</version>
109+
<scope>test</scope>
110+
</dependency>
111+
<dependency>
112+
<groupId>org.mockito</groupId>
113+
<artifactId>mockito-core</artifactId>
114+
<version>${mockito.version}</version>
115+
<scope>test</scope>
116+
</dependency>
99117
<dependency>
100118
<groupId>org.mockito</groupId>
101-
<artifactId>mockito-inline</artifactId>
119+
<artifactId>mockito-junit-jupiter</artifactId>
102120
<version>${mockito.version}</version>
103121
<scope>test</scope>
104122
</dependency>

src/main/java/com/cronutils/model/definition/CronDefinitionBuilder.java

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,7 @@ public void register(final FieldDefinition definition) {
240240
* @return returns CronDefinition instance, never null
241241
*/
242242
public CronDefinition instance() {
243-
final Set<CronConstraint> validations = new HashSet<>();
244-
validations.addAll(cronConstraints);
243+
final Set<CronConstraint> validations = new HashSet<>(cronConstraints);
245244
final List<FieldDefinition> values = new ArrayList<>(fields.values());
246245
values.sort(FieldDefinition.createFieldDefinitionComparator());
247246
return new CronDefinition(values, validations, cronNicknames, matchDayOfWeekAndDayOfMonth);
@@ -323,8 +322,8 @@ private static CronDefinition cron4j() {
323322
* </table>
324323
*
325324
* <p>Thus in general Quartz cron expressions are as follows:
326-
*
327-
* <p>S M H DoM M DoW [Y]
325+
* "0 30 17 ? * 7L *"
326+
* <p>S M H DoM M DoW [Y]
328327
*
329328
* @return {@link CronDefinition} instance, never {@code null}
330329
*/
@@ -414,7 +413,7 @@ private static CronDefinition spring() {
414413

415414
/**
416415
* Creates CronDefinition instance matching Spring (v5.2 onwards) specification.
417-
* https://spring.io/blog/2020/11/10/new-in-spring-5-3-improved-cron-expressions
416+
* <a href="https://spring.io/blog/2020/11/10/new-in-spring-5-3-improved-cron-expressions">...</a>
418417
*
419418
* <p>The cron expression is expected to be a string comprised of 6
420419
* fields separated by white space. Fields can contain any of the allowed
@@ -498,9 +497,9 @@ private static CronDefinition unixCrontab() {
498497
return CronDefinitionBuilder.defineCron()
499498
.withMinutes().withValidRange(0, 59).withStrictRange().and()
500499
.withHours().withValidRange(0, 23).withStrictRange().and()
501-
.withDayOfMonth().withValidRange(1, 31).withStrictRange().and()
500+
.withDayOfMonth().withValidRange(1, 31).supportsL().supportsLW().supportsW().withStrictRange().and()
502501
.withMonth().withValidRange(1, 12).withStrictRange().and()
503-
.withDayOfWeek().withValidRange(0, 7).withMondayDoWValue(1).withIntMapping(7, 0).withStrictRange().and()
502+
.withDayOfWeek().withValidRange(0, 7).withMondayDoWValue(1).withIntMapping(7, 0).supportsL().supportsW().withStrictRange().and()
504503
.instance();
505504
}
506505

@@ -511,19 +510,12 @@ private static CronDefinition unixCrontab() {
511510
* @return CronDefinition instance if definition is found; a RuntimeException otherwise.
512511
*/
513512
public static CronDefinition instanceDefinitionFor(final CronType cronType) {
514-
switch (cronType) {
515-
case CRON4J:
516-
return cron4j();
517-
case QUARTZ:
518-
return quartz();
519-
case UNIX:
520-
return unixCrontab();
521-
case SPRING:
522-
return spring();
523-
case SPRING53:
524-
return spring53();
525-
default:
526-
throw new IllegalArgumentException(String.format("No cron definition found for %s", cronType));
527-
}
513+
return switch (cronType) {
514+
case CRON4J -> cron4j();
515+
case QUARTZ -> quartz();
516+
case UNIX -> unixCrontab();
517+
case SPRING -> spring();
518+
case SPRING53 -> spring53();
519+
};
528520
}
529-
}
521+
}

src/test/java/com/cronutils/Issue143Test.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
import static org.junit.jupiter.api.Assertions.assertEquals;
2929
import static org.junit.jupiter.api.Assertions.fail;
3030

31-
public class Issue143Test {
31+
class Issue143Test {
3232

3333
private static final String LAST_EXECUTION_NOT_PRESENT_ERROR = "last execution was not present";
3434
private CronParser parser;
@@ -44,7 +44,7 @@ public void setUp() {
4444
}
4545

4646
@Test
47-
public void testCase1() {
47+
void testCase1() {
4848
ExecutionTime et = ExecutionTime.forCron(parser.parse("0 0 12 31 12 ? *"));
4949
Optional<ZonedDateTime> olast = et.lastExecution(currentDateTime);
5050
ZonedDateTime last = olast.orElse(null);
@@ -55,7 +55,7 @@ public void testCase1() {
5555
}
5656

5757
@Test
58-
public void testCase2() {
58+
void testCase2() {
5959
final ExecutionTime et = ExecutionTime.forCron(parser.parse("0 0 12 ? 12 SAT#5 *"));
6060
final Optional<ZonedDateTime> lastExecution = et.lastExecution(currentDateTime);
6161
if (lastExecution.isPresent()) {
@@ -67,7 +67,7 @@ public void testCase2() {
6767
}
6868

6969
@Test
70-
public void testCase3() {
70+
void testCase3() {
7171
final ExecutionTime et = ExecutionTime.forCron(parser.parse("0 0 12 31 1/1 ? *"));
7272
final Optional<ZonedDateTime> lastExecution = et.lastExecution(currentDateTime);
7373
if (lastExecution.isPresent()) {
@@ -79,7 +79,7 @@ public void testCase3() {
7979
}
8080

8181
@Test
82-
public void testCase4() {
82+
void testCase4() {
8383
final ExecutionTime et = ExecutionTime.forCron(parser.parse("0 0 12 ? 1/1 SAT#5 *"));
8484
final Optional<ZonedDateTime> lastExecution = et.lastExecution(currentDateTime);
8585
if (lastExecution.isPresent()) {

src/test/java/com/cronutils/mapper/CronMapperIntegrationTest.java

Lines changed: 65 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -13,74 +13,83 @@
1313

1414
package com.cronutils.mapper;
1515

16+
import com.cronutils.model.Cron;
1617
import com.cronutils.model.CronType;
18+
import com.cronutils.model.definition.CronDefinition;
1719
import com.cronutils.model.definition.CronDefinitionBuilder;
1820
import com.cronutils.parser.CronParser;
1921
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.Arguments;
24+
import org.junit.jupiter.params.provider.MethodSource;
2025

2126
import java.util.Arrays;
27+
import java.util.stream.Stream;
2228

29+
import static com.cronutils.model.CronType.CRON4J;
30+
import static com.cronutils.model.CronType.QUARTZ;
31+
import static com.cronutils.model.CronType.SPRING;
32+
import static com.cronutils.model.CronType.UNIX;
2333
import static org.junit.jupiter.api.Assertions.assertEquals;
2434
import static org.junit.jupiter.api.Assertions.assertTrue;
2535

26-
public class CronMapperIntegrationTest {
27-
28-
@Test
29-
public void testSpecificTimeCron4jToQuartz() {
30-
assertEquals("0 30 8 10 6 ? *", CronMapper.fromCron4jToQuartz().map(cron4jParser().parse("30 8 10 6 *")).asString());
31-
}
32-
33-
@Test
34-
public void testMoreThanOneInstanceCron4jToQuartz() {
35-
assertEquals("0 0 11,16 * * ? *", CronMapper.fromCron4jToQuartz().map(cron4jParser().parse("0 11,16 * * *")).asString());
36-
}
37-
38-
@Test
39-
public void testRangeOfTimeCron4jToQuartz() {
40-
final String expression = "0 9-18 * * 1-3";
41-
final String expected = "0 0 9-18 ? * 2-4 *";
42-
assertEquals(expected, CronMapper.fromCron4jToQuartz().map(cron4jParser().parse(expression)).asString());
43-
}
44-
45-
@Test
46-
public void testSpecificTimeQuartzToCron4j() {
47-
final String expression = "5 30 8 10 6 ? 1984";
48-
assertEquals("30 8 10 6 *", CronMapper.fromQuartzToCron4j().map(quartzParser().parse(expression)).asString());
49-
}
50-
51-
@Test
52-
public void testMoreThanOneInstanceQuartzToCron4j() {
53-
final String expression = "5 0 11,16 * * ? 1984";
54-
assertEquals("0 11,16 * * *", CronMapper.fromQuartzToCron4j().map(quartzParser().parse(expression)).asString());
55-
}
56-
57-
@Test
58-
public void testRangeOfTimeQuartzToCron4j() {
59-
final String expected = "0 9-18 * * 0-2";
60-
final String expression = "5 0 9-18 ? * 1-3 1984";
61-
assertEquals(expected, CronMapper.fromQuartzToCron4j().map(quartzParser().parse(expression)).asString());
62-
}
63-
64-
@Test
65-
public void testRangeOfTimeQuartzToSpring() {
66-
final String expected = "5 0 9-18 ? * 0-2";
67-
final String expression = "5 0 9-18 ? * 1-3 1984";
68-
assertEquals(expected, CronMapper.fromQuartzToSpring().map(quartzParser().parse(expression)).asString());
36+
class CronMapperIntegrationTest {
37+
static Stream<Arguments> cronExpressions() {
38+
return Stream.of(
39+
Arguments.of(CRON4J, CronMapper.fromCron4jToQuartz(), "0 11,16 * * *", "0 0 11,16 * * ? *"),
40+
Arguments.of(CRON4J, CronMapper.fromCron4jToQuartz(), "0 9-18 * * 1-3", "0 0 9-18 ? * 2-4 *"),
41+
Arguments.of(CRON4J, CronMapper.fromCron4jToQuartz(), "30 8 10 6 *", "0 30 8 10 6 ? *"),
42+
Arguments.of(QUARTZ, CronMapper.fromQuartzToCron4j(), "0 0 0 ? * 5#1", "0 0 * * 4#1"),
43+
Arguments.of(QUARTZ, CronMapper.fromQuartzToCron4j(), "5 0 11,16 * * ? 1984", "0 11,16 * * *"),
44+
Arguments.of(QUARTZ, CronMapper.fromQuartzToCron4j(), "5 0 9-18 ? * 1-3 1984", "0 9-18 * * 0-2"),
45+
Arguments.of(QUARTZ, CronMapper.fromQuartzToCron4j(), "5 30 8 10 6 ? 1984", "30 8 10 6 *"),
46+
Arguments.of(QUARTZ, CronMapper.fromQuartzToSpring(), "0 0 0 ? * 5#1", "0 0 0 ? * 4#1"),
47+
Arguments.of(QUARTZ, CronMapper.fromQuartzToSpring(), "5 0 9-18 ? * 1-3 1984", "5 0 9-18 ? * 0-2"),
48+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 1 ? 1/3 FRI#1 *", "0 1 * 1/3 5#1"),
49+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 2 ? 1/1 SUN#2", "0 2 * 1/1 0#2"),
50+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 8,12 ? * *", "0 8,12 * * *"),
51+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "* * 8 ? * SAT", "* 8 * * 6"),
52+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 22 ? * MON,TUE,WED,THU,FRI *", "0 22 * * 1,2,3,4,5"),
53+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 13 LW * ?", "0 13 LW * *"),
54+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 0 1 1-12 ? *", "0 0 1 1-12 *"),
55+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 0 1 JAN ? 2099", "0 0 1 1 *"),
56+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 0 1,15 * ? *", "0 0 1,15 * *"),
57+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 0 19 1-12 ? *", "0 0 19 1-12 *"),
58+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 0 21 * ? 2020-2025", "0 0 21 * *"),
59+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 0 ? * 3L *", "0 0 * * 2L"),
60+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 0 ? 1/1 MON#2 *", "0 0 * 1/1 1#2"),
61+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 0,12 ? JAN,MAY,OCT * *", "0 0,12 * 1,5,10 *"),
62+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 13 ? 1,4,7,10 6#2", "0 13 * 1,4,7,10 5#2"),
63+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 13 ? MAR,JUN,SEP,DEC 1L", "0 13 * 3,6,9,12 0L"),
64+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 0 L-10 1-12 ? *", "0 0 L-10 1-12 *"),
65+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0/5 * ? 1-12 4#2 *", "0/5 * * 1-12 3#2"),
66+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 1 1W * ?", "0 1 1W * *"),
67+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 13 ? 1,4,7,10 6#2", "0 13 * 1,4,7,10 5#2"),
68+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 15 14,19 ? JAN,MAY,JUL,OCT * *", "15 14,19 * 1,5,7,10 *"),
69+
70+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 0 ? * 5#1", "0 0 * * 4#1"),
71+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 0 0 L-10 1-12 ? *", "0 0 L-10 1-12 *"),
72+
Arguments.of(QUARTZ, CronMapper.fromQuartzToUnix(), "0 30 17 ? * 7L *", "30 17 * * 6L"),
73+
Arguments.of(SPRING, CronMapper.fromSpringToQuartz(), "0 0 0 ? * 5#1", "0 0 0 ? * 6#1 *"),
74+
Arguments.of(UNIX, CronMapper.fromUnixToQuartz(), "* * * * 3,5-6,*/2,2/3,7/4", "0 * * ? * 4,6-7,*/2,3/3,1/4 *"),
75+
Arguments.of(UNIX, CronMapper.fromUnixToQuartz(), "0 0 * * 1", "0 0 0 ? * 2 *")
76+
);
6977
}
7078

71-
@Test
72-
public void testDaysOfWeekUnixToQuartz() {
73-
final String input = "* * * * 3,5-6,*/2,2/3,7/4";
74-
final String expected = "0 * * ? * 4,6-7,*/2,3/3,1/4 *";
75-
assertEquals(expected, CronMapper.fromUnixToQuartz().map(unixParser().parse(input)).asString());
79+
@ParameterizedTest
80+
@MethodSource("cronExpressions")
81+
void testCronMapping(CronType cronType, CronMapper mapper, String quartzExpression, String expectedExpression) {
82+
Cron sourceCron = getCron(cronType, quartzExpression);
83+
String actualCron = mapper.map(sourceCron).asString();
84+
assertEquals(expectedExpression, actualCron, String.format("Expected [%s] but got [%s]", expectedExpression, actualCron));
7685
}
7786

7887
/**
7988
* Issue #36, #56: Unix to Quartz not accurately mapping every minute pattern
8089
* or patterns that involve every day of month and every day of week.
8190
*/
8291
@Test
83-
public void testEveryMinuteUnixToQuartz() {
92+
void testEveryMinuteUnixToQuartz() {
8493
final String input = "* * * * *";
8594
final String expected1 = "0 * * * * ? *";
8695
final String expected2 = "0 * * ? * * *";
@@ -96,22 +105,20 @@ public void testEveryMinuteUnixToQuartz() {
96105
* or patterns that involve every day of month and every day of week.
97106
*/
98107
@Test
99-
public void testUnixToQuartzQuestionMarkRequired() {
108+
void testUnixToQuartzQuestionMarkRequired() {
100109
final String input = "0 0 * * 1";
101110
final String expected = "0 0 0 ? * 2 *";
102111
final String mapping = CronMapper.fromUnixToQuartz().map(unixParser().parse(input)).asString();
103112
assertEquals(expected, mapping, String.format("Expected [%s] but got [%s]", expected, mapping));
104113
}
105114

106-
private CronParser cron4jParser() {
107-
return new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.CRON4J));
108-
}
109-
110-
private CronParser quartzParser() {
111-
return new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ));
115+
private CronParser unixParser() {
116+
return new CronParser(CronDefinitionBuilder.instanceDefinitionFor(UNIX));
112117
}
113118

114-
private CronParser unixParser() {
115-
return new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.UNIX));
119+
private Cron getCron(CronType cronType, String quartzExpression) {
120+
final CronDefinition cronDefinition = CronDefinitionBuilder.instanceDefinitionFor(cronType);
121+
final CronParser parser = new CronParser(cronDefinition);
122+
return parser.parse(quartzExpression);
116123
}
117124
}

src/test/java/com/cronutils/mapper/CronMapperTest.java

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,56 +19,52 @@
1919
import com.cronutils.model.field.CronFieldName;
2020
import com.cronutils.model.field.expression.Always;
2121
import com.cronutils.model.field.expression.On;
22-
import org.junit.jupiter.api.BeforeEach;
2322
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.api.extension.ExtendWith;
2424
import org.mockito.Mock;
25-
import org.mockito.MockitoAnnotations;
25+
import org.mockito.junit.jupiter.MockitoExtension;
2626

2727
import static org.junit.jupiter.api.Assertions.assertEquals;
2828
import static org.junit.jupiter.api.Assertions.assertThrows;
2929
import static org.mockito.Mockito.mock;
3030

31-
public class CronMapperTest {
32-
private CronFieldName testCronFieldName;
31+
@ExtendWith(MockitoExtension.class)
32+
class CronMapperTest {
33+
private static final CronFieldName TEST_CRON_FIELD_NAME = CronFieldName.SECOND;
34+
3335
@Mock
3436
private CronField mockCronField;
3537

36-
@BeforeEach
37-
public void setUp() {
38-
MockitoAnnotations.initMocks(this);
39-
testCronFieldName = CronFieldName.SECOND;
40-
}
41-
4238
@Test
43-
public void testConstructorSourceDefinitionNull() {
39+
void testConstructorSourceDefinitionNull() {
4440
assertThrows(NullPointerException.class, () -> new CronMapper(mock(CronDefinition.class), null, null));
4541
}
4642

4743
@Test
48-
public void testConstructorTargetDefinitionNull() {
44+
void testConstructorTargetDefinitionNull() {
4945
assertThrows(NullPointerException.class, () -> new CronMapper(null, mock(CronDefinition.class), null));
5046
}
5147

5248
@Test
53-
public void testReturnSameExpression() {
49+
void testReturnSameExpression() {
5450
final Function<CronField, CronField> function = CronMapper.returnSameExpression();
5551
assertEquals(mockCronField, function.apply(mockCronField));
5652
}
5753

5854
@Test
59-
public void testReturnOnZeroExpression() {
60-
final Function<CronField, CronField> function = CronMapper.returnOnZeroExpression(testCronFieldName);
55+
void testReturnOnZeroExpression() {
56+
final Function<CronField, CronField> function = CronMapper.returnOnZeroExpression(TEST_CRON_FIELD_NAME);
6157

62-
assertEquals(testCronFieldName, function.apply(mockCronField).getField());
58+
assertEquals(TEST_CRON_FIELD_NAME, function.apply(mockCronField).getField());
6359
final On result = (On) function.apply(mockCronField).getExpression();
6460
assertEquals(0, (int) result.getTime().getValue());
6561
}
6662

6763
@Test
68-
public void testReturnAlwaysExpression() {
69-
final Function<CronField, CronField> function = CronMapper.returnAlwaysExpression(testCronFieldName);
64+
void testReturnAlwaysExpression() {
65+
final Function<CronField, CronField> function = CronMapper.returnAlwaysExpression(TEST_CRON_FIELD_NAME);
7066

71-
assertEquals(testCronFieldName, function.apply(mockCronField).getField());
67+
assertEquals(TEST_CRON_FIELD_NAME, function.apply(mockCronField).getField());
7268
assertEquals(Always.class, function.apply(mockCronField).getExpression().getClass());
7369
}
7470
}

0 commit comments

Comments
 (0)