Skip to content

Commit

Permalink
feat: (cron) correct order on various list
Browse files Browse the repository at this point in the history
  • Loading branch information
teletha committed Oct 10, 2024
1 parent 996fd35 commit 7093f64
Show file tree
Hide file tree
Showing 3 changed files with 446 additions and 277 deletions.
137 changes: 52 additions & 85 deletions src/main/java/kiss/Cron.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand All @@ -30,10 +30,6 @@ class Cron {

private ChronoField field;

private int min;

private List<String> names;

/**
* [0] - start
* [1] - end
Expand All @@ -51,63 +47,53 @@ class Cron {
*/
Cron(ChronoField field, int min, int max, String names, String modifier, String increment, String expr) {
this.field = field;
this.min = min;
this.names = Arrays.asList(names.split("(?<=\\G...)")); // split every three letters

for (String range : expr.split(",")) {
Matcher m = FORMAT.matcher(range);
if (!m.matches()) error(range);
if (!m.matches()) {
throw new IllegalArgumentException(range);
}

String start = m.group(3);
String mod = m.group(4);
String end = m.group(5);
String incmod = m.group(6);
String inc = m.group(7);

int[] part = {-1, -1, -1, 0, 0};
int[] part = {-1, -1, 1, 0, 0};
if (start != null) {
part[0] = map(start);
part[0] = part[1] = map(start, names);
part[3] = mod == null ? 0 : mod.charAt(0);
if (end != null) {
part[1] = map(end);
part[2] = 1;
part[1] = map(end, names);
} else if (inc != null) {
part[1] = max;
} else {
part[1] = part[0];
}
} else if (m.group(1) != null) {
}

// astarisk
if (m.group(1) != null) {
part[0] = min;
part[1] = max;
part[2] = 1;
} else if (m.group(2) != null) {
}
if (m.group(2) != null) {
mod = m.group(2);
part[3] = mod.length() == 2 ? 'W' : mod.charAt(0);
} else {
error(range);
part[3] = mod.charAt(mod.length() - 1);
}

if (inc != null) {
part[4] = incmod.charAt(0);
part[4] = m.group(6).charAt(0);
part[2] = Integer.parseInt(inc);
}

// validate range
if ((part[0] != -1 && part[0] < min) || (part[1] != -1 && part[1] > max) || (part[0] != -1 && part[1] != -1 && part[0] > part[1])) {
error(range);
}

// validate part
if (part[3] != 0 && modifier.indexOf(part[3]) == -1) {
error(String.valueOf((char) part[3]));
}
if (part[4] != 0 && increment.indexOf(part[4]) == -1) {
error(String.valueOf((char) part[4]));
// validate parts
// @formatter:off
if ((part[0] != -1 && part[0] < min) || max < part[1] || part[0] > part[1] || (part[3] != 0 && modifier.indexOf(part[3]) == -1) || part[4] != 0 && increment.indexOf(part[4]) == -1) {
throw new IllegalArgumentException(range);
}
// @formatter:on
parts.add(part);
}

Collections.sort(parts, (x, y) -> Integer.compare(x[0], y[0]));
Collections.sort(parts, (x, y) -> Integer.compare(x[0] + x[3], y[0] + y[3]));
}

/**
Expand All @@ -116,10 +102,13 @@ class Cron {
* @param name The string representation to map.
* @return The corresponding numeric value.
*/
private int map(String name) {
int index = names.indexOf(name.toUpperCase());
private int map(String name, String names) {
int index = names.indexOf(name.toUpperCase().concat(" "));
if (index != -1) {
return index + min;
// The minimum value of the field needs to be added, but since this function is only
// used for Month and DayOfWeek, there is no problem with always using the constant
// value 1 instead of field.range().getMinimum().
return index / 4 + 1;
}
int value = Integer.parseInt(name);
return value == 0 && field == ChronoField.DAY_OF_WEEK ? 7 : value;
Expand Down Expand Up @@ -163,11 +152,9 @@ boolean matches(ZonedDateTime date) {
}
}
} else if (part[4] == '#') {
if (dow == part[0]) {
int num = day / 7;
return part[2] == (day % 7 == 0 ? num : num + 1);
if (dow == part[0] && part[2] == (day % 7 == 0 ? day / 7 : day / 7 + 1)) {
return true;
}
return false;
} else {
int value = date.get(field);
if (part[3] == '?' || (part[0] <= value && value <= part[1] && (value - part[0]) % part[2] == 0)) {
Expand All @@ -184,62 +171,42 @@ boolean matches(ZonedDateTime date) {
* @param date Array containing a single ZonedDateTime to be updated.
* @return true if a match was found, false if the field overflowed.
*/
boolean nextMatch(ZonedDateTime[] date) {
boolean matches(ZonedDateTime[] date) {
int value = date[0].get(field);
TreeSet<ZonedDateTime> set = new TreeSet();

for (int[] part : parts) {
int nextMatch = nextMatch(value, part);
if (nextMatch > -1) {
if (nextMatch != value) {
int next = part[0] <= value ? value : part[0];
int rem = (next - part[0]) % part[2];
if (rem != 0) next += part[2] - rem;

if (next <= part[1]) {
ZonedDateTime target = date[0];
if (next != value) {
if (field == ChronoField.MONTH_OF_YEAR) {
date[0] = date[0].withMonth(nextMatch).withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS);
target = target.with(field, next).withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS);
} else {
date[0] = date[0].with(field, nextMatch).truncatedTo(field.getBaseUnit());
target = target.with(field, next).truncatedTo(field.getBaseUnit());
}
}
return true;
set.add(target);
continue;
}
}

if (!set.isEmpty()) {
date[0] = set.first();
return true;
}

if (field == ChronoField.MONTH_OF_YEAR) {
date[0] = date[0].plusYears(1).withMonth(1).withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS);
date[0] = date[0].plus(1, field.getRangeUnit()).withMonth(1).withDayOfMonth(1).truncatedTo(ChronoUnit.DAYS);
} else {
date[0] = date[0].plus(1, field.getRangeUnit()).with(field, min).truncatedTo(field.getBaseUnit());
// The field must be set to the minimum value, but this method is only used for Second,
// Minute, Hour, and Month, and since Month is handled in the above branch, there is no
// problem with always using the constant value 0 instead of field.range().getMinimum().
date[0] = date[0].plus(1, field.getRangeUnit()).with(field, 0).truncatedTo(field.getBaseUnit());
}
return false;
}

/**
* Finds the next matching value within a single Part.
*
* @param value The current value.
* @param part The Part to match against.
* @return The next matching value, or -1 if no match is found.
*/
private int nextMatch(int value, int[] part) {
if (value > part[1]) {
return -1;
}
int nextPotential = Math.max(value, part[0]);
if (part[2] == 1 || nextPotential == part[0]) {
return nextPotential;
}

int remainder = ((nextPotential - part[0]) % part[2]);
if (remainder != 0) {
nextPotential += part[2] - remainder;
}

return nextPotential <= part[1] ? nextPotential : -1;
}

/**
* Throw the invalid format error.
*
* @param cron
* @return
*/
static int error(String cron) {
throw new IllegalArgumentException("Invalid format '" + cron + "'");
}
}
28 changes: 15 additions & 13 deletions src/main/java/kiss/Scheduler.java
Original file line number Diff line number Diff line change
Expand Up @@ -243,15 +243,17 @@ public ScheduledFuture<?> scheduleAt(Runnable command, String format) {
*/
static Cron[] parse(String cron) {
String[] parts = cron.strip().split("\\s+");
int i = parts.length == 5 ? 0 : parts.length == 6 ? 1 : Cron.error(cron);

return new Cron[] { //
new Cron(ChronoField.SECOND_OF_MINUTE, 0, 59, "", "", "/", i == 1 ? parts[0] : "0"), //
new Cron(ChronoField.MINUTE_OF_HOUR, 0, 59, "", "", "/", parts[i++]), //
new Cron(ChronoField.HOUR_OF_DAY, 0, 23, "", "", "/", parts[i++]), //
new Cron(ChronoField.DAY_OF_MONTH, 1, 31, "", "?LW", "/", parts[i++]), //
new Cron(ChronoField.MONTH_OF_YEAR, 1, 12, "JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC", "", "/", parts[i++]), //
new Cron(ChronoField.DAY_OF_WEEK, 1, 7, "MONTUEWEDTHUFRISATSUN", "?L", "#/", parts[i++])};
int i = parts.length - 5;
if (i != 0 && i != 1) {
throw new IllegalArgumentException(cron);
}

return new Cron[] {new Cron(ChronoField.SECOND_OF_MINUTE, 0, 59, "", "", "/", i == 1 ? parts[0] : "0"),
new Cron(ChronoField.MINUTE_OF_HOUR, 0, 59, "", "", "/", parts[i++]),
new Cron(ChronoField.HOUR_OF_DAY, 0, 23, "", "", "/", parts[i++]),
new Cron(ChronoField.DAY_OF_MONTH, 1, 31, "", "?LW", "/", parts[i++]),
new Cron(ChronoField.MONTH_OF_YEAR, 1, 12, "JAN FEB MAR APR MAY JUN JUL AUG SEP OCT NOV DEC ", "", "/", parts[i++]),
new Cron(ChronoField.DAY_OF_WEEK, 1, 7, "MON TUE WED THU FRI SAT SUN ", "?L", "#/", parts[i++])};
}

/**
Expand All @@ -273,17 +275,17 @@ static ZonedDateTime next(Cron[] cron, ZonedDateTime base) {
ZonedDateTime[] next = {base.plusSeconds(1).truncatedTo(ChronoUnit.SECONDS)};
root: while (true) {
if (next[0].isAfter(limit)) throw new IllegalArgumentException("Next time is not found before " + limit);
if (!cron[4].nextMatch(next)) continue;
if (!cron[4].matches(next)) continue;

int month = next[0].getMonthValue();
while (!(cron[3].matches(next[0]) && cron[5].matches(next[0]))) {
next[0] = next[0].plusDays(1).truncatedTo(ChronoUnit.DAYS);
if (next[0].getMonthValue() != month) continue root;
}

if (!cron[2].nextMatch(next)) continue;
if (!cron[1].nextMatch(next)) continue;
if (!cron[0].nextMatch(next)) continue;
if (!cron[2].matches(next)) continue;
if (!cron[1].matches(next)) continue;
if (!cron[0].matches(next)) continue;
return next[0];
}
}
Expand Down
Loading

0 comments on commit 7093f64

Please sign in to comment.