Repository: jmrozanec/cron-utils
Pull request #524 (2022-06-24 00:16:25)

Added getting execution count and dates


Patch:
diff --git a/src/main/java/com/cronutils/model/time/ExecutionTime.java b/src/main/java/com/cronutils/model/time/ExecutionTime.java
index 1614796..d67b72c 100644
--- a/src/main/java/com/cronutils/model/time/ExecutionTime.java
+++ b/src/main/java/com/cronutils/model/time/ExecutionTime.java
@@ -21,8 +21,7 @@ import com.cronutils.model.field.CronFieldName;
 
 import java.time.Duration;
 import java.time.ZonedDateTime;
-import java.util.Map;
-import java.util.Optional;
+import java.util.*;
 import java.util.stream.Collectors;
 
 /**
@@ -37,7 +36,7 @@ public interface ExecutionTime {
      * @return ExecutionTime instance
      */
     public static ExecutionTime forCron(final Cron cron) {
-        if(cron instanceof SingleCron){
+        if (cron instanceof SingleCron) {
             final Map<CronFieldName, CronField> fields = cron.retrieveFieldsAsMap();
             final ExecutionTimeBuilder executionTimeBuilder = new ExecutionTimeBuilder(cron);
             for (final CronFieldName name : CronFieldName.values()) {
@@ -73,8 +72,8 @@ public interface ExecutionTime {
                 }
             }
             return executionTimeBuilder.build();
-        }else{
-            return new CompositeExecutionTime(((CompositeCron)cron).getCrons().parallelStream().map(ExecutionTime::forCron).collect(Collectors.toList()));
+        } else {
+            return new CompositeExecutionTime(((CompositeCron) cron).getCrons().parallelStream().map(ExecutionTime::forCron).collect(Collectors.toList()));
         }
 
     }
@@ -93,8 +92,8 @@ public interface ExecutionTime {
      * Due to the question #468 we clarify: crons execute on local instance time.
      * See: https://serverfault.com/questions/791713/what-time-zone-is-a-cron-job-using
      * We ask for a ZonedDateTime for two reasons:
-     *  (i) to provide flexibility on which timezone the cron is being executed
-     *  (ii) to be able to reproduce issues regardless of our own local time (e.g.: daylight savings, etc.)
+     * (i) to provide flexibility on which timezone the cron is being executed
+     * (ii) to be able to reproduce issues regardless of our own local time (e.g.: daylight savings, etc.)
      *
      * @param date - ZonedDateTime instance. If null, a NullPointerException will be raised.
      * @return Duration instance, never null. Time to next execution.
@@ -109,7 +108,7 @@ public interface ExecutionTime {
      * We ask for a ZonedDateTime for two reasons:
      * (i) to provide flexibility on which timezone the cron is being executed
      * (ii) to be able to reproduce issues regardless of our own local time (e.g.: daylight savings, etc.)
-     * 
+     *
      * @param date - ZonedDateTime instance. If null, a NullPointerException will be raised.
      * @return Optional ZonedDateTime instance, never null. Last execution time or empty.
      */
@@ -130,4 +129,43 @@ public interface ExecutionTime {
      * @return true if date matches cron expression requirements, false otherwise.
      */
     boolean isMatch(ZonedDateTime date);
+
+    /**
+     * Provide count of times cron expression would execute between given start and end dates
+     *
+     * @param startDate - Start date. If null, a NullPointerException will be raised.
+     * @param endDate - End date. If null, a NullPointerException will be raised.
+     * @return count of executions
+     */
+    default int countExecutions(ZonedDateTime startDate, ZonedDateTime endDate) {
+        return getExecutionDates(startDate, endDate).size();
+    }
+
+    /**
+     * Provide date times when cron expression would execute between given start and end dates.
+     * End date should be after start date. Otherwise, IllegalArgumentException is raised
+     *
+     * @param startDate - Start date. If null, a NullPointerException will be raised.
+     * @param endDate - End date. If null, a NullPointerException will be raised.
+     * @return list of date times
+     */
+    default List<ZonedDateTime> getExecutionDates(ZonedDateTime startDate, ZonedDateTime endDate) {
+        if (endDate.equals(startDate) || endDate.isBefore(startDate)) {
+            throw new IllegalArgumentException("End date should be after start date");
+        }
+
+        List<ZonedDateTime> executions = new ArrayList<>();
+        ZonedDateTime nextExecutionDate = nextExecution(startDate).orElse(null);
+
+        if (nextExecutionDate == null) return Collections.emptyList();
+        executions.add(nextExecutionDate);
+
+        while (nextExecutionDate.isBefore(endDate)) {
+            nextExecutionDate = nextExecution(nextExecutionDate).orElse(null);
+
+            if (nextExecutionDate == null) break;
+            executions.add(nextExecutionDate);
+        }
+        return executions;
+    }
 }


Test patch:
diff --git a/src/test/java/com/cronutils/model/ExecutionDatesTest.java b/src/test/java/com/cronutils/model/ExecutionDatesTest.java
new file mode 100644
index 0000000..9f4d1cd
--- /dev/null
+++ b/src/test/java/com/cronutils/model/ExecutionDatesTest.java
@@ -0,0 +1,79 @@
+package com.cronutils.model;
+
+import com.cronutils.model.definition.CronDefinitionBuilder;
+import com.cronutils.model.time.ExecutionTime;
+import com.cronutils.parser.CronParser;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class ExecutionDatesTest {
+    private CronParser cron4jCronParser;
+
+    @Before
+    public void setUp() {
+        cron4jCronParser = new CronParser(CronDefinitionBuilder.instanceDefinitionFor(CronType.CRON4J));
+    }
+
+    @Test
+    public void testExecutionCountBetweenDates() {
+        int numOfDays = 10;
+        ZonedDateTime startDate = ZonedDateTime.of(2022, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
+        ZonedDateTime endDate = startDate.plusDays(numOfDays);
+
+        ExecutionTime executionTime = ExecutionTime.forCron(cron4jCronParser.parse("0 * * * *"));
+        int executionCount = executionTime.countExecutions(startDate, endDate);
+        assertEquals(240, executionCount);
+    }
+
+    @Test
+    public void testExecutionDatesBetweenDates() {
+        int year = 2022;
+        int month = 1;
+        int dayOfMonth = 1;
+        int minute = 0;
+        int second = 0;
+        int nanoSecond = 0;
+        ZoneId zoneId = ZoneId.systemDefault();
+
+        int numOfDays = 1;
+        ZonedDateTime startDate = ZonedDateTime.of(year, month, dayOfMonth, 0, minute, second, nanoSecond, zoneId);
+        ZonedDateTime endDate = startDate.plusDays(numOfDays);
+
+        ExecutionTime executionTime = ExecutionTime.forCron(cron4jCronParser.parse("0 * * * *"));
+        List<ZonedDateTime> dates = executionTime.getExecutionDates(startDate, endDate);
+
+        assertEquals(24, dates.size());
+
+        for (int i = 1; i < 24; i++) {
+            ZonedDateTime expectedDate = ZonedDateTime.of(year, month, dayOfMonth, i, minute, second, nanoSecond, zoneId);
+            ZonedDateTime actualDate = dates.get(i - 1);
+            assertEquals(expectedDate, actualDate);
+        }
+    }
+
+    @Test
+    public void throwExceptionWhenEndDateIsBeforeStartDate() {
+        ZonedDateTime startDate = ZonedDateTime.of(2022, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
+        ZonedDateTime endDate = startDate.minusDays(1);
+
+        ExecutionTime executionTime = ExecutionTime.forCron(cron4jCronParser.parse("0 * * * *"));
+        assertThrows(IllegalArgumentException.class, () -> executionTime.getExecutionDates(startDate, endDate));
+    }
+
+    @Test
+    public void throwExceptionWhenEndDateEqualsStarDate() {
+        ZonedDateTime startDate = ZonedDateTime.of(2022, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
+        ZonedDateTime endDate = ZonedDateTime.of(2022, 1, 1, 0, 0, 0, 0, ZoneId.systemDefault());
+
+        ExecutionTime executionTime = ExecutionTime.forCron(cron4jCronParser.parse("0 * * * *"));
+        assertThrows(IllegalArgumentException.class, () -> executionTime.getExecutionDates(startDate, endDate));
+
+    }
+}