Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,31 @@
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,7 @@ public void register(final FieldDefinition definition) {
* @return returns CronDefinition instance, never null
*/
public CronDefinition instance() {
final Set<CronConstraint> validations = new HashSet<>();
validations.addAll(cronConstraints);
final Set<CronConstraint> validations = new HashSet<>(cronConstraints);
final List<FieldDefinition> values = new ArrayList<>(fields.values());
values.sort(FieldDefinition.createFieldDefinitionComparator());
return new CronDefinition(values, validations, cronNicknames, matchDayOfWeekAndDayOfMonth);
Expand Down Expand Up @@ -323,8 +322,8 @@ private static CronDefinition cron4j() {
* </table>
*
* <p>Thus in general Quartz cron expressions are as follows:
*
* <p>S M H DoM M DoW [Y]
* "0 30 17 ? * 7L *"
* <p>S M H DoM M DoW [Y]
*
* @return {@link CronDefinition} instance, never {@code null}
*/
Expand Down Expand Up @@ -414,7 +413,7 @@ private static CronDefinition spring() {

/**
* Creates CronDefinition instance matching Spring (v5.2 onwards) specification.
* https://spring.io/blog/2020/11/10/new-in-spring-5-3-improved-cron-expressions
* <a href="https://spring.io/blog/2020/11/10/new-in-spring-5-3-improved-cron-expressions">...</a>
*
* <p>The cron expression is expected to be a string comprised of 6
* fields separated by white space. Fields can contain any of the allowed
Expand Down Expand Up @@ -492,15 +491,20 @@ private static CronDefinition spring53() {
/**
* Creates CronDefinition instance matching unix crontab specification.
*
* <p>Beyond the POSIX crontab syntax, day of month additionally accepts {@code L}, {@code L-n},
* {@code nW} and {@code LW}, and day of week additionally accepts {@code L} and {@code nL}.
*
* <p>M H DoM M DoW
*
* @return CronDefinition instance, never null;
*/
private static CronDefinition unixCrontab() {
return CronDefinitionBuilder.defineCron()
.withMinutes().withValidRange(0, 59).withStrictRange().and()
.withHours().withValidRange(0, 23).withStrictRange().and()
.withDayOfMonth().withValidRange(1, 31).withStrictRange().and()
.withDayOfMonth().withValidRange(1, 31).supportsL().supportsLW().supportsW().withStrictRange().and()
.withMonth().withValidRange(1, 12).withStrictRange().and()
.withDayOfWeek().withValidRange(0, 7).withMondayDoWValue(1).withIntMapping(7, 0).withStrictRange().and()
.withDayOfWeek().withValidRange(0, 7).withMondayDoWValue(1).withIntMapping(7, 0).supportsL().withStrictRange().and()
.instance();
}

Expand All @@ -511,19 +515,12 @@ private static CronDefinition unixCrontab() {
* @return CronDefinition instance if definition is found; a RuntimeException otherwise.
*/
public static CronDefinition instanceDefinitionFor(final CronType cronType) {
switch (cronType) {
case CRON4J:
return cron4j();
case QUARTZ:
return quartz();
case UNIX:
return unixCrontab();
case SPRING:
return spring();
case SPRING53:
return spring53();
default:
throw new IllegalArgumentException(String.format("No cron definition found for %s", cronType));
}
return switch (cronType) {
case CRON4J -> cron4j();
case QUARTZ -> quartz();
case UNIX -> unixCrontab();
case SPRING -> spring();
case SPRING53 -> spring53();
};
}
}
}
10 changes: 5 additions & 5 deletions src/test/java/com/cronutils/Issue143Test.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;

public class Issue143Test {
class Issue143Test {

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

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

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

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

@Test
public void testCase4() {
void testCase4() {
final ExecutionTime et = ExecutionTime.forCron(parser.parse("0 0 12 ? 1/1 SAT#5 *"));
final Optional<ZonedDateTime> lastExecution = et.lastExecution(currentDateTime);
if (lastExecution.isPresent()) {
Expand Down
130 changes: 130 additions & 0 deletions src/test/java/com/cronutils/Issue670Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright 2015 jmrozanec
* 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
* http://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 com.cronutils;

import com.cronutils.descriptor.CronDescriptor;
import com.cronutils.model.Cron;
import com.cronutils.model.CronType;
import com.cronutils.model.definition.CronDefinitionBuilder;
import com.cronutils.model.time.ExecutionTime;
import com.cronutils.parser.CronParser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;

import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Locale;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Unix crontab support for the L, LW and W special characters.
*/
class Issue670Test {

private final CronParser parser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.UNIX));
private final CronDescriptor descriptor = CronDescriptor.instance(Locale.UK);

@ParameterizedTest
@ValueSource(strings = {
"0 0 L * *",
"0 0 L-3 * *",
"0 0 LW * *",
"0 13 LW * *",
"0 0 1W * *",
"0 0 15W * *",
"0 0 L,15 * *",
"0 0 * * L",
"0 0 * * 0L",
"0 0 * * 6L",
"0 0 LW * 6L"
})
void unixParserAcceptsSpecialChars(String expression) {
final Cron cron = parser.parse(expression);
cron.validate();
assertEquals(expression, cron.asString());
}

/**
* W is a day-of-month modifier ("nearest weekday to the nth"); it has no day-of-week meaning
* and no value generator implements it there, so it must not parse.
*/
@ParameterizedTest
@ValueSource(strings = {"0 0 * * 5W", "0 0 * * 0W", "0 0 * * 1W", "0 0 * * LW", "0 0 * * 1-5W"})
void unixParserRejectsWInDayOfWeek(String expression) {
assertThrows(IllegalArgumentException.class, () -> parser.parse(expression));
}

@Test
void unixParserRejectsBareWInDayOfMonth() {
assertThrows(IllegalArgumentException.class, () -> parser.parse("0 0 W * *"));
}

@ParameterizedTest
@CsvSource({
// June 2025: 1st is a Sunday, 14th a Saturday, 30th a Monday
"0 0 L * *, 2025-06-30",
"0 0 L-3 * *, 2025-06-27",
"0 0 LW * *, 2025-06-30",
"0 0 1W * *, 2025-06-02",
"0 0 14W * *, 2025-06-13",
"0 0 15W * *, 2025-06-16",
"0 0 * * 1L, 2025-06-30",
"0 0 * * 6L, 2025-06-28",
"0 0 * * 0L, 2025-06-29",
// November 2025: 1st is a Saturday, so the nearest weekday rolls forward to Monday the 3rd
"0 0 1W 11 *, 2025-11-03",
// February 2026: last day is Saturday the 28th, so LW is Friday the 27th
"0 0 LW 2 *, 2026-02-27"
})
void nextExecutionMatchesSpecialChar(String expression, String expectedDate) {
final ZonedDateTime from = ZonedDateTime.of(2025, 6, 1, 0, 0, 0, 0, ZoneOffset.UTC);
final ExecutionTime executionTime = ExecutionTime.forCron(parser.parse(expression));
final ZonedDateTime next = executionTime.nextExecution(from)
.orElseThrow(() -> new AssertionError(String.format("no next execution for [%s]", expression)));

assertEquals(LocalDate.parse(expectedDate), next.toLocalDate());
assertTrue(executionTime.isMatch(next), String.format("[%s] should match its own next execution", expression));
}

/**
* Unix day-of-month and day-of-week are OR'ed when both are restricted.
*/
@Test
void dayOfMonthAndDayOfWeekAreOred() {
final ExecutionTime executionTime = ExecutionTime.forCron(parser.parse("0 0 LW * 6L"));
final ZonedDateTime lastSaturday = ZonedDateTime.of(2025, 6, 28, 0, 0, 0, 0, ZoneOffset.UTC);
final ZonedDateTime lastWeekday = ZonedDateTime.of(2025, 6, 30, 0, 0, 0, 0, ZoneOffset.UTC);

assertTrue(executionTime.isMatch(lastSaturday), "last Saturday should match");
assertTrue(executionTime.isMatch(lastWeekday), "last weekday should match");
}

@ParameterizedTest
@CsvSource({
"0 0 L * *, at 00:00 last day of month",
"0 0 L-10 * *, at 00:00 10 days before the last day of the month",
"0 0 LW * *, at 00:00 last weekday of month",
"0 0 1W * *, at 00:00 the nearest weekday to the 1 of the month",
"0 0 * * 6L, at 00:00 last Saturday of every month"
})
void descriptorDescribesSpecialChars(String expression, String expectedDescription) {
assertEquals(expectedDescription, descriptor.describe(parser.parse(expression)));
}
}
Loading
Loading