diff --git a/README.md b/README.md
index 003dddf..bde56bf 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
JavaCron
============
-JavaCron is a java library which provides functionality for parsing crontab expression
+JavaCron is a java library which provides functionality for parsing crontab expression
and calculating the next run, based on current or specified date time.
## Features
@@ -22,7 +22,7 @@ next run(s).
│ │ ┌───────────── day of the month (1 - 31)
│ │ │ ┌───────────── month (1 - 12 or Jan/January - Dec/December)
│ │ │ │ ┌───────────── day of the week (0 - 6 or Sun/Sunday - Sat/Saturday)
-│ │ │ │ │
+│ │ │ │ │
│ │ │ │ │
│ │ │ │ │
* * * * *
@@ -36,7 +36,7 @@ next run(s).
│ │ │ ┌───────────── day of the month (1 - 31)
│ │ │ │ ┌───────────── month (1 - 12 or Jan/January - Dec/December)
│ │ │ │ │ ┌───────────── day of the week (0 - 6 or Sun/Sunday - Sat/Saturday)
-│ │ │ │ │ │
+│ │ │ │ │ │
│ │ │ │ │ │
│ │ │ │ │ │
* * * * * *
@@ -75,6 +75,10 @@ public class App {
Schedule schedule4 = Schedule.create("0 0 0 29 2 */4");
System.out.println(dateFormatter.format(schedule4.next(baseDate))); // 2024-02-29 00:00:00
+ // at 00:00:00 last Friday in Jan
+ Schedule schedule5 = Schedule.create("0 0 0 * * 5L");
+ System.out.println(dateFormatter.format(schedule5.next(baseDate))); // 2019-01-25 00:00:00
+
// Calculating the next 5 runs
Date[] nextRuns = schedule3.next(baseDate, 5);
for (Date run : nextRuns) {
diff --git a/pom.xml b/pom.xml
index 9eeee5c..d8357f1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.asahaf.javacron
javacron
- 1.2.1
+ 1.3.0
javacron
A java library for parsing crontab expressions and calculating the next run time
https://github.com/asahaf/javacron
diff --git a/src/main/java/com/asahaf/javacron/CronFieldParser.java b/src/main/java/com/asahaf/javacron/CronFieldParser.java
index c6632b7..56a7ecd 100644
--- a/src/main/java/com/asahaf/javacron/CronFieldParser.java
+++ b/src/main/java/com/asahaf/javacron/CronFieldParser.java
@@ -62,39 +62,39 @@ class CronFieldParser {
CronFieldParser(CronFieldType fieldType) {
this.fieldType = fieldType;
switch (fieldType) {
- case SECOND:
- case MINUTE:
- this.fieldName = this.fieldType.toString().toLowerCase();
- this.length = 60;
- this.maxAllowedValue = 59;
- this.minAllowedValue = 0;
- break;
- case HOUR:
- this.fieldName = this.fieldType.toString().toLowerCase();
- this.length = 24;
- this.maxAllowedValue = 23;
- this.minAllowedValue = 0;
- break;
- case DAY:
- this.fieldName = this.fieldType.toString().toLowerCase();
- this.length = 31;
- this.maxAllowedValue = 31;
- this.minAllowedValue = 1;
- break;
- case MONTH:
- this.choices = CronFieldParser.MONTHS_NAMES;
- this.fieldName = this.fieldType.toString().toLowerCase();
- this.length = 12;
- this.maxAllowedValue = 12;
- this.minAllowedValue = 1;
- break;
- case DAY_OF_WEEK:
- this.choices = CronFieldParser.DAYS_OF_WEEK_NAMES;
- this.fieldName = "day of week";
- this.length = 7;
- this.maxAllowedValue = 6;
- this.minAllowedValue = 0;
- break;
+ case SECOND:
+ case MINUTE:
+ this.fieldName = this.fieldType.toString().toLowerCase();
+ this.length = 60;
+ this.maxAllowedValue = 59;
+ this.minAllowedValue = 0;
+ break;
+ case HOUR:
+ this.fieldName = this.fieldType.toString().toLowerCase();
+ this.length = 24;
+ this.maxAllowedValue = 23;
+ this.minAllowedValue = 0;
+ break;
+ case DAY:
+ this.fieldName = this.fieldType.toString().toLowerCase();
+ this.length = 31;
+ this.maxAllowedValue = 31;
+ this.minAllowedValue = 1;
+ break;
+ case MONTH:
+ this.choices = CronFieldParser.MONTHS_NAMES;
+ this.fieldName = this.fieldType.toString().toLowerCase();
+ this.length = 12;
+ this.maxAllowedValue = 12;
+ this.minAllowedValue = 1;
+ break;
+ case DAY_OF_WEEK:
+ this.choices = CronFieldParser.DAYS_OF_WEEK_NAMES;
+ this.fieldName = "day of week";
+ this.length = 7;
+ this.maxAllowedValue = 6;
+ this.minAllowedValue = 0;
+ break;
}
}
@@ -145,6 +145,12 @@ private int parseValue(String token) {
}
public BitSet parse(String token) throws InvalidExpressionException {
+ // This is when last day of the month is specified
+ if (this.fieldType == CronFieldType.DAY_OF_WEEK) {
+ if (token.length() == 2 && token.endsWith("l")) {
+ return this.parseLiteral(token.substring(0, 1));
+ }
+ }
if (token.indexOf(",") > -1) {
BitSet bitSet = new BitSet(this.length);
String[] items = token.split(",");
diff --git a/src/main/java/com/asahaf/javacron/Schedule.java b/src/main/java/com/asahaf/javacron/Schedule.java
index 020b182..98fa599 100644
--- a/src/main/java/com/asahaf/javacron/Schedule.java
+++ b/src/main/java/com/asahaf/javacron/Schedule.java
@@ -42,6 +42,7 @@ private Schedule() {
private BitSet months;
private BitSet daysOfWeek;
private BitSet daysOf5Weeks;
+ private boolean isSpecificLastDayOfMonth;
/**
* Parses crontab expression and create a Schedule object representing that
@@ -117,10 +118,10 @@ public static Schedule create(String expression) throws InvalidExpressionExcepti
token = fields[index++];
schedule.hours = Schedule.HOURS_FIELD_PARSER.parse(token);
- token = fields[index++];
- schedule.days = Schedule.DAYS_FIELD_PARSER.parse(token);
+ String daysToken = fields[index++];
+ schedule.days = Schedule.DAYS_FIELD_PARSER.parse(daysToken);
boolean daysStartWithAsterisk = false;
- if (token.startsWith("*"))
+ if (daysToken.startsWith("*"))
daysStartWithAsterisk = true;
token = fields[index++];
@@ -131,6 +132,16 @@ public static Schedule create(String expression) throws InvalidExpressionExcepti
boolean daysOfWeekStartAsterisk = false;
if (token.startsWith("*"))
daysOfWeekStartAsterisk = true;
+
+ if (token.length() == 2 && token.endsWith("l")) {
+ if (!daysToken.equalsIgnoreCase("*")) {
+ throw new InvalidExpressionException(
+ "when last days of month is specified. the day of the month must be \"*\"");
+ }
+ // this flag will be used later duing finding the next schedual
+ // this is because some months has less than 31 days
+ schedule.isSpecificLastDayOfMonth = true;
+ }
schedule.daysOf5Weeks = generateDaysOf5Weeks(schedule.daysOfWeek);
schedule.daysAndDaysOfWeekRelation = (daysStartWithAsterisk || daysOfWeekStartAsterisk)
@@ -223,7 +234,7 @@ public Date next(Date baseDate) {
day = 1;
}
month = candidateMonth;
- BitSet adjustedDaysSet = getUpdatedDays(year, month);
+ BitSet adjustedDaysSet = getUpdatedDays(this, year, month);
candidateDay = adjustedDaysSet.nextSetBit(day - 1) + 1;
if (candidateDay < 1) {
month++;
@@ -331,7 +342,7 @@ public int compareTo(Schedule anotherSchedule) {
* if and only if the argument is not {@code null} and is a {@code Schedule}
* object that whose seconds, minutes, hours, days, months, and days of
* weeks sets are equal to those of this schedule.
- *
+ *
* The expression string used to create the schedule is not considered, as two
* different expressions may produce same schedules.
*
@@ -397,32 +408,40 @@ private static BitSet generateDaysOf5Weeks(BitSet daysOfWeek) {
return bitSet;
}
- private BitSet getUpdatedDays(int year, int month) {
+ private BitSet getUpdatedDays(Schedule schedule, int year, int month) {
Date date = new Date(year, month, 1);
int daysOf5WeeksOffset = date.getDay();
BitSet updatedDays = new BitSet(31);
updatedDays.or(this.days);
BitSet monthDaysOfWeeks = this.daysOf5Weeks.get(daysOf5WeeksOffset, daysOf5WeeksOffset + 31);
- if (this.daysAndDaysOfWeekRelation == DaysAndDaysOfWeekRelation.INTERSECT) {
+ if (schedule.isSpecificLastDayOfMonth
+ || this.daysAndDaysOfWeekRelation == DaysAndDaysOfWeekRelation.INTERSECT) {
updatedDays.and(monthDaysOfWeeks);
} else {
updatedDays.or(monthDaysOfWeeks);
}
- int i;
+ int monthDaysCount;
if (month == 1 /* Feb */) {
- i = 28;
+ monthDaysCount = 28;
if (isLeapYear(year)) {
- i++;
+ monthDaysCount++;
}
} else {
// We cannot use lengthOfMonth method with the month Feb
// because it returns incorrect number of days for years
// that are dividable by 400 like the year 2000, a bug??
- i = YearMonth.of(year, month + 1).lengthOfMonth();
+ monthDaysCount = YearMonth.of(year, month + 1).lengthOfMonth();
}
// remove days beyond month length
- for (; i < 31; i++) {
- updatedDays.set(i, false);
+ for (int j = monthDaysCount; j < 31; j++) {
+ updatedDays.set(j, false);
+ }
+
+ // remove days before the last 7 days
+ if (schedule.isSpecificLastDayOfMonth) {
+ for (int j = 0; j < monthDaysCount - 7; j++) {
+ updatedDays.set(j, false);
+ }
}
return updatedDays;
}
diff --git a/src/test/java/com/asahaf/javacron/ScheduleTest.java b/src/test/java/com/asahaf/javacron/ScheduleTest.java
index de063aa..7961e43 100644
--- a/src/test/java/com/asahaf/javacron/ScheduleTest.java
+++ b/src/test/java/com/asahaf/javacron/ScheduleTest.java
@@ -568,6 +568,18 @@ public static Collection getTestCases() {
{ "2019-01-01 00:00:00", "0 0 0 29 2 4", "2019-02-07 00:00:00" },
{ "2019-01-01 00:00:00", "0 0 0 29 2 5", "2019-02-01 00:00:00" },
+ // last day of month
+ { "2019-01-01 00:00:00", "0 0 0 * * 5l", "2019-01-25 00:00:00" },
+ { "2019-01-01 00:00:00", "0 0 0 * * 6l", "2019-01-26 00:00:00" },
+ { "2019-01-01 00:00:00", "0 0 0 * * 0L", "2019-01-27 00:00:00" },
+ { "2019-01-01 00:00:00", "0 0 0 * * 5l", "2019-01-25 00:00:00" },
+ { "2019-01-01 00:00:00", "0 0 0 * 2 5L", "2019-02-22 00:00:00" },
+ { "2019-01-01 00:00:00", "0 0 0 * 9 0l", "2019-09-29 00:00:00" },
+ { "2019-01-01 00:00:00", "0 0 0 * 5 2L", "2019-05-28 00:00:00" },
+ { "2019-01-01 00:00:00", "0 0 0 * 2 6L", "2019-02-23 00:00:00" },
+ { "2019-01-01 00:00:00", "0 0 0 * 4 3L", "2019-04-24 00:00:00" },
+ { "2019-01-01 00:00:00", "0 0 0 * 4 2L", "2019-04-30 00:00:00" },
+
});
}