Repository: jmrozanec/cron-utils (java)

Issue:
IllegalArgumentException in SingleExecutionTime#lastExecution()
Some unit tests in my application failed today even though I hadn't changed any relevant code. The relevant part of the stack trace:

```
java.lang.IllegalArgumentException: Values must not be empty
	at com.cronutils.utils.Preconditions.checkNotNullNorEmpty(Preconditions.java:235)
	at com.cronutils.model.time.TimeNode.<init>(TimeNode.java:28)
	at com.cronutils.model.time.SingleExecutionTime.generateDayCandidatesUsingDoM(SingleExecutionTime.java:713)
	at com.cronutils.model.time.SingleExecutionTime.generateDays(SingleExecutionTime.java:473)
	at com.cronutils.model.time.SingleExecutionTime.potentialPreviousClosestMatch(SingleExecutionTime.java:305)
	at com.cronutils.model.time.SingleExecutionTime.previousClosestMatch(SingleExecutionTime.java:293)
	at com.cronutils.model.time.SingleExecutionTime.lastExecution(SingleExecutionTime.java:551)
```

The cause is that the logic doesn't correctly handle situations where day-of-month is specified in the Cron expression, and the previous month does not have that day.

I have written a unit test to reproduce this and made a change that fixes it. Will open a PR shortly.

Pull request:
Fix #512 - handle previous month having fewer days

Patch:
diff --git a/src/main/java/com/cronutils/model/time/SingleExecutionTime.java b/src/main/java/com/cronutils/model/time/SingleExecutionTime.java
index afd579f..c10c731 100755
--- a/src/main/java/com/cronutils/model/time/SingleExecutionTime.java
+++ b/src/main/java/com/cronutils/model/time/SingleExecutionTime.java
@@ -477,7 +477,7 @@ public class SingleExecutionTime implements ExecutionTime {
             return generateDaysDoWAndDoMSupported(cronDefinition, date);
         }
         if (cronDefinition.getFieldDefinition(DAY_OF_WEEK) == null) {
-            return Optional.of(generateDayCandidatesUsingDoM(date));
+            return generateDayCandidatesUsingDoM(date);
         }
         return Optional
                 .of(generateDayCandidatesUsingDoW(date, ((DayOfWeekFieldDefinition) cronDefinition.getFieldDefinition(DAY_OF_WEEK)).getMondayDoWValue()));
@@ -710,14 +710,14 @@ public class SingleExecutionTime implements ExecutionTime {
         }
     }
 
-    private TimeNode generateDayCandidatesUsingDoM(final ZonedDateTime reference) {
+    private Optional<TimeNode> generateDayCandidatesUsingDoM(final ZonedDateTime reference) {
         final LocalDate date = LocalDate.of(reference.getYear(), reference.getMonthValue(), 1);
         final int lengthOfMonth = date.lengthOfMonth();
         final List<Integer> candidates = createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, reference.getYear(), reference.getMonthValue())
                 .generateCandidates(1, lengthOfMonth)
                 .stream().distinct().sorted()
                 .collect(Collectors.toList());
-        return new TimeNode(candidates);
+        return candidates.isEmpty() ? Optional.empty() : Optional.of(new TimeNode(candidates));
     }
 
     private TimeNode generateDayCandidatesUsingDoW(final ZonedDateTime reference, final WeekDay mondayDoWValue) {


Test patch:
diff --git a/src/test/java/com/cronutils/Issue512Test.java b/src/test/java/com/cronutils/Issue512Test.java
new file mode 100644
index 0000000..a7fab98
--- /dev/null
+++ b/src/test/java/com/cronutils/Issue512Test.java
@@ -0,0 +1,49 @@
+package com.cronutils;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+import org.junit.jupiter.api.Test;
+
+import com.cronutils.builder.CronBuilder;
+import com.cronutils.model.definition.CronDefinitionBuilder;
+import com.cronutils.model.field.expression.FieldExpressionFactory;
+import com.cronutils.model.time.ExecutionTime;
+
+public class Issue512Test {
+    
+    @Test
+    public void testPreviousMonthIsShorter() {
+        LocalDateTime time = LocalDateTime.of(2022, 3, 30, 9, 55, 0);
+        
+        ExecutionTime execTime = ExecutionTime.forCron(CronBuilder
+                .cron(CronDefinitionBuilder
+                        .defineCron()
+                        .withYear().and()
+                        .withMonth().and()
+                        .withDayOfMonth().and()
+                        .withHours().and()
+                        .withMinutes().and()
+                        .withSeconds().and()
+                        .instance())
+                .withYear(FieldExpressionFactory.on(time.getYear()))
+                .withMonth(FieldExpressionFactory.on(time.getMonthValue()))
+                .withDoM(FieldExpressionFactory.on(time.getDayOfMonth()))
+                .withHour(FieldExpressionFactory.on(time.getHour()))
+                .withMinute(FieldExpressionFactory.on(time.getMinute()))
+                .withSecond(FieldExpressionFactory.on(time.getSecond()))
+                .instance());
+        
+        Optional<ZonedDateTime> result =
+                execTime.lastExecution(ZonedDateTime.of(2022, 3, 30, 9, 54, 0, 0, ZoneId.systemDefault()));
+        
+        assertNotNull(result);
+        assertFalse(result.isPresent());
+    }
+    
+}