From 67c88d9b62f899541255d1539a87b4f62a27dce6 Mon Sep 17 00:00:00 2001 From: frodecarlsen Date: Fri, 30 May 2014 10:34:35 +0200 Subject: [PATCH] Publish to github --- .gitattributes | 22 + .gitignore | 218 +++++++++ pom.xml | 59 +++ src/main/java/fc/cron/CronExpression.java | 447 ++++++++++++++++++ src/test/java/fc/cron/CronExpressionTest.java | 445 +++++++++++++++++ 5 files changed, 1191 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/fc/cron/CronExpression.java create mode 100644 src/test/java/fc/cron/CronExpressionTest.java diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..412eeda --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..968bfad --- /dev/null +++ b/.gitignore @@ -0,0 +1,218 @@ +################# +## Eclipse +################# + +*.pydevproject +.project +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.classpath +.settings/ +.loadpath + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# PDT-specific +.buildpath + +# Maven +target/ + +################# +## Visual Studio +################# + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results + +[Dd]ebug/ +[Rr]elease/ +x64/ +build/ +[Bb]in/ +[Oo]bj/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +*_i.c +*_p.c +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.log +*.scc + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +*.ncrunch* +.*crunch*.local.xml + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.Publish.xml +*.pubxml + +# NuGet Packages Directory +## TODO: If you have NuGet Package Restore enabled, uncomment the next line +#packages/ + +# Windows Azure Build Output +csx +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.[Pp]ublish.xml +*.pfx +*.publishsettings + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file to a newer +# Visual Studio version. Backup files are not needed, because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +App_Data/*.mdf +App_Data/*.ldf + +############# +## Windows detritus +############# + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Mac crap +.DS_Store + + +############# +## Python +############# + +*.py[co] + +# Packages +*.egg +*.egg-info +dist/ +build/ +eggs/ +parts/ +var/ +sdist/ +develop-eggs/ +.installed.cfg + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox + +#Translations +*.mo + +#Mr Developer +.mr.developer.cfg +*.class diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0afa913 --- /dev/null +++ b/pom.xml @@ -0,0 +1,59 @@ + + 4.0.0 + fc.cron + cron + jar + 1.0 + cron + https://github.com/frode-carlsen/cron + + + + Apache License Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + + + + + + joda-time + joda-time + 2.3 + + + + org.easytesting + fest-assert + 1.4 + test + + + + junit + junit + 4.11 + test + + + + + + + maven-compiler-plugin + 3.1 + + true + 1.7 + 1.7 + UTF-8 + + + + + + + UTF-8 + UTF-8 + + diff --git a/src/main/java/fc/cron/CronExpression.java b/src/main/java/fc/cron/CronExpression.java new file mode 100644 index 0000000..4569f48 --- /dev/null +++ b/src/main/java/fc/cron/CronExpression.java @@ -0,0 +1,447 @@ +/* + * Copyright (C) 2012 Frode Carlsen. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fc.cron; + +import static org.joda.time.DateTimeConstants.DAYS_PER_WEEK; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.joda.time.DateTime; +import org.joda.time.LocalDate; +import org.joda.time.MutableDateTime; + +/** + * Parser for unix-like cron expressions: Cron expressions allow specifying combinations of criteria for time + * such as: "Each Monday-Friday at 08:00" or "Every last friday of the month at 01:30" + *

+ * A cron expressions consists of 6 mandatory fields separated by space.
+ * These are: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Field Allowable values Special Characters
Seconds  + * 0-59  + * , - * /
Minutes  + * 0-59  + * , - * /
Hours  + * 0-23  + * , - * /
Day of month  + * 1-31  + * , - * ? / L W
Month  + * 1-12 or JAN-DEC (note: english abbreviations)  + * , - * /
Day of week  + * 1-7 or MON-SUN (note: english abbreviations)  + * , - * ? / L #
+ * + *

+ * '*' Can be used in all fields and means 'for all values'. E.g. "*" in minutes, means 'for all minutes' + *

+ * '?' Ca be used in Day-of-month and Day-of-week fields. Used to signify 'no special value'. It is used when one want + * to specify something for one of those two fields, but not the other. + *

+ * '-' Used to specify a time interval. E.g. "10-12" in Hours field means 'for hours 10, 11 and 12' + *

+ * ',' Used to specify multiple values for a field. E.g. "MON,WED,FRI" in Day-of-week field means "for + * monday, wednesday and friday" + *

+ * '/' Used to specify increments. E.g. "0/15" in Seconds field means "for seconds 0, 15, 30, ad + * 45". And "5/15" in seconds field means "for seconds 5, 20, 35, and 50". If '*' s specified + * before '/' it is the same as saying it starts at 0. For every field there's a list of values that can be turned on or + * off. For Seconds and Minutes these range from 0-59. For Hours from 0 to 23, For Day-of-month it's 1 to 31, For Months + * 1 to 12. "/" character helsp turn some of these values back on. Thus "7/6" in Months field + * specify just Month 7. It doesn't turn on every 6 month following, since cron fields never roll over + *

+ * 'L' Can be used on Day-of-month and Day-of-week fields. It signifies last day of the set of allowed values. In + * Day-of-month field it's the last day of the month (e.g.. 31 jan, 28 feb (29 in leap years), 31 march, etc.). In + * Day-of-week field it's Sunday. If there's a prefix, this will be subtracted (5L in Day-of-month means 5 days before + * last day of Month: 26 jan, 23 feb, etc.) + *

+ * 'W' Can be specified in Day-of-Month field. It specifies closest weekday (monday-friday). Holidays are not accounted + * for. "15W" in Day-of-Month field means 'closest weekday to 15 i in given month'. If the 15th is a Saturday, + * it gives Friday. If 15th is a Sunday, the it gives following Monday. + *

+ * '#' Can be used in Day-of-Week field. For example: "5#3" means 'third friday in month' (day 5 = friday, #3 + * - the third). If the day does not exist (e.g. "5#5" - 5th friday of month) and there aren't 5 fridays in + * the month, then it won't match until the next month with 5 fridays. + *

+ * Case-sensitivt No fields are case-sensitive + *

+ * Dependencies between fields Fields are always evaluated independently, but the expression doesn't match until + * the constraints of each field are met.Feltene evalueres Overlap of intervals are not allowed. That is: for + * Day-of-week field "FRI-MON" is invalid,but "FRI-SUN,MON" is valid + * + */ +public class CronExpression { + + enum CronFieldType { + SECOND(0, 59, null), + MINUTE(0, 59, null), + HOUR(0, 23, null), + DAY_OF_MONTH(1, 31, null), + MONTH(1, 12, Arrays.asList("JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC")), + DAY_OF_WEEK(1, 7, Arrays.asList("MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN")); + + final int from, to; + final List names; + + CronFieldType(int from, int to, List names) { + this.from = from; + this.to = to; + this.names = names; + } + + } + + private final String expr; + private final SimpleField secondField; + private final SimpleField minuteField; + private final SimpleField hourField; + private final DayOfWeekField dayOfWeekField; + private final SimpleField monthField; + private final DayOfMonthField dayOfMonthField; + + public CronExpression(String expr) { + if (expr == null) { + throw new IllegalArgumentException("expr is null"); + } + this.expr = expr; + String[] parts = expr.split("\\s+"); + if (parts.length != 6) { + throw new IllegalArgumentException(String.format("Invalid cronexpression [%s], expected %s felt, got %s" + , expr, CronFieldType.values().length, parts.length)); + } + + this.secondField = new SimpleField(CronFieldType.SECOND, parts[0]); + this.minuteField = new SimpleField(CronFieldType.MINUTE, parts[1]); + this.hourField = new SimpleField(CronFieldType.HOUR, parts[2]); + this.dayOfMonthField = new DayOfMonthField(parts[3]); + this.monthField = new SimpleField(CronFieldType.MONTH, parts[4]); + this.dayOfWeekField = new DayOfWeekField(parts[5]); + } + + public DateTime nextTimeAfter(DateTime afterTime) { + MutableDateTime nextTime = new MutableDateTime(afterTime); + nextTime.setMillisOfSecond(0); + nextTime.secondOfDay().add(1); + + while (true) { // day of week + while (true) { // month + while (true) { // day of month + while (true) { // hour + while (true) { // minute + while (true) { // second + if (secondField.matches(nextTime.getSecondOfMinute())) { + break; + } + nextTime.secondOfDay().add(1); + } + if (minuteField.matches(nextTime.getMinuteOfHour())) { + break; + } + nextTime.minuteOfDay().add(1); + nextTime.secondOfMinute().set(0); + } + if (hourField.matches(nextTime.getHourOfDay())) { + break; + } + nextTime.hourOfDay().add(1); + nextTime.minuteOfHour().set(0); + nextTime.secondOfMinute().set(0); + } + if (dayOfMonthField.matches(new LocalDate(nextTime))) { + break; + } + nextTime.addDays(1); + nextTime.setTime(0, 0, 0, 0); + } + if (monthField.matches(nextTime.getMonthOfYear())) { + break; + } + nextTime.addMonths(1); + nextTime.setDayOfMonth(1); + nextTime.setTime(0, 0, 0, 0); + } + if (dayOfWeekField.matches(new LocalDate(nextTime))) { + break; + } + nextTime.addDays(1); + nextTime.setTime(0, 0, 0, 0); + } + + return nextTime.toDateTime(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "<" + expr + ">"; + } + + static class FieldPart { + private Integer from, to, increment; + private String modifier, incrementModifier; + } + + abstract static class BasicField { + private static final Pattern CRON_FELT_REGEXP = Pattern + .compile("(?: # start of group 1\n" + + " (?:(?\\*)|(?\\?)|(?L)) # globalt flag (L, ?, *)\n" + + " | (?[0-9]{1,2}|[a-z]{3,3}) # or start number or symbol\n" + + " (?: # start of group 2\n" + + " (?L|W) # modifier (L,W)\n" + + " | -(?[0-9]{1,2}|[a-z]{3,3}) # or end nummer or symbol (in range)\n" + + " )? # end of group 2\n" + + ") # end of group 1\n" + + "(?:(?/|\\#)(?[0-9]{1,7}))? # increment and increment modifier (/ or \\#)\n" + , Pattern.CASE_INSENSITIVE | Pattern.COMMENTS); + + final CronFieldType fieldType; + final List parts = new ArrayList<>(); + + private BasicField(CronFieldType fieldType, String fieldExpr) { + this.fieldType = fieldType; + parse(fieldExpr); + } + + private void parse(String fieldExpr) { // NOSONAR + String[] rangeParts = fieldExpr.split(","); + for (String rangePart : rangeParts) { + Matcher m = CRON_FELT_REGEXP.matcher(rangePart); + if (!m.matches()) { + throw new IllegalArgumentException("Invalid cron field '" + rangePart + "' for field [" + fieldType + "]"); + } + String startNummer = m.group("start"); + String modifier = m.group("mod"); + String sluttNummer = m.group("end"); + String inkrementModifier = m.group("inkmod"); + String inkrement = m.group("ink"); + + FieldPart part = new FieldPart(); + part.increment = 999; + if (startNummer != null) { + part.from = mapValue(startNummer); + part.modifier = modifier; + if (sluttNummer != null) { + part.to = mapValue(sluttNummer); + part.increment = 1; + } else if (inkrement != null) { + part.to = fieldType.to; + } else { + part.to = part.from; + } + } else if (m.group("all") != null) { + part.from = fieldType.from; + part.to = fieldType.to; + part.increment = 1; + } else if (m.group("ignorer") != null) { + part.modifier = m.group("ignorer"); + } else if (m.group("last") != null) { + part.modifier = m.group("last"); + } else { + throw new IllegalArgumentException("Invalid cron part: " + rangePart); + } + + if (inkrement != null) { + part.incrementModifier = inkrementModifier; + part.increment = Integer.valueOf(inkrement); + } + + validateRange(part); + validatePart(part); + parts.add(part); + + } + } + + protected void validatePart(FieldPart part) { + if (part.modifier != null) { + throw new IllegalArgumentException(String.format("Invalid modifier [%s]", part.modifier)); + } else if (part.incrementModifier != null && !"/".equals(part.incrementModifier)) { + throw new IllegalArgumentException(String.format("Invalid increment modifier [%s]", part.incrementModifier)); + } + } + + private void validateRange(FieldPart part) { + if ((part.from != null && part.from < fieldType.from) || (part.to != null && part.to > fieldType.to)) { + throw new IllegalArgumentException(String.format("Invalid interval [%s-%s], must be %s<=_<=%s", part.from, part.to, fieldType.from, + fieldType.to)); + } else if (part.from != null && part.to != null && part.from > part.to) { + throw new IllegalArgumentException( + String.format( + "Invalid interval [%s-%s]. Rolling periods are not supported (ex. 5-1, only 1-5) since this won't give a deterministic result. Must be %s<=_<=%s", + part.from, part.to, fieldType.from, fieldType.to)); + } + } + + protected Integer mapValue(String value) { + Integer idx; + if (fieldType.names != null && (idx = fieldType.names.indexOf(value.toUpperCase(Locale.getDefault()))) >= 0) { + return idx + 1; + } + return Integer.valueOf(value); + } + + protected boolean matches(int val, FieldPart part) { + if (val >= part.from && val <= part.to && (val - part.from) % part.increment == 0) { + return true; + } + return false; + } + } + + static class SimpleField extends BasicField { + SimpleField(CronFieldType fieldType, String fieldExpr) { + super(fieldType, fieldExpr); + } + + public boolean matches(int val) { + if (val >= fieldType.from && val <= fieldType.to) { + for (FieldPart part : parts) { + if (matches(val, part)) { + return true; + } + } + } + return false; + } + } + + static class DayOfWeekField extends BasicField { + + DayOfWeekField(String fieldExpr) { + super(CronFieldType.DAY_OF_WEEK, fieldExpr); + } + + boolean matches(LocalDate dato) { + for (FieldPart part : parts) { + if ("L".equals(part.modifier)) { + return dato.getDayOfWeek() == part.from && dato.getDayOfMonth() > (dato.dayOfMonth().getMaximumValue() - DAYS_PER_WEEK); + } else if ("#".equals(part.incrementModifier)) { + if (dato.getDayOfWeek() == part.from) { + int num = dato.getDayOfMonth() / 7; + return part.increment == (dato.getDayOfMonth() % 7 == 0 ? num : num + 1); + } + return false; + } else if (matches(dato.getDayOfWeek(), part)) { + return true; + } + } + return false; + } + + @Override + protected Integer mapValue(String value) { + // Use 1-7 for weedays, but 0 will also represent sunday (linux practice) + return "0".equals(value) ? Integer.valueOf(7) : super.mapValue(value); + } + + @Override + protected boolean matches(int val, FieldPart part) { + return "?".equals(part.modifier) || super.matches(val, part); + } + + @Override + protected void validatePart(FieldPart part) { + if (part.modifier != null && Arrays.asList("L", "?").indexOf(part.modifier) == -1) { + throw new IllegalArgumentException(String.format("Invalid modifier [%s]", part.modifier)); + } else if (part.incrementModifier != null && Arrays.asList("/", "#").indexOf(part.incrementModifier) == -1) { + throw new IllegalArgumentException(String.format("Invalid increment modifier [%s]", part.incrementModifier)); + } + } + } + + static class DayOfMonthField extends BasicField { + DayOfMonthField(String fieldExpr) { + super(CronFieldType.DAY_OF_MONTH, fieldExpr); + } + + boolean matches(LocalDate dato) { + for (FieldPart part : parts) { + if ("L".equals(part.modifier)) { + return dato.getDayOfMonth() == (dato.dayOfMonth().getMaximumValue() - (part.from == null ? 0 : part.from)); + } else if ("W".equals(part.modifier)) { + if (dato.getDayOfWeek() <= 5) { + if (dato.getDayOfMonth() == part.from) { + return true; + } else if (dato.getDayOfWeek() == 5) { + return dato.plusDays(1).getDayOfMonth() == part.from; + } else if (dato.getDayOfWeek() == 1) { + return dato.minusDays(1).getDayOfMonth() == part.from; + } + } + } else if (matches(dato.getDayOfMonth(), part)) { + return true; + } + } + return false; + } + + @Override + protected void validatePart(FieldPart part) { + if (part.modifier != null && Arrays.asList("L", "W", "?").indexOf(part.modifier) == -1) { + throw new IllegalArgumentException(String.format("Invalid modifier [%s]", part.modifier)); + } else if (part.incrementModifier != null && !"/".equals(part.incrementModifier)) { + throw new IllegalArgumentException(String.format("Invalid increment modifier [%s]", part.incrementModifier)); + } + } + + @Override + protected boolean matches(int val, FieldPart part) { + return "?".equals(part.modifier) || super.matches(val, part); + } + + } +} diff --git a/src/test/java/fc/cron/CronExpressionTest.java b/src/test/java/fc/cron/CronExpressionTest.java new file mode 100644 index 0000000..f8085a1 --- /dev/null +++ b/src/test/java/fc/cron/CronExpressionTest.java @@ -0,0 +1,445 @@ +/* + * Copyright (C) 2012 Frode Carlsen + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package fc.cron; + +import static org.fest.assertions.Assertions.assertThat; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import fc.cron.CronExpression; +import fc.cron.CronExpression.CronFieldType; +import fc.cron.CronExpression.DayOfMonthField; +import fc.cron.CronExpression.DayOfWeekField; +import fc.cron.CronExpression.SimpleField; + +import org.joda.time.DateTime; +import org.joda.time.Hours; +import org.joda.time.LocalDate; +import org.junit.Test; + +public class CronExpressionTest { + + @Test + public void shall_parse_number() throws Exception { + SimpleField field = new SimpleField(CronFieldType.MINUTE, "5"); + assertPossibleValues(field, 5); + } + + private void assertPossibleValues(SimpleField field, Integer... values) { + Set valid = values == null ? new HashSet() : new HashSet<>(Arrays.asList(values)); + for (int i = field.fieldType.from; i <= field.fieldType.to; i++) { + String errorText = i + ":" + valid; + if (valid.contains(i)) { + assertThat(field.matches(i)).as(errorText).isTrue(); + } else { + assertThat(field.matches(i)).as(errorText).isFalse(); + } + } + } + + @Test + public void shall_parse_number_with_increment() throws Exception { + SimpleField field = new SimpleField(CronFieldType.MINUTE, "0/15"); + assertPossibleValues(field, 0, 15, 30, 45); + } + + @Test + public void shall_parse_range() throws Exception { + SimpleField field = new SimpleField(CronFieldType.MINUTE, "5-10"); + assertPossibleValues(field, 5, 6, 7, 8, 9, 10); + } + + @Test + public void shall_parse_range_with_increment() throws Exception { + SimpleField field = new SimpleField(CronFieldType.MINUTE, "20-30/2"); + assertPossibleValues(field, 20, 22, 24, 26, 28, 30); + } + + @Test + public void shall_parse_asterix() throws Exception { + SimpleField field = new SimpleField(CronFieldType.DAY_OF_WEEK, "*"); + assertPossibleValues(field, 1, 2, 3, 4, 5, 6, 7); + } + + @Test + public void shall_parse_asterix_with_increment() throws Exception { + SimpleField field = new SimpleField(CronFieldType.DAY_OF_WEEK, "*/2"); + assertPossibleValues(field, 1, 3, 5, 7); + } + + @Test + public void shall_ignore_field_in_day_of_week() throws Exception { + DayOfWeekField field = new DayOfWeekField("?"); + assertThat(field.matches(new LocalDate())).isTrue(); + } + + @Test + public void shall_ignore_field_in_day_of_month() throws Exception { + DayOfMonthField field = new DayOfMonthField("?"); + assertThat(field.matches(new LocalDate())).isTrue(); + } + + @Test(expected = IllegalArgumentException.class) + public void shall_give_error_if_invalid_count_field() throws Exception { + new CronExpression("* 3 *"); + } + + @Test(expected = IllegalArgumentException.class) + public void shall_give_error_if_minute_field_ignored() throws Exception { + SimpleField field = new SimpleField(CronFieldType.MINUTE, "?"); + field.matches(1); + } + + @Test(expected = IllegalArgumentException.class) + public void shall_give_error_if_hour_field_ignored() throws Exception { + SimpleField field = new SimpleField(CronFieldType.HOUR, "?"); + field.matches(1); + } + + @Test(expected = IllegalArgumentException.class) + public void shall_give_error_if_month_field_ignored() throws Exception { + SimpleField field = new SimpleField(CronFieldType.MONTH, "?"); + field.matches(1); + } + + @Test + public void shall_give_last_day_of_month_in_leapyear() throws Exception { + CronExpression.DayOfMonthField field = new DayOfMonthField("L"); + assertThat(field.matches(new LocalDate(2012, 02, 29))).isTrue(); + } + + @Test + public void shall_give_last_day_of_month() throws Exception { + CronExpression.DayOfMonthField field = new DayOfMonthField("L"); + assertThat(field.matches(new LocalDate().withDayOfMonth(new LocalDate().dayOfMonth().getMaximumValue()))).isTrue(); + } + + @Test + public void check_all() throws Exception { + assertThat(new CronExpression("* * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00, 01))).isEqualTo(new DateTime(2012, 4, 10, 13, 00, 02)); + assertThat(new CronExpression("* * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 02))).isEqualTo(new DateTime(2012, 4, 10, 13, 02, 01)); + assertThat(new CronExpression("* * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 59, 59))).isEqualTo(new DateTime(2012, 4, 10, 14, 00)); + } + + @Test(expected = IllegalArgumentException.class) + public void check_invalid_input() throws Exception { + new CronExpression(null); + } + + @Test + public void check_second_number() throws Exception { + assertThat(new CronExpression("3 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 01))).isEqualTo(new DateTime(2012, 4, 10, 13, 01, 03)); + assertThat(new CronExpression("3 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 01, 03))).isEqualTo(new DateTime(2012, 4, 10, 13, 02, 03)); + assertThat(new CronExpression("3 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 59, 03))).isEqualTo(new DateTime(2012, 4, 10, 14, 00, 03)); + assertThat(new CronExpression("3 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 23, 59, 03))).isEqualTo(new DateTime(2012, 4, 11, 00, 00, 03)); + assertThat(new CronExpression("3 * * * * *").nextTimeAfter(new DateTime(2012, 4, 30, 23, 59, 03))).isEqualTo(new DateTime(2012, 5, 01, 00, 00, 03)); + } + + @Test + public void check_second_increment() throws Exception { + assertThat(new CronExpression("5/15 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00))).isEqualTo(new DateTime(2012, 4, 10, 13, 00, 05)); + assertThat(new CronExpression("5/15 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00, 05))).isEqualTo(new DateTime(2012, 4, 10, 13, 00, 20)); + assertThat(new CronExpression("5/15 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00, 20))).isEqualTo(new DateTime(2012, 4, 10, 13, 00, 35)); + assertThat(new CronExpression("5/15 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00, 35))).isEqualTo(new DateTime(2012, 4, 10, 13, 00, 50)); + assertThat(new CronExpression("5/15 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00, 50))).isEqualTo(new DateTime(2012, 4, 10, 13, 01, 05)); + + // if rolling over minute then reset second (cron rules - increment affects only values in own field) + assertThat(new CronExpression("10/100 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00, 50))) + .isEqualTo(new DateTime(2012, 4, 10, 13, 01, 10)); + assertThat(new CronExpression("10/100 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 01, 10))) + .isEqualTo(new DateTime(2012, 4, 10, 13, 02, 10)); + } + + @Test + public void check_second_list() throws Exception { + assertThat(new CronExpression("7,19 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00))).isEqualTo(new DateTime(2012, 4, 10, 13, 00, 07)); + assertThat(new CronExpression("7,19 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00, 07))).isEqualTo(new DateTime(2012, 4, 10, 13, 00, 19)); + assertThat(new CronExpression("7,19 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00, 19))).isEqualTo(new DateTime(2012, 4, 10, 13, 01, 07)); + } + + @Test + public void check_second_range() throws Exception { + assertThat(new CronExpression("42-45 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00))).isEqualTo(new DateTime(2012, 4, 10, 13, 00, 42)); + assertThat(new CronExpression("42-45 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00, 42))).isEqualTo(new DateTime(2012, 4, 10, 13, 00, 43)); + assertThat(new CronExpression("42-45 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00, 43))).isEqualTo(new DateTime(2012, 4, 10, 13, 00, 44)); + assertThat(new CronExpression("42-45 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00, 44))).isEqualTo(new DateTime(2012, 4, 10, 13, 00, 45)); + assertThat(new CronExpression("42-45 * * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00, 45))).isEqualTo(new DateTime(2012, 4, 10, 13, 01, 42)); + } + + @Test(expected = IllegalArgumentException.class) + public void check_second_invalid_range() throws Exception { + new CronExpression("42-63 * * * * *"); + } + + @Test(expected = IllegalArgumentException.class) + public void check_second_invalid_increment_modifier() throws Exception { + new CronExpression("42#3 * * * * *"); + } + + @Test + public void check_minute_number() throws Exception { + assertThat(new CronExpression("0 3 * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 01))).isEqualTo(new DateTime(2012, 4, 10, 13, 03)); + assertThat(new CronExpression("0 3 * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 03))).isEqualTo(new DateTime(2012, 4, 10, 14, 03)); + } + + @Test + public void check_minute_increment() throws Exception { + assertThat(new CronExpression("0 0/15 * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00))).isEqualTo(new DateTime(2012, 4, 10, 13, 15)); + assertThat(new CronExpression("0 0/15 * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 15))).isEqualTo(new DateTime(2012, 4, 10, 13, 30)); + assertThat(new CronExpression("0 0/15 * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 30))).isEqualTo(new DateTime(2012, 4, 10, 13, 45)); + assertThat(new CronExpression("0 0/15 * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 45))).isEqualTo(new DateTime(2012, 4, 10, 14, 00)); + } + + @Test + public void check_minute_list() throws Exception { + assertThat(new CronExpression("0 7,19 * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00))).isEqualTo(new DateTime(2012, 4, 10, 13, 07)); + assertThat(new CronExpression("0 7,19 * * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 07))).isEqualTo(new DateTime(2012, 4, 10, 13, 19)); + } + + @Test + public void check_hour_number() throws Exception { + assertThat(new CronExpression("0 * 3 * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 01))).isEqualTo(new DateTime(2012, 4, 11, 03, 00)); + assertThat(new CronExpression("0 * 3 * * *").nextTimeAfter(new DateTime(2012, 4, 11, 03, 00))).isEqualTo(new DateTime(2012, 4, 11, 03, 01)); + assertThat(new CronExpression("0 * 3 * * *").nextTimeAfter(new DateTime(2012, 4, 11, 03, 59))).isEqualTo(new DateTime(2012, 4, 12, 03, 00)); + } + + @Test + public void check_hour_increment() throws Exception { + assertThat(new CronExpression("0 * 0/15 * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00))).isEqualTo(new DateTime(2012, 4, 10, 15, 00)); + assertThat(new CronExpression("0 * 0/15 * * *").nextTimeAfter(new DateTime(2012, 4, 10, 15, 00))).isEqualTo(new DateTime(2012, 4, 10, 15, 01)); + assertThat(new CronExpression("0 * 0/15 * * *").nextTimeAfter(new DateTime(2012, 4, 10, 15, 59))).isEqualTo(new DateTime(2012, 4, 11, 00, 00)); + assertThat(new CronExpression("0 * 0/15 * * *").nextTimeAfter(new DateTime(2012, 4, 11, 00, 00))).isEqualTo(new DateTime(2012, 4, 11, 00, 01)); + assertThat(new CronExpression("0 * 0/15 * * *").nextTimeAfter(new DateTime(2012, 4, 11, 15, 00))).isEqualTo(new DateTime(2012, 4, 11, 15, 01)); + } + + @Test + public void check_hour_list() throws Exception { + assertThat(new CronExpression("0 * 7,19 * * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00))).isEqualTo(new DateTime(2012, 4, 10, 19, 00)); + assertThat(new CronExpression("0 * 7,19 * * *").nextTimeAfter(new DateTime(2012, 4, 10, 19, 00))).isEqualTo(new DateTime(2012, 4, 10, 19, 01)); + assertThat(new CronExpression("0 * 7,19 * * *").nextTimeAfter(new DateTime(2012, 4, 10, 19, 59))).isEqualTo(new DateTime(2012, 4, 11, 07, 00)); + } + + @Test + public void check_hour_shall_run_25_times_in_DST_change_to_wintertime() throws Exception { + CronExpression cron = new CronExpression("0 1 * * * *"); + DateTime start = new DateTime(2011, 10, 30, 0, 0, 0, 0); + DateTime slutt = start.toLocalDate().plusDays(1).toDateTimeAtStartOfDay(); + DateTime tid = start; + assertThat(Hours.hoursBetween(start, slutt).getHours()).isEqualTo(25); + int count=0; + DateTime lastTime = tid; + while(tid.isBefore(slutt)){ + DateTime nextTime = cron.nextTimeAfter(tid); + assertThat(nextTime.isAfter(lastTime)).isTrue(); + lastTime = nextTime; + tid = tid.plusHours(1); + count++; + } + assertThat(count).isEqualTo(25); + } + + @Test + public void check_hour_shall_run_23_times_in_DST_change_to_summertime() throws Exception { + CronExpression cron = new CronExpression("0 0 * * * *"); + DateTime start = new DateTime(2011, 03, 27, 0, 0, 0, 0); + DateTime slutt = start.toLocalDate().plusDays(1).toDateTimeAtStartOfDay(); + DateTime tid = start; + assertThat(Hours.hoursBetween(start, slutt).getHours()).isEqualTo(23); + int count=0; + DateTime lastTime = tid; + while(tid.isBefore(slutt)){ + DateTime nextTime = cron.nextTimeAfter(tid); + assertThat(nextTime.isAfter(lastTime)).isTrue(); + lastTime = nextTime; + tid = tid.plusHours(1); + count++; + } + assertThat(count).isEqualTo(23); + } + + @Test + public void check_dayOfMonth_number() throws Exception { + assertThat(new CronExpression("0 * * 3 * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00))).isEqualTo(new DateTime(2012, 5, 03, 00, 00)); + assertThat(new CronExpression("0 * * 3 * *").nextTimeAfter(new DateTime(2012, 5, 03, 00, 00))).isEqualTo(new DateTime(2012, 5, 03, 00, 01)); + assertThat(new CronExpression("0 * * 3 * *").nextTimeAfter(new DateTime(2012, 5, 03, 00, 59))).isEqualTo(new DateTime(2012, 5, 03, 01, 00)); + assertThat(new CronExpression("0 * * 3 * *").nextTimeAfter(new DateTime(2012, 5, 03, 23, 59))).isEqualTo(new DateTime(2012, 6, 03, 00, 00)); + } + + @Test + public void check_dayOfMonth_increment() throws Exception { + assertThat(new CronExpression("0 0 0 1/15 * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00))).isEqualTo(new DateTime(2012, 4, 16, 00, 00)); + assertThat(new CronExpression("0 0 0 1/15 * *").nextTimeAfter(new DateTime(2012, 4, 16, 00, 00))).isEqualTo(new DateTime(2012, 5, 01, 00, 00)); + assertThat(new CronExpression("0 0 0 1/15 * *").nextTimeAfter(new DateTime(2012, 4, 30, 00, 00))).isEqualTo(new DateTime(2012, 5, 01, 00, 00)); + assertThat(new CronExpression("0 0 0 1/15 * *").nextTimeAfter(new DateTime(2012, 5, 01, 00, 00))).isEqualTo(new DateTime(2012, 5, 16, 00, 00)); + } + + @Test + public void check_dayOfMonth_list() throws Exception { + assertThat(new CronExpression("0 0 0 7,19 * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00))).isEqualTo(new DateTime(2012, 4, 19, 00, 00)); + assertThat(new CronExpression("0 0 0 7,19 * *").nextTimeAfter(new DateTime(2012, 4, 19, 00, 00))).isEqualTo(new DateTime(2012, 5, 07, 00, 00)); + assertThat(new CronExpression("0 0 0 7,19 * *").nextTimeAfter(new DateTime(2012, 5, 07, 00, 00))).isEqualTo(new DateTime(2012, 5, 19, 00, 00)); + assertThat(new CronExpression("0 0 0 7,19 * *").nextTimeAfter(new DateTime(2012, 5, 30, 00, 00))).isEqualTo(new DateTime(2012, 6, 07, 00, 00)); + } + + @Test + public void check_dayOfMonth_last() throws Exception { + assertThat(new CronExpression("0 0 0 L * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00))).isEqualTo(new DateTime(2012, 4, 30, 00, 00)); + assertThat(new CronExpression("0 0 0 L * *").nextTimeAfter(new DateTime(2012, 2, 12, 00, 00))).isEqualTo(new DateTime(2012, 2, 29, 00, 00)); + } + + @Test + public void check_dayOfMonth_number_last_L() throws Exception { + assertThat(new CronExpression("0 0 0 3L * *").nextTimeAfter(new DateTime(2012, 4, 10, 13, 00))).isEqualTo(new DateTime(2012, 4, 30 - 3, 00, 00)); + assertThat(new CronExpression("0 0 0 3L * *").nextTimeAfter(new DateTime(2012, 2, 12, 00, 00))).isEqualTo(new DateTime(2012, 2, 29 - 3, 00, 00)); + } + + @Test + public void check_dayOfMonth_closest_weekday_W() throws Exception { + // 9 - is weekday in may + assertThat(new CronExpression("0 0 0 9W * *").nextTimeAfter(new DateTime(2012, 5, 2, 00, 00))).isEqualTo(new DateTime(2012, 5, 9, 00, 00)); + + // 9 - is weekday in may + assertThat(new CronExpression("0 0 0 9W * *").nextTimeAfter(new DateTime(2012, 5, 8, 00, 00))).isEqualTo(new DateTime(2012, 5, 9, 00, 00)); + + // 9 - saturday, friday closest weekday in june + assertThat(new CronExpression("0 0 0 9W * *").nextTimeAfter(new DateTime(2012, 5, 9, 00, 00))).isEqualTo(new DateTime(2012, 6, 8, 00, 00)); + + // 9 - sunday, monday closest weekday in september + assertThat(new CronExpression("0 0 0 9W * *").nextTimeAfter(new DateTime(2012, 9, 1, 00, 00))).isEqualTo(new DateTime(2012, 9, 10, 00, 00)); + } + + @Test(expected = IllegalArgumentException.class) + public void check_dayOfMonth_invalid_modifier() throws Exception { + new CronExpression("0 0 0 9X * *"); + } + + @Test(expected = IllegalArgumentException.class) + public void check_dayOfMonth_invalid_increment_modifier() throws Exception { + new CronExpression("0 0 0 9#2 * *"); + } + + @Test + public void check_month_number() throws Exception { + assertThat(new CronExpression("0 0 0 1 5 *").nextTimeAfter(new DateTime(2012, 2, 12, 00, 00))).isEqualTo(new DateTime(2012, 5, 1, 00, 00)); + } + + @Test + public void check_month_increment() throws Exception { + assertThat(new CronExpression("0 0 0 1 5/2 *").nextTimeAfter(new DateTime(2012, 2, 12, 00, 00))).isEqualTo(new DateTime(2012, 5, 1, 00, 00)); + assertThat(new CronExpression("0 0 0 1 5/2 *").nextTimeAfter(new DateTime(2012, 5, 1, 00, 00))).isEqualTo(new DateTime(2012, 7, 1, 00, 00)); + + // if rolling over year then reset month field (cron rules - increments only affect own field) + assertThat(new CronExpression("0 0 0 1 5/10 *").nextTimeAfter(new DateTime(2012, 5, 1, 00, 00))).isEqualTo(new DateTime(2013, 5, 1, 00, 00)); + } + + @Test + public void check_month_list() throws Exception { + assertThat(new CronExpression("0 0 0 1 3,7,12 *").nextTimeAfter(new DateTime(2012, 2, 12, 00, 00))).isEqualTo(new DateTime(2012, 3, 1, 00, 00)); + assertThat(new CronExpression("0 0 0 1 3,7,12 *").nextTimeAfter(new DateTime(2012, 3, 1, 00, 00))).isEqualTo(new DateTime(2012, 7, 1, 00, 00)); + assertThat(new CronExpression("0 0 0 1 3,7,12 *").nextTimeAfter(new DateTime(2012, 7, 1, 00, 00))).isEqualTo(new DateTime(2012, 12, 1, 00, 00)); + } + + @Test + public void check_month_list_by_name() throws Exception { + assertThat(new CronExpression("0 0 0 1 MAR,JUL,DEC *").nextTimeAfter(new DateTime(2012, 2, 12, 00, 00))).isEqualTo(new DateTime(2012, 3, 1, 00, 00)); + assertThat(new CronExpression("0 0 0 1 MAR,JUL,DEC *").nextTimeAfter(new DateTime(2012, 3, 1, 00, 00))).isEqualTo(new DateTime(2012, 7, 1, 00, 00)); + assertThat(new CronExpression("0 0 0 1 MAR,JUL,DEC *").nextTimeAfter(new DateTime(2012, 7, 1, 00, 00))).isEqualTo(new DateTime(2012, 12, 1, 00, 00)); + } + + @Test(expected = IllegalArgumentException.class) + public void check_month_invalid_modifier() throws Exception { + new CronExpression("0 0 0 1 ? *"); + } + + @Test + public void check_dayOfWeek_number() throws Exception { + assertThat(new CronExpression("0 0 0 * * 3").nextTimeAfter(new DateTime(2012, 4, 1, 00, 00))).isEqualTo(new DateTime(2012, 4, 4, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 3").nextTimeAfter(new DateTime(2012, 4, 4, 00, 00))).isEqualTo(new DateTime(2012, 4, 11, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 3").nextTimeAfter(new DateTime(2012, 4, 12, 00, 00))).isEqualTo(new DateTime(2012, 4, 18, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 3").nextTimeAfter(new DateTime(2012, 4, 18, 00, 00))).isEqualTo(new DateTime(2012, 4, 25, 00, 00)); + } + + @Test + public void check_dayOfWeek_increment() throws Exception { + assertThat(new CronExpression("0 0 0 * * 3/2").nextTimeAfter(new DateTime(2012, 4, 1, 00, 00))).isEqualTo(new DateTime(2012, 4, 4, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 3/2").nextTimeAfter(new DateTime(2012, 4, 4, 00, 00))).isEqualTo(new DateTime(2012, 4, 6, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 3/2").nextTimeAfter(new DateTime(2012, 4, 6, 00, 00))).isEqualTo(new DateTime(2012, 4, 8, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 3/2").nextTimeAfter(new DateTime(2012, 4, 8, 00, 00))).isEqualTo(new DateTime(2012, 4, 11, 00, 00)); + } + + @Test + public void check_dayOfWeek_list() throws Exception { + assertThat(new CronExpression("0 0 0 * * 1,5,7").nextTimeAfter(new DateTime(2012, 4, 1, 00, 00))).isEqualTo(new DateTime(2012, 4, 2, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 1,5,7").nextTimeAfter(new DateTime(2012, 4, 2, 00, 00))).isEqualTo(new DateTime(2012, 4, 6, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 1,5,7").nextTimeAfter(new DateTime(2012, 4, 6, 00, 00))).isEqualTo(new DateTime(2012, 4, 8, 00, 00)); + } + + @Test + public void check_dayOfWeek_list_by_name() throws Exception { + assertThat(new CronExpression("0 0 0 * * MON,FRI,SUN").nextTimeAfter(new DateTime(2012, 4, 1, 00, 00))).isEqualTo(new DateTime(2012, 4, 2, 00, 00)); + assertThat(new CronExpression("0 0 0 * * MON,FRI,SUN").nextTimeAfter(new DateTime(2012, 4, 2, 00, 00))).isEqualTo(new DateTime(2012, 4, 6, 00, 00)); + assertThat(new CronExpression("0 0 0 * * MON,FRI,SUN").nextTimeAfter(new DateTime(2012, 4, 6, 00, 00))).isEqualTo(new DateTime(2012, 4, 8, 00, 00)); + } + + @Test + public void check_dayOfWeek_last_friday_in_month() throws Exception { + assertThat(new CronExpression("0 0 0 * * 5L").nextTimeAfter(new DateTime(2012, 4, 1, 00, 00))).isEqualTo(new DateTime(2012, 4, 27, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 5L").nextTimeAfter(new DateTime(2012, 4, 27, 00, 00))).isEqualTo(new DateTime(2012, 5, 25, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 5L").nextTimeAfter(new DateTime(2012, 2, 6, 00, 00))).isEqualTo(new DateTime(2012, 2, 24, 00, 00)); + assertThat(new CronExpression("0 0 0 * * FRIL").nextTimeAfter(new DateTime(2012, 2, 6, 00, 00))).isEqualTo(new DateTime(2012, 2, 24, 00, 00)); + } + + @Test(expected = IllegalArgumentException.class) + public void check_dayOfWeek_invalid_modifier() throws Exception { + new CronExpression("0 0 0 * * 5W"); + } + + @Test(expected = IllegalArgumentException.class) + public void check_dayOfWeek_invalid_increment_modifier() throws Exception { + new CronExpression("0 0 0 * * 5?3"); + } + + @Test + public void check_dayOfWeek_shall_interpret_0_as_sunday() throws Exception { + assertThat(new CronExpression("0 0 0 * * 0").nextTimeAfter(new DateTime(2012, 4, 1, 00, 00))).isEqualTo(new DateTime(2012, 4, 8, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 0L").nextTimeAfter(new DateTime(2012, 4, 1, 00, 00))).isEqualTo(new DateTime(2012, 4, 29, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 0#2").nextTimeAfter(new DateTime(2012, 4, 1, 00, 00))).isEqualTo(new DateTime(2012, 4, 8, 00, 00)); + } + + @Test + public void check_dayOfWeek_shall_interpret_7_as_sunday() throws Exception { + assertThat(new CronExpression("0 0 0 * * 7").nextTimeAfter(new DateTime(2012, 4, 1, 00, 00))).isEqualTo(new DateTime(2012, 4, 8, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 7L").nextTimeAfter(new DateTime(2012, 4, 1, 00, 00))).isEqualTo(new DateTime(2012, 4, 29, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 7#2").nextTimeAfter(new DateTime(2012, 4, 1, 00, 00))).isEqualTo(new DateTime(2012, 4, 8, 00, 00)); + } + + @Test + public void check_dayOfWeek_nth_friday_in_month() throws Exception { + assertThat(new CronExpression("0 0 0 * * 5#3").nextTimeAfter(new DateTime(2012, 4, 1, 00, 00))).isEqualTo(new DateTime(2012, 4, 20, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 5#3").nextTimeAfter(new DateTime(2012, 4, 20, 00, 00))).isEqualTo(new DateTime(2012, 5, 18, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 7#1").nextTimeAfter(new DateTime(2012, 3, 30, 00, 00))).isEqualTo(new DateTime(2012, 4, 1, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 7#1").nextTimeAfter(new DateTime(2012, 4, 1, 00, 00))).isEqualTo(new DateTime(2012, 5, 6, 00, 00)); + assertThat(new CronExpression("0 0 0 * * 3#5").nextTimeAfter(new DateTime(2012, 2, 6, 00, 00))).isEqualTo(new DateTime(2012, 2, 29, 00, 00)); // leapday + assertThat(new CronExpression("0 0 0 * * WED#5").nextTimeAfter(new DateTime(2012, 2, 6, 00, 00))).isEqualTo(new DateTime(2012, 2, 29, 00, 00)); // leapday + } + + @Test(expected = IllegalArgumentException.class) + public void shall_not_not_support_rolling_period() throws Exception { + new CronExpression("* * 5-1 * * *"); + } +}