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
2 changes: 1 addition & 1 deletion src/main/java/com/cronutils/descriptor/CronDescriptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ private String addExpressions(final String description, final String singular, f
*/
public String describeYear(final Map<CronFieldName, CronField> fields) {
final String description =
DescriptionStrategyFactory.plainInstance(
DescriptionStrategyFactory.yearsInstance(
resourceBundle,
fields.containsKey(CronFieldName.YEAR) ? fields.get(CronFieldName.YEAR).getExpression() : null
).describe();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ abstract class DescriptionStrategy {
private static final String WHITE_SPACE = " ";
protected Function<Integer, String> nominalValueFunction;
protected ResourceBundle bundle;
// true when a value already names its own unit ("October", "Tuesday"), so the unit word is redundant
protected boolean selfDescribingValues;

public DescriptionStrategy(final ResourceBundle bundle) {
this.bundle = bundle;
Expand Down Expand Up @@ -169,7 +171,11 @@ protected String describe(final Every every, final boolean and) {
}
if (every.getExpression() instanceof On) {
final On on = (On) every.getExpression();
description += bundle.getString("from") + " %s " + nominalValue(on.getTime());
// "every 1 unit starting at 0" is just "every unit"; 0 is out of range for the 1-based fields
if (every.getPeriod().getValue() == 1 && on.getTime().getValue() == 0) {
return description;
}
description += bundle.getString("from") + (selfDescribingValues ? WHITE_SPACE : " %s ") + nominalValue(on.getTime());
}
return description;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,62 @@
import com.cronutils.Function;
import com.cronutils.model.field.definition.DayOfWeekFieldDefinition;
import com.cronutils.model.field.definition.FieldDefinition;
import com.cronutils.model.field.expression.And;
import com.cronutils.model.field.expression.Every;
import com.cronutils.model.field.expression.FieldExpression;
import com.cronutils.model.field.expression.On;
import com.cronutils.model.field.value.SpecialChar;

import java.text.MessageFormat;
import java.time.DayOfWeek;
import java.time.Month;
import java.time.format.TextStyle;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;

class DescriptionStrategyFactory {

private DescriptionStrategyFactory() {
}

/**
* Names the position an nth day of week occupies, falling back to the plain number for
* positions no bundle spells out.
*/
private static String ordinal(final int nth, final ResourceBundle bundle) {
final String key = "nth_" + nth;
return bundle.containsKey(key) ? bundle.getString(key) : String.valueOf(nth);
}

/**
* Joins a list of plain On values as "a, b and c", so a list can reuse the same phrasing
* as the single value case.
*
* @return the joined values, or null if the expression is not a list of at least two plain On values
*/
private static String joinPlainValues(final FieldExpression expression, final Function<Integer, String> nominal, final ResourceBundle bundle) {
if (!(expression instanceof And)) {
return null;
}
final List<String> values = new ArrayList<>();
for (final FieldExpression each : ((And) expression).getExpressions()) {
if (!(each instanceof On) || ((On) each).getSpecialChar().getValue() != SpecialChar.NONE) {
return null;
}
values.add(nominal.apply(((On) each).getTime().getValue()));
}
if (values.size() < 2) {
return null;
}
final String last = values.remove(values.size() - 1);
return String.join(", ", values) + " " + bundle.getString("and") + " " + last;
}

private static boolean isPlainOn(final FieldExpression expression) {
return expression instanceof On && ((On) expression).getSpecialChar().getValue() == SpecialChar.NONE;
}

/**
* Creates description strategy for days of week.
*
Expand All @@ -47,21 +88,25 @@ public static DescriptionStrategy daysOfWeekInstance(final ResourceBundle bundle
return DayOfWeek.of(integer + diff < 1 ? 7 : integer + diff).getDisplayName(TextStyle.FULL, bundle.getLocale());
};

final NominalDescriptionStrategy dow = new NominalDescriptionStrategy(bundle, nominal, expression);
final NominalDescriptionStrategy dow = new NominalDescriptionStrategy(bundle, nominal, expression).withSelfDescribingValues();

dow.addDescription(fieldExpression -> {
if (fieldExpression instanceof On) {
final On on = (On) fieldExpression;
switch (on.getSpecialChar().getValue()) {
case HASH:
return String.format("%s %s %s ", nominal.apply(on.getTime().getValue()), on.getNth(), bundle.getString("of_every_month"));
return MessageFormat.format(bundle.getString("on_nth_day_of_week_x"),
ordinal(on.getNth().getValue(), bundle), nominal.apply(on.getTime().getValue()));
case L:
return String.format("%s %s %s ", bundle.getString("last"), nominal.apply(on.getTime().getValue()), bundle.getString("of_every_month"));
return MessageFormat.format(bundle.getString("on_last_day_of_week_x"), nominal.apply(on.getTime().getValue()));
case NONE:
return MessageFormat.format(bundle.getString("on_day_of_week_x"), nominal.apply(on.getTime().getValue()));
default:
return "";
}
}
return "";
final String joined = joinPlainValues(fieldExpression, nominal, bundle);
return joined == null ? "" : MessageFormat.format(bundle.getString("on_day_of_week_x"), joined);
});
return dow;
}
Expand Down Expand Up @@ -94,11 +139,14 @@ public static DescriptionStrategy daysOfMonthInstance(final ResourceBundle bundl
}
case LW:
return bundle.getString("last_weekday_of_month");
case NONE:
return MessageFormat.format(bundle.getString("on_day_x"), on.getTime().getValue());
default:
return "";
}
}
return "";
final String joined = joinPlainValues(fieldExpression, Object::toString, bundle);
return joined == null ? "" : MessageFormat.format(bundle.getString("on_days_x"), joined);
});
return dom;
}
Expand All @@ -111,13 +159,40 @@ public static DescriptionStrategy daysOfMonthInstance(final ResourceBundle bundl
* @return - DescriptionStrategy instance, never null
*/
public static DescriptionStrategy monthsInstance(final ResourceBundle bundle, final FieldExpression expression) {
Function<Integer, String> mappingFunction;
if (expression instanceof Every) {
mappingFunction = Object::toString;
} else {
mappingFunction = integer -> Month.of(integer).getDisplayName(TextStyle.FULL, bundle.getLocale());
}
return new NominalDescriptionStrategy(bundle, mappingFunction, expression);
final Function<Integer, String> nominal = integer -> Month.of(integer).getDisplayName(TextStyle.FULL, bundle.getLocale());
final NominalDescriptionStrategy months =
new NominalDescriptionStrategy(bundle, nominal, expression).withSelfDescribingValues();

months.addDescription(fieldExpression -> {
if (isPlainOn(fieldExpression)) {
return MessageFormat.format(bundle.getString("in_month_x"), nominal.apply(((On) fieldExpression).getTime().getValue()));
}
final String joined = joinPlainValues(fieldExpression, nominal, bundle);
return joined == null ? "" : MessageFormat.format(bundle.getString("in_month_x"), joined);
});
return months;
}

/**
* Creates description strategy for years.
*
* @param bundle - locale
* @param expression - CronFieldExpression
* @return - DescriptionStrategy instance, never null
*/
public static DescriptionStrategy yearsInstance(final ResourceBundle bundle, final FieldExpression expression) {
final NominalDescriptionStrategy years =
new NominalDescriptionStrategy(bundle, null, expression).withSelfDescribingValues();

years.addDescription(fieldExpression -> {
// as Strings, so MessageFormat does not group four digit years into "2,005"
if (isPlainOn(fieldExpression)) {
return MessageFormat.format(bundle.getString("in_year_x"), String.valueOf(((On) fieldExpression).getTime().getValue()));
}
final String joined = joinPlainValues(fieldExpression, String::valueOf, bundle);
return joined == null ? "" : MessageFormat.format(bundle.getString("in_year_x"), joined);
});
return years;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import com.cronutils.Function;
import com.cronutils.model.field.expression.FieldExpression;

import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.ResourceBundle;
import java.util.Set;

Expand All @@ -41,7 +41,7 @@ class NominalDescriptionStrategy extends DescriptionStrategy {
*/
public NominalDescriptionStrategy(final ResourceBundle bundle, final Function<Integer, String> nominalValueFunction, final FieldExpression expression) {
super(bundle);
descriptions = new HashSet<>();
descriptions = new LinkedHashSet<>();
if (nominalValueFunction != null) {
this.nominalValueFunction = nominalValueFunction;
}
Expand Down Expand Up @@ -74,4 +74,14 @@ public NominalDescriptionStrategy addDescription(final Function<FieldExpression,
descriptions.add(desc);
return this;
}

/**
* Marks the described values as naming their own unit, so the unit word is left out.
*
* @return NominalDescriptionStrategy, this instance
*/
public NominalDescriptionStrategy withSelfDescribingValues() {
selfDescribingValues = true;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import com.cronutils.utils.Preconditions;
import com.cronutils.utils.StringUtils;

import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.ResourceBundle;
import java.util.Set;

Expand Down Expand Up @@ -59,7 +59,7 @@ class TimeDescriptionStrategy extends DescriptionStrategy {
this.hours = ensureInstance(hours, always());
this.minutes = ensureInstance(minutes, always());
this.seconds = ensureInstance(seconds, new On(new IntegerFieldValue(DEFAULTSECONDS)));
descriptions = new HashSet<>();
descriptions = new LinkedHashSet<>();
registerFunctions();
}

Expand All @@ -85,20 +85,23 @@ private FieldExpression ensureInstance(final FieldExpression expression, final F
public String describe() {
final TimeFields fields = new TimeFields(hours, minutes, seconds);
for (final Function<TimeFields, String> function : descriptions) {
if (!"".equals(function.apply(fields))) {
return function.apply(fields);
final String description = function.apply(fields);
if (!"".equals(description)) {
return description;
}
}
String secondsDesc = "";
String minutesDesc = "";
String hoursDesc = "";
// minute 0 may only stay implicit while seconds are too, else we claim an unbounded sub-minute frequency
final boolean defaultSeconds = seconds instanceof On && isDefault((On) seconds);
if (!(hours instanceof Always)) {
hoursDesc = addTimeExpressions(describe(hours), bundle.getString(HOUR), bundle.getString("hours"));
}
if (!(minutes instanceof On && isDefault((On) minutes)) && !((minutes instanceof Always) && (hours instanceof Always))) {
if (!(minutes instanceof On && isDefault((On) minutes) && defaultSeconds) && !((minutes instanceof Always) && (hours instanceof Always))) {
minutesDesc = addTimeExpressions(describe(minutes), bundle.getString(MINUTE), bundle.getString("minutes"));
}
if (!(seconds instanceof On && isDefault((On) seconds))) {
if (!defaultSeconds) {
secondsDesc = addTimeExpressions(describe(seconds), bundle.getString(SECOND), bundle.getString("seconds"));
}
return String.format("%s %s %s", secondsDesc, minutesDesc, hoursDesc);
Expand Down
12 changes: 12 additions & 0 deletions src/main/resources/com/cronutils/CronUtilsI18N.properties
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ months=months
year=year
years=years
between=between
in_month_x=in {0}
in_year_x=in {0}
on_day_x=on day {0}
on_days_x=on days {0}
on_day_of_week_x=on {0}
on_nth_day_of_week_x=on the {0} {1} of the month
on_last_day_of_week_x=on the last {0} of the month
nth_1=first
nth_2=second
nth_3=third
nth_4=fourth
nth_5=fifth
of_every_month=of every month
of_the_month=of the month
last=last
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ public void noDateTimeExceptionIsThrownGeneratingNextExecutionWithDayOfWeekFilte
public void descriptionForExpressionTellsWrongDoW() {
final CronDescriptor descriptor = CronDescriptor.instance();
final Cron quartzCron = parser.parse("0 0 8 ? * SUN *");
assertEquals("at 08:00 at Sunday day", descriptor.describe(quartzCron));
assertEquals("at 08:00 on Sunday", descriptor.describe(quartzCron));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public void testEveryDayFireAtTenFifteen() {

@Test
public void testEveryDayFireAtTenFifteenYear2005() {
assertExpression("0 15 10 * * ? 2005", "at 10:15 at 2005 year");
assertExpression("0 15 10 * * ? 2005", "at 10:15 in 2005");
}

@Test
Expand All @@ -107,7 +107,7 @@ public void testEveryFiveDaysStartingOnDay3OfTheMonth() {

@Test
public void testEveryFiveDaysStartingOnTuesday() {
assertExpression("0 0 0 ? * 3/5", "at 00:00 every 5 days from day Tuesday");
assertExpression("0 0 0 ? * 3/5", "at 00:00 every 5 days from Tuesday");
}

/**
Expand All @@ -123,7 +123,7 @@ public void testEveryDayEveryFourHoursFromHour2() {
*/
@Test
public void testDescriptionDayOfWeek() {
assertExpression("* 0/1 * ? * TUE", "every second every minute from minute 0 at Tuesday day");
assertExpression("* 0/1 * ? * TUE", "every second every minute on Tuesday");
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public void testEverySecondInMonth() {
results.add(new CronField(CronFieldName.MINUTE, FieldExpression.always(), nullFieldConstraints));
results.add(new CronField(CronFieldName.SECOND, FieldExpression.always(), nullFieldConstraints));
results.add(new CronField(CronFieldName.MONTH, new On(new IntegerFieldValue(month)), nullFieldConstraints));
assertEquals("every second at February month", descriptor.describe(new SingleCron(mockDefinition, results)));
assertEquals("every second in February", descriptor.describe(new SingleCron(mockDefinition, results)));
}

@Test
Expand All @@ -148,7 +148,7 @@ public void testLastDayOfWeekInMonth() {
results.add(new CronField(CronFieldName.MINUTE, new On(new IntegerFieldValue(minute)), nullFieldConstraints));
results.add(new CronField(CronFieldName.DAY_OF_WEEK, new On(new IntegerFieldValue(dayOfWeek), new SpecialCharFieldValue(SpecialChar.L)),
nullFieldConstraints));
assertEquals(String.format("at %s:%s last Tuesday of every month", hour, minute), descriptor.describe(new SingleCron(mockDefinition, results)));
assertEquals(String.format("at %s:%s on the last Tuesday of the month", hour, minute), descriptor.describe(new SingleCron(mockDefinition, results)));
}

@Test
Expand All @@ -161,7 +161,7 @@ public void testNthDayOfWeekInMonth() {
results.add(new CronField(CronFieldName.MINUTE, new On(new IntegerFieldValue(minute)), nullFieldConstraints));
results.add(new CronField(CronFieldName.DAY_OF_WEEK,
new On(new IntegerFieldValue(dayOfWeek), new SpecialCharFieldValue(SpecialChar.HASH), new IntegerFieldValue(dayOfWeek)), nullFieldConstraints));
assertEquals(String.format("at %s:%s Tuesday %s of every month", hour, minute, dayOfWeek), descriptor.describe(new SingleCron(mockDefinition, results)));
assertEquals(String.format("at %s:%s on the second Tuesday of the month", hour, minute), descriptor.describe(new SingleCron(mockDefinition, results)));
}

@Test
Expand Down
Loading
Loading