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" }, + }); }