diff --git a/src/main/java/com/cronutils/builder/CronBuilder.java b/src/main/java/com/cronutils/builder/CronBuilder.java index 3054c61a..4daf5a5e 100644 --- a/src/main/java/com/cronutils/builder/CronBuilder.java +++ b/src/main/java/com/cronutils/builder/CronBuilder.java @@ -83,132 +83,75 @@ public Cron instance() { return new SingleCron(definition, new ArrayList<>(fields.values())).validate(); } + private CronBuilder applyIfPresent( + CronFieldName fieldName, + FieldExpression expression) { - - - - - public static Cron yearly(final CronDefinition definition){ - CronBuilder builder = new CronBuilder(definition); - if(definition.containsFieldDefinition(SECOND)){ - builder = builder.withSecond(new On(new IntegerFieldValue(0))); - } - if(definition.containsFieldDefinition(MINUTE)){ - builder = builder.withMinute(new On(new IntegerFieldValue(0))); - } - if(definition.containsFieldDefinition(HOUR)){ - builder = builder.withHour(new On(new IntegerFieldValue(0))); + if (definition.containsFieldDefinition(fieldName)) { + addField(fieldName, expression); } - if(definition.containsFieldDefinition(DAY_OF_MONTH)){ - builder = builder.withDoM(new On(new IntegerFieldValue(1))); - } - if(definition.containsFieldDefinition(MONTH)){ - builder = builder.withMonth(new On(new IntegerFieldValue(1))); - } - if(definition.containsFieldDefinition(DAY_OF_WEEK)){ - builder = builder.withDoW(FieldExpression.always()); - } - return builder.instance(); + return this; } + public static Cron yearly(final CronDefinition definition) { + return new CronBuilder(definition) + .applyIfPresent(SECOND, new On(new IntegerFieldValue(0))) + .applyIfPresent(MINUTE, new On(new IntegerFieldValue(0))) + .applyIfPresent(HOUR, new On(new IntegerFieldValue(0))) + .applyIfPresent(DAY_OF_MONTH, new On(new IntegerFieldValue(1))) + .applyIfPresent(MONTH, new On(new IntegerFieldValue(1))) + .applyIfPresent(DAY_OF_WEEK, FieldExpression.always()) + .instance(); + } public static Cron annually(final CronDefinition definition){ return yearly(definition); } - public static Cron monthly(final CronDefinition definition){ - CronBuilder builder = new CronBuilder(definition); - if(definition.containsFieldDefinition(SECOND)){ - builder = builder.withSecond(new On(new IntegerFieldValue(0))); - } - if(definition.containsFieldDefinition(MINUTE)){ - builder = builder.withMinute(new On(new IntegerFieldValue(0))); - } - if(definition.containsFieldDefinition(HOUR)){ - builder = builder.withHour(new On(new IntegerFieldValue(0))); - } - if(definition.containsFieldDefinition(DAY_OF_MONTH)){ - builder = builder.withDoM(new On(new IntegerFieldValue(1))); - } - if(definition.containsFieldDefinition(MONTH)){ - builder = builder.withMonth(FieldExpression.always()); - } - if(definition.containsFieldDefinition(DAY_OF_WEEK)){ - builder = builder.withDoW(FieldExpression.always()); - } - return builder.instance(); - } - - public static Cron weekly(final CronDefinition definition){ - CronBuilder builder = new CronBuilder(definition); - if(definition.containsFieldDefinition(SECOND)){ - builder = builder.withSecond(new On(new IntegerFieldValue(0))); - } - if(definition.containsFieldDefinition(MINUTE)){ - builder = builder.withMinute(new On(new IntegerFieldValue(0))); - } - if(definition.containsFieldDefinition(HOUR)){ - builder = builder.withHour(new On(new IntegerFieldValue(0))); - } - if(definition.containsFieldDefinition(DAY_OF_MONTH)){ - builder = builder.withDoM(FieldExpression.always()); - } - if(definition.containsFieldDefinition(MONTH)){ - builder = builder.withMonth(FieldExpression.always()); - } - if(definition.containsFieldDefinition(DAY_OF_WEEK)){ - builder = builder.withDoW(new On(new IntegerFieldValue(0))); - } - return builder.instance(); - } - - public static Cron daily(final CronDefinition definition){ - CronBuilder builder = new CronBuilder(definition); - if(definition.containsFieldDefinition(SECOND)){ - builder = builder.withSecond(new On(new IntegerFieldValue(0))); - } - if(definition.containsFieldDefinition(MINUTE)){ - builder = builder.withMinute(new On(new IntegerFieldValue(0))); - } - if(definition.containsFieldDefinition(HOUR)){ - builder = builder.withHour(new On(new IntegerFieldValue(0))); - } - if(definition.containsFieldDefinition(DAY_OF_MONTH)){ - builder = builder.withDoM(FieldExpression.always()); - } - if(definition.containsFieldDefinition(MONTH)){ - builder = builder.withMonth(FieldExpression.always()); - } - if(definition.containsFieldDefinition(DAY_OF_WEEK)){ - builder = builder.withDoW(FieldExpression.always()); - } - return builder.instance(); + public static Cron monthly(final CronDefinition definition) { + return new CronBuilder(definition) + .applyIfPresent(SECOND, new On(new IntegerFieldValue(0))) + .applyIfPresent(MINUTE, new On(new IntegerFieldValue(0))) + .applyIfPresent(HOUR, new On(new IntegerFieldValue(0))) + .applyIfPresent(DAY_OF_MONTH, new On(new IntegerFieldValue(1))) + .applyIfPresent(MONTH, FieldExpression.always()) + .applyIfPresent(DAY_OF_WEEK, FieldExpression.always()) + .instance(); + } + public static Cron weekly(final CronDefinition definition) { + return new CronBuilder(definition) + .applyIfPresent(SECOND, new On(new IntegerFieldValue(0))) + .applyIfPresent(MINUTE, new On(new IntegerFieldValue(0))) + .applyIfPresent(HOUR, new On(new IntegerFieldValue(0))) + .applyIfPresent(DAY_OF_MONTH, FieldExpression.always()) + .applyIfPresent(MONTH, FieldExpression.always()) + .applyIfPresent(DAY_OF_WEEK, new On(new IntegerFieldValue(0))) + .instance(); + } + + public static Cron daily(final CronDefinition definition) { + return new CronBuilder(definition) + .applyIfPresent(SECOND, new On(new IntegerFieldValue(0))) + .applyIfPresent(MINUTE, new On(new IntegerFieldValue(0))) + .applyIfPresent(HOUR, new On(new IntegerFieldValue(0))) + .applyIfPresent(DAY_OF_MONTH, FieldExpression.always()) + .applyIfPresent(MONTH, FieldExpression.always()) + .applyIfPresent(DAY_OF_WEEK, FieldExpression.always()) + .instance(); } public static Cron midnight(final CronDefinition definition){ return daily(definition); } - public static Cron hourly(final CronDefinition definition){ - CronBuilder builder = new CronBuilder(definition); - if(definition.containsFieldDefinition(SECOND)){ - builder = builder.withSecond(new On(new IntegerFieldValue(0))); - } - if(definition.containsFieldDefinition(MINUTE)){ - builder = builder.withMinute(new On(new IntegerFieldValue(0))); - } - if(definition.containsFieldDefinition(HOUR)){ - builder = builder.withHour(FieldExpression.always()); - } - if(definition.containsFieldDefinition(DAY_OF_MONTH)){ - builder = builder.withDoM(FieldExpression.always()); - } - if(definition.containsFieldDefinition(MONTH)){ - builder = builder.withMonth(FieldExpression.always()); - } - if(definition.containsFieldDefinition(DAY_OF_WEEK)){ - builder = builder.withDoW(FieldExpression.always()); - } - return builder.instance(); + public static Cron hourly(final CronDefinition definition) { + return new CronBuilder(definition) + .applyIfPresent(SECOND, new On(new IntegerFieldValue(0))) + .applyIfPresent(MINUTE, new On(new IntegerFieldValue(0))) + .applyIfPresent(HOUR, FieldExpression.always()) + .applyIfPresent(DAY_OF_MONTH, FieldExpression.always()) + .applyIfPresent(MONTH, FieldExpression.always()) + .applyIfPresent(DAY_OF_WEEK, FieldExpression.always()) + .instance(); } public static Cron reboot(final CronDefinition definition){ diff --git a/src/main/java/com/cronutils/mapper/CronMapper.java b/src/main/java/com/cronutils/mapper/CronMapper.java index 99590b3b..3e9e1b33 100755 --- a/src/main/java/com/cronutils/mapper/CronMapper.java +++ b/src/main/java/com/cronutils/mapper/CronMapper.java @@ -97,54 +97,40 @@ public Cron map(final Cron cron) { * Creates a CronMapper that maps a cron4j expression to a quartz expression. * @return a CronMapper for mapping from cron4j to quartz */ - public static CronMapper fromCron4jToQuartz() { + private static CronMapper build( + final CronType from, + final CronType to, + final Function rules) { + return new CronMapper( - CronDefinitionBuilder.instanceDefinitionFor(CronType.CRON4J), - CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ), - setQuestionMark() + CronDefinitionBuilder.instanceDefinitionFor(from), + CronDefinitionBuilder.instanceDefinitionFor(to), + rules ); } + public static CronMapper fromCron4jToQuartz() { + return build(CronType.CRON4J, CronType.QUARTZ, setQuestionMark()); + } public static CronMapper fromQuartzToCron4j() { - return new CronMapper( - CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ), - CronDefinitionBuilder.instanceDefinitionFor(CronType.CRON4J), - sameCron() - ); + return build(CronType.QUARTZ, CronType.CRON4J, sameCron()); } public static CronMapper fromQuartzToUnix() { - return new CronMapper( - CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ), - CronDefinitionBuilder.instanceDefinitionFor(CronType.UNIX), - sameCron() - ); + return build(CronType.QUARTZ, CronType.UNIX, sameCron()); } public static CronMapper fromUnixToQuartz() { - return new CronMapper( - CronDefinitionBuilder.instanceDefinitionFor(CronType.UNIX), - CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ), - setQuestionMark() - ); + return build(CronType.UNIX, CronType.QUARTZ, setQuestionMark()); } public static CronMapper fromQuartzToSpring() { - return new CronMapper( - CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ), - CronDefinitionBuilder.instanceDefinitionFor(CronType.SPRING), - setQuestionMark() - ); + return build(CronType.QUARTZ, CronType.SPRING, setQuestionMark()); } public static CronMapper fromSpringToQuartz() { - return new CronMapper( - CronDefinitionBuilder.instanceDefinitionFor(CronType.SPRING), - CronDefinitionBuilder.instanceDefinitionFor(CronType.QUARTZ), - setQuestionMark() - ); + return build(CronType.SPRING, CronType.QUARTZ, setQuestionMark()); } - public static CronMapper sameCron(final CronDefinition cronDefinition) { return new CronMapper(cronDefinition, cronDefinition, sameCron()); } @@ -186,44 +172,68 @@ private static Function setQuestionMark() { * @param from - source CronDefinition * @param to - target CronDefinition */ + private void buildMappings(final CronDefinition from, final CronDefinition to) { - final Map sourceFieldDefinitions = getFieldDefinitions(from); - final Map destFieldDefinitions = getFieldDefinitions(to); - boolean startedDestMapping = false; + final Map src = getFieldDefinitions(from); + final Map dest = getFieldDefinitions(to); + + boolean startedDestMapping = false; boolean startedSourceMapping = false; + for (final CronFieldName name : CronFieldName.values()) { - final FieldDefinition destinationFieldDefinition = destFieldDefinitions.get(name); - final FieldDefinition sourceFieldDefinition = sourceFieldDefinitions.get(name); - if (destinationFieldDefinition != null) { - startedDestMapping = true; - } - if (sourceFieldDefinition != null) { - startedSourceMapping = true; - } - if (startedDestMapping && destinationFieldDefinition == null) { - break; - } - //destination has fields before source definition starts. We default them to zero. - if (!startedSourceMapping && destinationFieldDefinition != null) { - mappings.put(name, returnOnZeroExpression(name)); - } - //destination has fields after source definition was processed. We default them to always. - if (startedSourceMapping && sourceFieldDefinition == null && destinationFieldDefinition != null) { - mappings.put(name, returnAlwaysExpression(name)); - } - if (sourceFieldDefinition == null || destinationFieldDefinition == null) { - continue; - } - if (CronFieldName.DAY_OF_WEEK.equals(name)) { - mappings.put(name, dayOfWeekMapping((DayOfWeekFieldDefinition) sourceFieldDefinition, (DayOfWeekFieldDefinition) destinationFieldDefinition)); - } else if (CronFieldName.DAY_OF_MONTH.equals(name)) { - mappings.put(name, dayOfMonthMapping(sourceFieldDefinition, destinationFieldDefinition)); - } else { - mappings.put(name, returnSameExpression()); - } + if (dest.get(name) != null) startedDestMapping = true; + if (src.get(name) != null) startedSourceMapping = true; + if (startedDestMapping && dest.get(name) == null) break; + startedSourceMapping = buildMappingForField( + name, src, dest, startedSourceMapping, startedDestMapping + ); + } + } + + private boolean buildMappingForField( + final CronFieldName name, + final Map src, + final Map dest, + final boolean startedSourceMapping, + final boolean startedDestMapping) { + + final FieldDefinition srcDef = src.get(name); + final FieldDefinition destDef = dest.get(name); + + if (!startedSourceMapping && destDef != null) { + mappings.put(name, returnOnZeroExpression(name)); + return false; + } + + if (startedSourceMapping && srcDef == null && destDef != null) { + mappings.put(name, returnAlwaysExpression(name)); + return true; + } + + if (srcDef == null || destDef == null) return startedSourceMapping; + + mappings.put(name, resolveFieldMapping(name, srcDef, destDef)); + return true; + } + + private static Function resolveFieldMapping( + final CronFieldName name, + final FieldDefinition srcDef, + final FieldDefinition destDef) { + + if (CronFieldName.DAY_OF_WEEK.equals(name)) { + return dayOfWeekMapping( + (DayOfWeekFieldDefinition) srcDef, + (DayOfWeekFieldDefinition) destDef + ); + } + if (CronFieldName.DAY_OF_MONTH.equals(name)) { + return dayOfMonthMapping(srcDef, destDef); } + return returnSameExpression(); } + private Map getFieldDefinitions(final CronDefinition from) { final Map result = new EnumMap<>(CronFieldName.class); @@ -268,38 +278,31 @@ static Function returnAlwaysExpression(final CronFieldName return field -> new CronField(name, always(), FieldConstraintsBuilder.instance().forField(name).createConstraintsInstance()); } - private static IntegerFieldValue mapDayOfWeek(DayOfWeekFieldDefinition sourceDef, DayOfWeekFieldDefinition targetDef, IntegerFieldValue fieldValue) { - return new IntegerFieldValue(ConstantsMapper.weekDayMapping(sourceDef.getMondayDoWValue(), targetDef.getMondayDoWValue(), fieldValue.getValue())); - } - - private static FieldValue mapDayOfWeek(DayOfWeekFieldDefinition sourceDef, DayOfWeekFieldDefinition targetDef, FieldValue fieldValue) { - if (fieldValue instanceof IntegerFieldValue) { - return mapDayOfWeek(sourceDef, targetDef, (IntegerFieldValue) fieldValue); - } - return fieldValue; - } - @VisibleForTesting static Function dayOfWeekMapping(final DayOfWeekFieldDefinition sourceDef, final DayOfWeekFieldDefinition targetDef) { + final DayOfWeekMapping mapping = new DayOfWeekMapping(sourceDef, targetDef); + return field -> { final FieldExpression expression = field.getExpression(); - FieldExpression dest = null; - dest = expression.accept(new FieldExpressionVisitorAdaptor() { + + FieldExpression dest = expression.accept(new FieldExpressionVisitorAdaptor() { @Override public FieldExpression visit(Every every) { return new Every(every.getExpression().accept(this), every.getPeriod()); } - @Override public FieldExpression visit(On on) { - return new On(mapDayOfWeek(sourceDef, targetDef, on.getTime()), on.getSpecialChar(), on.getNth()); - } + return new On(mapping.mapValue(on.getTime()), on.getSpecialChar(), on.getNth()); + } @Override public FieldExpression visit(Between between) { - return new Between(mapDayOfWeek(sourceDef, targetDef, between.getFrom()), mapDayOfWeek(sourceDef, targetDef, between.getTo())); - } + return new Between( + mapping.mapValue(between.getFrom()), + mapping.mapValue(between.getTo()) + ); + } @Override public FieldExpression visit(And and) { And newAnd = new And(); @@ -310,11 +313,13 @@ public FieldExpression visit(And and) { } }); - if (expression instanceof QuestionMark && !targetDef.getConstraints().getSpecialChars().contains(SpecialChar.QUESTION_MARK)) { + if (expression instanceof QuestionMark && !mapping.targetSupportsQuestionMark()) { + // dest = always(); } - return new CronField(CronFieldName.DAY_OF_WEEK, dest, targetDef.getConstraints()); + return new CronField(CronFieldName.DAY_OF_WEEK, dest, mapping.getTargetConstraints()); + // }; } diff --git a/src/main/java/com/cronutils/mapper/DayOfWeekMapping.java b/src/main/java/com/cronutils/mapper/DayOfWeekMapping.java new file mode 100644 index 00000000..acabfa90 --- /dev/null +++ b/src/main/java/com/cronutils/mapper/DayOfWeekMapping.java @@ -0,0 +1,49 @@ +package com.cronutils.mapper; + +import com.cronutils.model.field.CronFieldName; +import com.cronutils.model.field.constraint.FieldConstraints; +import com.cronutils.model.field.definition.DayOfWeekFieldDefinition; +import com.cronutils.model.field.expression.FieldExpression; +import com.cronutils.model.field.value.FieldValue; +import com.cronutils.model.field.value.IntegerFieldValue; +import com.cronutils.model.field.value.SpecialChar; + +public class DayOfWeekMapping { + + private final DayOfWeekFieldDefinition source; + private final DayOfWeekFieldDefinition target; + + public DayOfWeekMapping( + final DayOfWeekFieldDefinition source, + final DayOfWeekFieldDefinition target) { + this.source = source; + this.target = target; + } + + public IntegerFieldValue mapValue(final IntegerFieldValue value) { + return new IntegerFieldValue( + ConstantsMapper.weekDayMapping( + source.getMondayDoWValue(), + target.getMondayDoWValue(), + value.getValue() + ) + ); + } + + public FieldValue mapValue(final FieldValue value) { + if (value instanceof IntegerFieldValue) { + return mapValue((IntegerFieldValue) value); + } + return value; + } + + public FieldConstraints getTargetConstraints() { + return target.getConstraints(); + } + + public boolean targetSupportsQuestionMark() { + return target.getConstraints() + .getSpecialChars() + .contains(SpecialChar.QUESTION_MARK); + } +} \ No newline at end of file diff --git a/src/main/java/com/cronutils/model/time/ExecutionTime.java b/src/main/java/com/cronutils/model/time/ExecutionTime.java index b56a10d3..5e55cda0 100644 --- a/src/main/java/com/cronutils/model/time/ExecutionTime.java +++ b/src/main/java/com/cronutils/model/time/ExecutionTime.java @@ -36,68 +36,59 @@ public interface ExecutionTime { * @return ExecutionTime instance */ public static ExecutionTime forCron(final Cron cron) { - if (cron instanceof SingleCron) { - final Map fields = cron.retrieveFieldsAsMap(); - final ExecutionTimeBuilder executionTimeBuilder = new ExecutionTimeBuilder(cron); - for (final CronFieldName name : CronFieldName.values()) { - if (fields.get(name) != null) { - switch (name) { - case SECOND: - executionTimeBuilder.forSecondsMatching(fields.get(name)); - break; - case MINUTE: - executionTimeBuilder.forMinutesMatching(fields.get(name)); - break; - case HOUR: - executionTimeBuilder.forHoursMatching(fields.get(name)); - break; - case DAY_OF_WEEK: - executionTimeBuilder.forDaysOfWeekMatching(fields.get(name)); - break; - case DAY_OF_MONTH: - executionTimeBuilder.forDaysOfMonthMatching(fields.get(name)); - break; - case MONTH: - executionTimeBuilder.forMonthsMatching(fields.get(name)); - break; - case YEAR: - executionTimeBuilder.forYearsMatching(fields.get(name)); - break; - case DAY_OF_YEAR: - executionTimeBuilder.forDaysOfYearMatching(fields.get(name)); - break; - default: - break; - } + if (cron instanceof SingleCron) return forSingleCron((SingleCron) cron); + if (cron instanceof CompositeCron) return forCompositeCron((CompositeCron) cron); + return emptyExecutionTime(); + } + private static ExecutionTime forSingleCron(final SingleCron cron) { + final Map fields = cron.retrieveFieldsAsMap(); + final ExecutionTimeBuilder executionTimeBuilder = new ExecutionTimeBuilder(cron); + for (final CronFieldName name : CronFieldName.values()) { + final CronField field = fields.get(name); + if (field != null) { + switch (name) { + case SECOND: executionTimeBuilder.forSecondsMatching(field); break; + case MINUTE: executionTimeBuilder.forMinutesMatching(field); break; + case HOUR: executionTimeBuilder.forHoursMatching(field); break; + case DAY_OF_WEEK: executionTimeBuilder.forDaysOfWeekMatching(field); break; + case DAY_OF_MONTH: executionTimeBuilder.forDaysOfMonthMatching(field);break; + case MONTH: executionTimeBuilder.forMonthsMatching(field); break; + case YEAR: executionTimeBuilder.forYearsMatching(field); break; + case DAY_OF_YEAR: executionTimeBuilder.forDaysOfYearMatching(field); break; + default: break; } } - return executionTimeBuilder.build(); - } - if (cron instanceof CompositeCron) { - return new CompositeExecutionTime(((CompositeCron) cron).getCrons().parallelStream().map(ExecutionTime::forCron).collect(Collectors.toList())); } + return executionTimeBuilder.build(); + } + private static ExecutionTime forCompositeCron(final CompositeCron cron) { + return new CompositeExecutionTime( + cron.getCrons() + .parallelStream() + .map(ExecutionTime::forCron) + .collect(Collectors.toList()) + ); + } + + private static ExecutionTime emptyExecutionTime() { return new ExecutionTime() { @Override public Optional nextExecution(ZonedDateTime date) { return Optional.empty(); } - @Override public Optional timeToNextExecution(ZonedDateTime date) { return Optional.empty(); } - @Override public Optional lastExecution(ZonedDateTime date) { return Optional.empty(); } - @Override public Optional timeFromLastExecution(ZonedDateTime date) { return Optional.empty(); } - @Override public boolean isMatch(ZonedDateTime date) { return false; @@ -176,10 +167,16 @@ default int countExecutions(ZonedDateTime startDate, ZonedDateTime endDate) { * @param endDate - End date. If null, a NullPointerException will be raised. * @return list of date times */ - default List getExecutionDates(ZonedDateTime startDate, ZonedDateTime endDate) { + + private void validateDateRange(ZonedDateTime startDate, ZonedDateTime endDate) { if (endDate.equals(startDate) || endDate.isBefore(startDate)) { - throw new IllegalArgumentException("endDate should take place later in time than startDate"); + throw new IllegalArgumentException( + "endDate should take place later in time than startDate"); } + } + + default List getExecutionDates(ZonedDateTime startDate, ZonedDateTime endDate) { + validateDateRange(startDate, endDate); List executions = new ArrayList<>(); ZonedDateTime nextExecutionDate = nextExecution(startDate).orElse(null); diff --git a/src/main/java/com/cronutils/model/time/SingleExecutionTime.java b/src/main/java/com/cronutils/model/time/SingleExecutionTime.java index 2f9b5eb3..037d5b39 100755 --- a/src/main/java/com/cronutils/model/time/SingleExecutionTime.java +++ b/src/main/java/com/cronutils/model/time/SingleExecutionTime.java @@ -56,6 +56,11 @@ public class SingleExecutionTime implements ExecutionTime { private static final LocalTime MAX_SECONDS = LocalTime.MAX.truncatedTo(SECONDS); + private enum Direction { + NEXT, + PREVIOUS + } + private final CronDefinition cronDefinition; private final FieldValueGenerator yearsValueGenerator; private final CronField daysOfWeekCronField; @@ -262,10 +267,13 @@ private ExecutionTimeResult getNextPotentialSecond(final ZonedDateTime date) thr return getNextPotentialValue(date, seconds, ChronoField.SECOND_OF_MINUTE); } - private static ExecutionTimeResult getNextPotentialValue( + private static ExecutionTimeResult getPotentialValue( final ZonedDateTime date, final TimeNode node, - final TemporalField field) throws NoSuchValueException { + final TemporalField field, + final Direction direction) + throws NoSuchValueException { + Set values = new HashSet<>(node.values); TemporalUnit unit = field.getBaseUnit(); @@ -275,19 +283,33 @@ private static ExecutionTimeResult getNextPotentialValue( long range = maximum - minimum; ZonedDateTime newDate = date; + for (long i = 0; i < 2 * range; i++) { - newDate = newDate.plus(1, unit); + + newDate = direction == Direction.NEXT + ? newDate.plus(1, unit) + : newDate.minus(1, unit); if (values.contains(newDate.get(field))) { - newDate = newDate - .truncatedTo(unit); + + if (direction == Direction.NEXT) { + newDate = newDate.truncatedTo(unit); + } else { + newDate = newDate.truncatedTo(unit) + .plus(1, unit) + .minusSeconds(1); + } + return new ExecutionTimeResult(newDate, false); } } throw new NoSuchValueException(); } + private static ExecutionTimeResult getNextPotentialValue(final ZonedDateTime date, final TimeNode node, final TemporalField field) throws NoSuchValueException { + return getPotentialValue(date, node, field, Direction.NEXT); + } private ZonedDateTime toBeginOfNextMonth(final ZonedDateTime datetime) { return datetime.truncatedTo(DAYS).plusMonths(1).withDayOfMonth(1); } @@ -313,53 +335,22 @@ private ZonedDateTime previousClosestMatch(final ZonedDateTime date) throws NoSu } private ExecutionTimeResult potentialPreviousClosestMatch(final ZonedDateTime date) throws NoSuchValueException { - // Get all valid years up to the current year - final List year; - if (cronDefinition.containsFieldDefinition(CronFieldName.YEAR)) { - year = yearsValueGenerator.generateCandidates( - cronDefinition.getFieldDefinition(CronFieldName.YEAR).getConstraints().getStartRange(), - date.getYear() - ).stream().filter(y -> y <= date.getYear()).collect(Collectors.toList()); - } else { - // For cron expressions without a YEAR field (e.g. Unix crons), we use the current year - year = Collections.singletonList(date.getYear()); - } - + final List validYears = getValidYears(date); // For the current date, get the valid days final Optional optionalDays = generateDays(cronDefinition, date); // Get the highest values for each field - final int highestMonth = months.getValues().get(months.getValues().size() - 1); - final int highestHour = hours.getValues().get(hours.getValues().size() - 1); - final int highestMinute = minutes.getValues().get(minutes.getValues().size() - 1); - final int highestSecond = seconds.getValues().get(seconds.getValues().size() - 1); + final int highestMonth = getHighest(months); + final int highestHour = getHighest(hours); + final int highestMinute = getHighest(minutes); + final int highestSecond = getHighest(seconds); // Check each field from largest to smallest - if (!year.contains(date.getYear())) { - Optional validPrevYear = year.stream().filter(y -> y < date.getYear()).max(Integer::compareTo); - if (validPrevYear.isPresent()) { - // When moving to a previous year, we need to check the last valid day in the highest month - ZonedDateTime lastDateOfYear = ZonedDateTime.of( - validPrevYear.get(), highestMonth, - 1, // We'll adjust the day after checking the month's length - highestHour, highestMinute, highestSecond, 0, - date.getZone() - ); - // Get valid days for this date - Optional yearEndDays = generateDays(cronDefinition, lastDateOfYear); - if (yearEndDays.isPresent()) { - int lastValidDay = yearEndDays.get().getValues().get(yearEndDays.get().getValues().size() - 1); - ZonedDateTime result = lastDateOfYear.withDayOfMonth(Math.min(lastValidDay, lastDateOfYear.toLocalDate().lengthOfMonth())); - // If seconds are not part of the cron definition, truncate to minutes - if (!cronDefinition.containsFieldDefinition(CronFieldName.SECOND)) { - result = result.truncatedTo(ChronoUnit.MINUTES); - } - return new ExecutionTimeResult(result, false); - } - } - return getPreviousPotentialYear(date, optionalDays.orElse(null), highestMonth, optionalDays.map(d -> d.getValues().get(d.getValues().size() - 1)).orElse(1), highestHour, highestMinute, highestSecond); + if (!validYears.contains(date.getYear())) { + return handleInvalidYear(date, validYears, optionalDays, highestMonth, highestHour, highestMinute, highestSecond + ); } - + if (!months.getValues().contains(date.getMonthValue())) { return getPreviousPotentialMonth(date, optionalDays.map(d -> d.getValues().get(d.getValues().size() - 1)).orElse(1), highestHour, highestMinute, highestSecond); } @@ -382,13 +373,59 @@ private ExecutionTimeResult potentialPreviousClosestMatch(final ZonedDateTime da return getPreviousPotentialSecond(date); } // If seconds are not part of the cron definition, truncate to minutes - ZonedDateTime result; + return new ExecutionTimeResult(truncateToGranularity(date), true); + } + + private List getValidYears(final ZonedDateTime date) { + if (cronDefinition.containsFieldDefinition(CronFieldName.YEAR)) { + return yearsValueGenerator.generateCandidates( + cronDefinition.getFieldDefinition(CronFieldName.YEAR) + .getConstraints().getStartRange(), + date.getYear() + ).stream() + .filter(y -> y <= date.getYear()) + .collect(Collectors.toList()); + } + return Collections.singletonList(date.getYear()); + } + + private int getHighest(final TimeNode node) { + List values = node.getValues(); + return values.get(values.size() - 1); + } + + private ZonedDateTime truncateToGranularity(final ZonedDateTime date) { if (!cronDefinition.containsFieldDefinition(CronFieldName.SECOND)) { - result = date.truncatedTo(ChronoUnit.MINUTES); - } else { - result = date.truncatedTo(ChronoUnit.SECONDS); + return date.truncatedTo(ChronoUnit.MINUTES); + } + return date.truncatedTo(ChronoUnit.SECONDS); + } + + private ExecutionTimeResult handleInvalidYear( + final ZonedDateTime date, + final List validYears, + final Optional optionalDays, + final int highestMonth, + final int highestHour, + final int highestMinute, + final int highestSecond) throws NoSuchValueException { + + Optional validPrevYear = validYears.stream() + .filter(y -> y < date.getYear()) + .max(Integer::compareTo); + + if (validPrevYear.isPresent()) {ZonedDateTime lastDateOfYear = ZonedDateTime.of(validPrevYear.get(), highestMonth, 1, highestHour, highestMinute, highestSecond, 0, date.getZone() + ); + Optional yearEndDays = generateDays(cronDefinition, lastDateOfYear); + if (yearEndDays.isPresent()) { + int lastValidDay = getHighest(yearEndDays.get()); + ZonedDateTime result = lastDateOfYear.withDayOfMonth( + Math.min(lastValidDay, lastDateOfYear.toLocalDate().lengthOfMonth())); + return new ExecutionTimeResult(truncateToGranularity(result), false); + } } - return new ExecutionTimeResult(result, true); + return getPreviousPotentialYear(date, optionalDays.orElse(null), highestMonth, optionalDays.map(d -> getHighest(d)).orElse(1), highestHour, highestMinute, highestSecond + ); } private ExecutionTimeResult getPreviousPotentialYear(final ZonedDateTime date, final TimeNode days, final int highestMonth, int highestDay, @@ -467,32 +504,9 @@ private ExecutionTimeResult getPreviousPotentialSecond(final ZonedDateTime date) return getPreviousPotentialValue(date, seconds, ChronoField.SECOND_OF_MINUTE); } - private static ExecutionTimeResult getPreviousPotentialValue( - final ZonedDateTime date, - final TimeNode node, - final TemporalField field) throws NoSuchValueException { - Set values = new HashSet<>(node.values); - - TemporalUnit unit = field.getBaseUnit(); - - long maximum = field.range().getMaximum(); - long minimum = field.range().getMinimum(); - long range = maximum - minimum; - - ZonedDateTime newDate = date; - for (long i = 0; i < 2 * range; i++) { - newDate = newDate.minus(1, unit); + private static ExecutionTimeResult getPreviousPotentialValue(final ZonedDateTime date, final TimeNode node, final TemporalField field) throws NoSuchValueException { - if (values.contains(newDate.get(field))) { - newDate = newDate - .truncatedTo(unit) - .plus(1, unit) - .minusSeconds(1); - return new ExecutionTimeResult(newDate, false); - } - } - - throw new NoSuchValueException(); + return getPotentialValue(date, node, field, Direction.PREVIOUS); } private ZonedDateTime toEndOfPreviousMonth(final ZonedDateTime datetime) { @@ -619,65 +633,79 @@ public Optional timeFromLastExecution(final ZonedDateTime date) { * @return true if date matches cron expression requirements, false otherwise. */ public boolean isMatch(ZonedDateTime date) { - // Issue #200: Truncating the date to the least granular precision supported by different cron systems. - // For Quartz, it's seconds while for Unix & Cron4J it's minutes. + date = normalizeDateForMatching(date); + if (date == null) { + return false; + } + return matchesExecutionTime(date); + } + + private ZonedDateTime normalizeDateForMatching(ZonedDateTime date) { final boolean isSecondGranularity = cronDefinition.containsFieldDefinition(SECOND); - // For Quartz-like crons (those with seconds, year, and day of week), we allow nanoseconds - final boolean isQuartzLike = isSecondGranularity && - cronDefinition.containsFieldDefinition(YEAR) && - cronDefinition.containsFieldDefinition(DAY_OF_WEEK); + final boolean isQuartzLike = isSecondGranularity + && cronDefinition.containsFieldDefinition(YEAR) + && cronDefinition.containsFieldDefinition(DAY_OF_WEEK); + if (isSecondGranularity) { if (!isQuartzLike && date.getNano() > 0) { - return false; + return null; } - date = date.truncatedTo(SECONDS); - } else { - // For non-second crons, we require seconds to be 0 - if (date.getSecond() != 0) { - return false; - } - // Check if the minute matches one of our values before checking nanoseconds - ZonedDateTime truncated = date.truncatedTo(ChronoUnit.MINUTES); - boolean matches = dateValuesInExpectedRanges(truncated, truncated); - if (!matches) { - return false; - } - // If we match the minute, then we require nanoseconds to be 0 - if (date.getNano() != 0) { - return false; - } - date = truncated; + return date.truncatedTo(SECONDS); + } + + if (date.getSecond() != 0) { + return null; + } + + ZonedDateTime truncated = date.truncatedTo(ChronoUnit.MINUTES); + + if (!dateValuesInExpectedRanges(truncated, truncated)) { + return null; + } + + if (date.getNano() != 0) { + return null; } + return truncated; + } + + private boolean matchesExecutionTime(ZonedDateTime date) { final Optional last = lastExecution(date); + if (last.isPresent()) { final Optional next = nextExecution(last.get()); + if (next.isPresent()) { return next.get().equals(date); - } else { - boolean everythingInRange = false; - try { - everythingInRange = dateValuesInExpectedRanges(nextClosestMatch(date), date); - } catch (final NoSuchValueException ignored) { - // Why is this ignored? - } - try { - everythingInRange = dateValuesInExpectedRanges(previousClosestMatch(date), date); - } catch (final NoSuchValueException ignored) { - // Why is this ignored? - } - return everythingInRange; } - } else { + + boolean everythingInRange = false; + try { - return dateValuesInExpectedRanges(nextClosestMatch(date.minusSeconds(1)), date); + everythingInRange = + dateValuesInExpectedRanges(nextClosestMatch(date), date); } catch (final NoSuchValueException ignored) { - // Why is this ignored? } + + try { + everythingInRange = + dateValuesInExpectedRanges(previousClosestMatch(date), date); + } catch (final NoSuchValueException ignored) { + } + + return everythingInRange; } - return false; - } + try { + return dateValuesInExpectedRanges( + nextClosestMatch(date.minusSeconds(1)), + date + ); + } catch (final NoSuchValueException ignored) { + return false; + } + } private boolean dateValuesInExpectedRanges(final ZonedDateTime validCronDate, final ZonedDateTime date) { boolean everythingInRange = true; if (cronDefinition.getFieldDefinition(YEAR) != null) { @@ -706,24 +734,28 @@ private boolean dateValuesInExpectedRanges(final ZonedDateTime validCronDate, fi return everythingInRange; } + private List generateSortedDistinctCandidates(List candidates) { + return candidates.stream().distinct().sorted().collect(Collectors.toList()); + } + private List generateDayCandidatesQuestionMarkNotSupportedUsingDoWAndDoM(final int year, final int month, final WeekDay mondayDoWValue) { final LocalDate date = LocalDate.of(year, month, 1); final int lengthOfMonth = date.lengthOfMonth(); if (daysOfMonthCronField.getExpression() instanceof Always && daysOfWeekCronField.getExpression() instanceof Always) { - return createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month) - .generateCandidates(1, lengthOfMonth) - .stream().distinct().sorted() - .collect(Collectors.toList()); + return generateSortedDistinctCandidates( + createDayOfWeekValueGeneratorInstance(daysOfWeekCronField, year, month, mondayDoWValue) + .generateCandidates(1, lengthOfMonth) + ); } else if (daysOfMonthCronField.getExpression() instanceof Always) { - return createDayOfWeekValueGeneratorInstance(daysOfWeekCronField, year, month, mondayDoWValue) - .generateCandidates(1, lengthOfMonth) - .stream().distinct().sorted() - .collect(Collectors.toList()); + return generateSortedDistinctCandidates( + createDayOfWeekValueGeneratorInstance(daysOfWeekCronField, year, month, mondayDoWValue) + .generateCandidates(1, lengthOfMonth) + ); } else if (daysOfWeekCronField.getExpression() instanceof Always) { - return createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month) - .generateCandidates(1, lengthOfMonth) - .stream().distinct().sorted() - .collect(Collectors.toList()); + return generateSortedDistinctCandidates( + createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month) + .generateCandidates(1, lengthOfMonth) + ); } else { final List dayOfWeekCandidates = createDayOfWeekValueGeneratorInstance(daysOfWeekCronField, year, month, mondayDoWValue).generateCandidates(1, lengthOfMonth); @@ -745,22 +777,22 @@ private List generateDayCandidatesQuestionMarkNotSupportedUsingDoWAndDo private List generateDayCandidatesQuestionMarkSupportedUsingDoWAndDoM(final int year, final int month, final WeekDay mondayDoWValue) { final LocalDate date = LocalDate.of(year, month, 1); - final int lengthOfMonth = date.lengthOfMonth(); + final int lengthOfMonth = getLengthOfMonth(year, month); if (daysOfMonthCronField.getExpression() instanceof Always && daysOfWeekCronField.getExpression() instanceof Always) { - return createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month) - .generateCandidates(1, lengthOfMonth) - .stream().distinct().sorted() - .collect(Collectors.toList()); + return generateSortedDistinctCandidates( + createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month) + .generateCandidates(1, lengthOfMonth) + ); } else if (daysOfMonthCronField.getExpression() instanceof QuestionMark) { - return createDayOfWeekValueGeneratorInstance(daysOfWeekCronField, year, month, mondayDoWValue) - .generateCandidates(1, lengthOfMonth) - .stream().distinct().sorted() - .collect(Collectors.toList()); + return generateSortedDistinctCandidates( + createDayOfWeekValueGeneratorInstance(daysOfWeekCronField, year, month, mondayDoWValue) + .generateCandidates(1, lengthOfMonth) + ); } else if (daysOfWeekCronField.getExpression() instanceof QuestionMark) { - return createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month) - .generateCandidates(1, lengthOfMonth) - .stream().distinct().sorted() - .collect(Collectors.toList()); + return generateSortedDistinctCandidates( + createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month) + .generateCandidates(1, lengthOfMonth) + ); } else { Set candidates = new HashSet<>(createDayOfMonthValueGeneratorInstance(daysOfMonthCronField, year, month).generateCandidates(1, lengthOfMonth)); Set daysOfWeek = new HashSet<>(createDayOfWeekValueGeneratorInstance(daysOfWeekCronField, year, month, mondayDoWValue).generateCandidates(1, lengthOfMonth)); @@ -770,6 +802,10 @@ private List generateDayCandidatesQuestionMarkSupportedUsingDoWAndDoM(f } } + private int getLengthOfMonth(int year, int month) { + return LocalDate.of(year, month, 1).lengthOfMonth(); + } + private Optional generateDayCandidatesUsingDoM(final ZonedDateTime reference) { final LocalDate date = LocalDate.of(reference.getYear(), reference.getMonthValue(), 1); final int lengthOfMonth = date.lengthOfMonth(); diff --git a/src/main/java/com/cronutils/parser/CronParser.java b/src/main/java/com/cronutils/parser/CronParser.java index e6052876..ad83e338 100755 --- a/src/main/java/com/cronutils/parser/CronParser.java +++ b/src/main/java/com/cronutils/parser/CronParser.java @@ -97,73 +97,91 @@ public Cron parse(final String expression) { if (StringUtils.isEmpty(replaced)) { throw new IllegalArgumentException("Empty expression!"); } + if (expression.startsWith("@")) return parseNickname(expression); + if (expression.contains("||")) return parseDoubleOrExpression(expression); + if (expression.contains("|")) return parseSingleOrExpression(expression); + return parsePlainExpression(replaced); + } + + private Cron resolveNickname(final String expression, + final Set cronNicknames, + final CronNicknames nickname, + final Cron cron) { + return validateAndReturnSupportedCronNickname( + expression, cronNicknames, nickname, cron); + } + private Cron parseNickname(final String expression) { Set cronNicknames = cronDefinition.getCronNicknames(); - if(expression.startsWith("@")){ - if(cronNicknames.isEmpty()){ - throw new IllegalArgumentException("Nicknames not supported!"); - } - switch (expression){ - case "@yearly": - return validateAndReturnSupportedCronNickname(expression, cronNicknames, CronNicknames.YEARLY, CronBuilder.yearly(cronDefinition)); - case "@annually": - return validateAndReturnSupportedCronNickname(expression, cronNicknames, CronNicknames.ANNUALLY, CronBuilder.annually(cronDefinition)); - case "@monthly": - return validateAndReturnSupportedCronNickname(expression, cronNicknames, CronNicknames.MONTHLY, CronBuilder.monthly(cronDefinition)); - case "@weekly": - return validateAndReturnSupportedCronNickname(expression, cronNicknames, CronNicknames.WEEKLY, CronBuilder.weekly(cronDefinition)); - case "@daily": - return validateAndReturnSupportedCronNickname(expression, cronNicknames, CronNicknames.DAILY, CronBuilder.daily(cronDefinition)); - case "@midnight": - return validateAndReturnSupportedCronNickname(expression, cronNicknames, CronNicknames.MIDNIGHT, CronBuilder.midnight(cronDefinition)); - case "@hourly": - return validateAndReturnSupportedCronNickname(expression, cronNicknames, CronNicknames.HOURLY, CronBuilder.hourly(cronDefinition)); - case "@reboot": - return validateAndReturnSupportedCronNickname(expression, cronNicknames, CronNicknames.REBOOT, CronBuilder.reboot(cronDefinition)); + if (cronNicknames.isEmpty()) { + throw new IllegalArgumentException("Nicknames not supported!"); + } + switch (expression) { + case "@yearly": return resolveNickname(expression, cronNicknames, CronNicknames.YEARLY, CronBuilder.yearly(cronDefinition)); + case "@annually": return resolveNickname(expression, cronNicknames, CronNicknames.ANNUALLY, CronBuilder.annually(cronDefinition)); + case "@monthly": return resolveNickname(expression, cronNicknames, CronNicknames.MONTHLY, CronBuilder.monthly(cronDefinition)); + case "@weekly": return resolveNickname(expression, cronNicknames, CronNicknames.WEEKLY, CronBuilder.weekly(cronDefinition)); + case "@daily": return resolveNickname(expression, cronNicknames, CronNicknames.DAILY, CronBuilder.daily(cronDefinition)); + case "@midnight": return resolveNickname(expression, cronNicknames, CronNicknames.MIDNIGHT, CronBuilder.midnight(cronDefinition)); + case "@hourly": return resolveNickname(expression, cronNicknames, CronNicknames.HOURLY, CronBuilder.hourly(cronDefinition)); + case "@reboot": return resolveNickname(expression, cronNicknames, CronNicknames.REBOOT, CronBuilder.reboot(cronDefinition)); + default: throw new IllegalArgumentException( + String.format("Nickname %s not supported!", expression)); + } + } + + private Cron parseDoubleOrExpression(final String expression) { + List crons = Arrays.stream(expression.split("\\|\\|")) + .map(this::parse) + .collect(Collectors.toList()); + return new CompositeCron(crons); + } + + private Cron parseSingleOrExpression(final String expression) { + int cronscount = Arrays.stream(expression.split("\\s+")) + .mapToInt(s -> s.split("\\|").length) + .max().orElse(0); + + List crons = new ArrayList<>(); + for (int j = 0; j < cronscount; j++) { + StringBuilder builder = new StringBuilder(); + for (String s : expression.split("\\s+")) { + builder.append(String.format("%s ", + s.contains("|") ? s.split("\\|")[j] : s)); } + crons.add(builder.toString().trim()); } + return new CompositeCron(crons.stream().map(this::parse).collect(Collectors.toList())); + } - if(expression.contains("||")) { - List crons = Arrays.stream(expression.split("\\|\\|")).map(this::parse).collect(Collectors.toList()); - return new CompositeCron(crons); + private Cron parsePlainExpression(final String replaced) { + final String[] expressionParts = replaced.toUpperCase().split(" "); + final int expressionLength = expressionParts.length; + + String fieldWithTrailingCommas = Arrays.stream(expressionParts) + .filter(x -> x.endsWith(",")) + .findAny().orElse(null); + if (fieldWithTrailingCommas != null) { + throw new IllegalArgumentException( + String.format("Invalid field value! Trailing commas not permitted! '%s'", + fieldWithTrailingCommas)); } - if(expression.contains("|")){ - List crons = new ArrayList<>(); - int cronscount = Arrays.stream(expression.split("\\s+")).mapToInt(s->s.split("\\|").length).max().orElse(0); - for(int j=0; j x.endsWith(",")).findAny().orElse(null); - if(fieldWithTrailingCommas!=null){ - throw new IllegalArgumentException(String.format("Invalid field value! Trailing commas not permitted! '%s'", fieldWithTrailingCommas)); - } - final List fields = expressions.get(expressionLength); - if (fields == null) { - throw new IllegalArgumentException( - String.format("Cron expression contains %s parts but we expect one of %s", expressionLength, expressions.keySet())); - } - try { - final int size = expressionParts.length; - final List results = new ArrayList<>(size + 1); - for (int j = 0; j < size; j++) { - results.add(fields.get(j).parse(expressionParts[j])); - } - return new SingleCron(cronDefinition, results).validate(); - } catch (final IllegalArgumentException e) { - throw new IllegalArgumentException(String.format("Failed to parse cron expression. %s", e.getMessage()), e); + + final List fields = expressions.get(expressionLength); + if (fields == null) { + throw new IllegalArgumentException( + String.format("Cron expression contains %s parts but we expect one of %s", + expressionLength, expressions.keySet())); + } + try { + final List results = new ArrayList<>(expressionParts.length + 1); + for (int j = 0; j < expressionParts.length; j++) { + results.add(fields.get(j).parse(expressionParts[j])); } + return new SingleCron(cronDefinition, results).validate(); + } catch (final IllegalArgumentException e) { + throw new IllegalArgumentException( + String.format("Failed to parse cron expression. %s", e.getMessage()), e); } } }