Skip to content

Commit

Permalink
Fixes for Issue #241:
Browse files Browse the repository at this point in the history
-Fixed ByYearDay support for leap years.
-Fixed ByMonthDay, ByYearDay and ByWeekNo support for negative offsets.
-Fixed ByWeekNo support to preserve DayOfWeek from seed if ByDay is
not specified.
  • Loading branch information
Fr Jeremy Krieg committed Jun 25, 2018
1 parent 35ad1ab commit a29dd04
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 19 deletions.
96 changes: 77 additions & 19 deletions src/main/java/net/fortuna/ical4j/model/Recur.java
Expand Up @@ -579,7 +579,8 @@ public final DateList getDates(final Date seed, final Date periodStart,
}
}
Calendar cal = getCalendarInstance(seed, true);

final Calendar rootSeed = (Calendar)cal.clone();

// optimize the start time for selecting candidates
// (only applicable where a COUNT is not specified)
if (getCount() < 1) {
Expand Down Expand Up @@ -624,7 +625,11 @@ public final DateList getDates(final Date seed, final Date periodStart,
}
}

final DateList candidates = getCandidates(candidateSeed, value);
// rootSeed = date used for the seed for the RRule at the
// start of the first period.
// candidateSeed = date used for the start of
// the current period.
final DateList candidates = getCandidates(rootSeed, candidateSeed, value);
if (!candidates.isEmpty()) {
noCandidateIncrementCount = 0;
// sort candidates for identifying when UNTIL date is exceeded..
Expand Down Expand Up @@ -676,6 +681,7 @@ public final DateList getDates(final Date seed, final Date periodStart,
public final Date getNextDate(final Date seed, final Date startDate) {

final Calendar cal = getCalendarInstance(seed, true);
final Calendar rootSeed = (Calendar)cal.clone();

// optimize the start time for selecting candidates
// (only applicable where a COUNT is not specified)
Expand Down Expand Up @@ -711,7 +717,7 @@ public final Date getNextDate(final Date seed, final Date startDate) {
}
}

final DateList candidates = getCandidates(candidateSeed, value);
final DateList candidates = getCandidates(rootSeed, candidateSeed, value);
if (!candidates.isEmpty()) {
noCandidateIncrementCount = 0;
// sort candidates for identifying when UNTIL date is exceeded..
Expand Down Expand Up @@ -785,7 +791,7 @@ private Calendar smartIncrement(final Calendar cal) {
* @param value the type of date list to return
* @return a DateList
*/
private DateList getCandidates(final Date date, final Value value) {
private DateList getCandidates(final Calendar rootSeed, final Date date, final Value value) {
DateList dates = new DateList(value);
if (date instanceof DateTime) {
if (((DateTime) date).isUtc()) {
Expand All @@ -800,7 +806,7 @@ private DateList getCandidates(final Date date, final Value value) {
if (log.isDebugEnabled()) {
log.debug("Dates after BYMONTH processing: " + dates);
}
dates = getWeekNoVariants(dates);
dates = getWeekNoVariants(rootSeed, dates);
// debugging..
if (log.isDebugEnabled()) {
log.debug("Dates after BYWEEKNO processing: " + dates);
Expand Down Expand Up @@ -902,18 +908,41 @@ private DateList getMonthVariants(final DateList dates) {
* Applies BYWEEKNO rules specified in this Recur instance to the specified date list. If no BYWEEKNO rules are
* specified the date list is returned unmodified.
*
* @param rootSeed the seed date supplied to the initial calculation.
* @param dates
* @return
*/
private DateList getWeekNoVariants(final DateList dates) {
private DateList getWeekNoVariants(final Calendar rootSeed, final DateList dates) {
if (getWeekNoList().isEmpty()) {
return dates;
}
final int initDayOfWeek = rootSeed.get(Calendar.DAY_OF_WEEK);
final DateList weekNoDates = getDateListInstance(dates);
for (final Date date : dates) {
final Calendar cal = getCalendarInstance(date, true);
final Calendar initCal = getCalendarInstance(date, true);
final int numWeeksInYear = initCal.getActualMaximum(Calendar.WEEK_OF_YEAR);

for (final Integer weekNo : getWeekNoList()) {
cal.set(Calendar.WEEK_OF_YEAR, Dates.getAbsWeekNo(cal.getTime(), weekNo));
if (weekNo == 0 || weekNo < -Dates.MAX_WEEKS_PER_YEAR || weekNo > Dates.MAX_WEEKS_PER_YEAR) {
if (log.isTraceEnabled()) {
log.trace("Invalid week of year: " + weekNo);
}
continue;
}
final Calendar cal = (Calendar)initCal.clone();
if (weekNo > 0) {
if (numWeeksInYear < weekNo) {
continue;
}
cal.set(Calendar.WEEK_OF_YEAR, weekNo);
} else {
if (numWeeksInYear < -weekNo) {
continue;
}
cal.set(Calendar.WEEK_OF_YEAR, numWeeksInYear);
cal.add(Calendar.WEEK_OF_YEAR, weekNo + 1);
}
cal.set(Calendar.DAY_OF_WEEK, initDayOfWeek);
weekNoDates.add(Dates.getInstance(cal.getTime(), weekNoDates.getType()));
}
}
Expand All @@ -933,10 +962,28 @@ private DateList getYearDayVariants(final DateList dates) {
}
final DateList yearDayDates = getDateListInstance(dates);
for (final Date date : dates) {
final Calendar cal = getCalendarInstance(date, true);
for (final Integer yearDay : getYearDayList()) {
cal.set(Calendar.DAY_OF_YEAR, Dates.getAbsYearDay(cal.getTime(), yearDay));
yearDayDates.add(Dates.getInstance(cal.getTime(), yearDayDates.getType()));
final Calendar cal = getCalendarInstance(date, false);
for (final int yearDay : getYearDayList()) {
if (yearDay == 0 || yearDay < -Dates.MAX_DAYS_PER_YEAR || yearDay > Dates.MAX_DAYS_PER_YEAR) {
if (log.isTraceEnabled()) {
log.trace("Invalid day of year: " + yearDay);
}
continue;
}
final int numDaysInYear = cal.getActualMaximum(Calendar.DAY_OF_YEAR);
if (yearDay > 0) {
if (numDaysInYear < yearDay) {
continue;
}
cal.set(Calendar.DAY_OF_YEAR, yearDay);
} else {
if (numDaysInYear < -yearDay) {
continue;
}
cal.set(Calendar.DAY_OF_YEAR, numDaysInYear);
cal.add(Calendar.DAY_OF_YEAR, yearDay + 1);
}
yearDayDates.add(Dates.getInstance(cal.getTime(), yearDayDates.getType()));
}
}
return yearDayDates;
Expand All @@ -956,16 +1003,27 @@ private DateList getMonthDayVariants(final DateList dates) {
final DateList monthDayDates = getDateListInstance(dates);
for (final Date date : dates) {
final Calendar cal = getCalendarInstance(date, false);
for (final Integer monthDay : getMonthDayList()) {
try {
cal.set(Calendar.DAY_OF_MONTH, Dates.getAbsMonthDay(cal.getTime(), monthDay));
monthDayDates.add(Dates.getInstance(cal.getTime(), monthDayDates.getType()));
} catch (IllegalArgumentException iae) {
for (final int monthDay : getMonthDayList()) {
if (monthDay == 0 || monthDay < -Dates.MAX_DAYS_PER_MONTH || monthDay > Dates.MAX_DAYS_PER_MONTH) {
if (log.isTraceEnabled()) {
log.trace("Invalid day of month: " + Dates.getAbsMonthDay(cal
.getTime(), monthDay));
log.trace("Invalid day of month: " + monthDay);
}
continue;
}
final int numDaysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
if (monthDay > 0) {
if (numDaysInMonth < monthDay) {
continue;
}
cal.set(Calendar.DAY_OF_MONTH, monthDay);
} else {
if (numDaysInMonth < -monthDay) {
continue;
}
cal.set(Calendar.DAY_OF_MONTH, numDaysInMonth);
cal.add(Calendar.DAY_OF_MONTH, monthDay + 1);
}
monthDayDates.add(Dates.getInstance(cal.getTime(), monthDayDates.getType()));
}
}
return monthDayDates;
Expand Down
107 changes: 107 additions & 0 deletions src/test/groovy/net/fortuna/ical4j/model/RecurSpec.groovy
Expand Up @@ -66,6 +66,113 @@ class RecurSpec extends Specification {
'FREQ=WEEKLY;BYDAY=MO' | '20110101' | '20110201' | ['20110103T000000', '20110110T000000', '20110117T000000', '20110124T000000', '20110131T000000']
}

@Unroll
def 'verify byweekno recurrence rules without byday: #rule wkst: #wkst'() {
setup: 'parse recurrence rule'
def recur = new Recur("FREQ=YEARLY;BYWEEKNO=$rule;WKST=$wkst")
def startDate = new Date(start)
def endDate = new Date(end)
def expectedDates = []
expected.each {
expectedDates << new Date(it)
}

expect:
recur.getDates(startDate, endDate, Value.DATE) == expectedDates

where:
rule | wkst | start | end || expected
'2,52,53' | 'MO' | '20110101' | '20131231' || ['20110115', '20111231', '20120114', '20121229', '20130112', '20131228']
// If WKST is Wed, then we'll have 53 weeks in 2011.
'2,52,53' | 'WE' | '20110101' | '20131231' || ['20110108', '20111224', '20111231', '20120114', '20121229', '20130112', '20131228']
'-2,-52,-53' | 'MO' | '20110101' | '20131231' || ['20110108', '20111224', '20120107', '20121222', '20130105', '20131221']
'-2,-52,-53' | 'WE' | '20110101' | '20131231' || ['20110101', '20110108', '20111224', '20120107', '20121222', '20130105', '20131221']
}

@Unroll
def 'verify byweekno recurrence rules: #rule wkst: #wkst byday: #byday'() {
setup: 'parse recurrence rule'
def recur = new Recur("FREQ=YEARLY;BYWEEKNO=$rule;WKST=$wkst;BYDAY=$byday")
def startDate = new Date(start)
def endDate = new Date(end)
def expectedDates = []
expected.each {
expectedDates << new Date(it)
}

expect:
recur.getDates(startDate, endDate, Value.DATE) == expectedDates

where:
rule | wkst | byday | start | end || expected
'2,52,53' | 'MO' | 'MO,TH' | '20110101' | '20131231' || ['20110110', '20110113', '20111226', '20111229', '20120109', '20120112', '20121224', '20121227', '20130107', '20130110', '20131223', '20131226']
// If WKST is Wed, then we'll have 53 weeks in 2011.
'2,52,53' | 'WE' | 'MO,TH' | '20110101' | '20131231' || ['20110106', '20110110', '20111222', '20111226', '20111229', '20120102', '20120112', '20120116', '20121227', '20121231', '20130110', '20130114', '20131226', '20131230']
}

@Unroll
def 'verify monthly bymonthday recurrence rules: #rule #year'() {
setup: 'parse recurrence rule'
def recur = new Recur("FREQ=MONTHLY;BYMONTHDAY=$rule")
def startDate = new Date("${year}${start}")
def endDate = new Date("${year}${end}")
def expectedDates = []
expected.each {
expectedDates << new Date("${year}${it}")
}

expect:
recur.getDates(startDate, endDate, Value.DATE) == expectedDates

where:
rule | year | start | end || expected
'2,29,30,31' | '2011' | '0101' | '0503' || ['0102', '0129', '0130', '0131', '0202', '0302', '0329', '0330', '0331', '0402', '0429', '0430', '0502']
'2,29,30,31' | '2012' | '0101' | '0503' || ['0102', '0129', '0130', '0131', '0202', '0229', '0302', '0329', '0330', '0331', '0402', '0429', '0430', '0502']
'-2,-29,-30,-31' | '2011' | '0101' | '0503' || ['0101', '0102', '0103', '0130', '0227', '0301', '0302', '0303', '0330', '0401', '0402', '0429', '0501', '0502']
'-2,-29,-30,-31' | '2012' | '0101' | '0503' || ['0101', '0102', '0103', '0130', '0201', '0228', '0301', '0302', '0303', '0330', '0401', '0402', '0429', '0501', '0502']
}

@Unroll
def 'verify yearly bymonthday recurrence rules: #rule #start'() {
setup: 'parse recurrence rule'
def recur = new Recur("FREQ=YEARLY;BYMONTHDAY=$rule")
def startDate = new Date(start)
def endDate = new Date(end)
def expectedDates = []
expected.each {
expectedDates << new Date(it)
}

expect:
recur.getDates(startDate, endDate, Value.DATE) == expectedDates

where:
rule | start | end || expected
'2,-1' | '20110101' | '20121231' || ['20110102', '20110131', '20120102', '20120131']
'2,-1' | '20110201' | '20121231' || ['20110202', '20110228', '20120202', '20120229']
}

@Unroll
def 'verify byyearday recurrence rules: #rule'() {
setup: 'parse recurrence rule'
def recur = new Recur("FREQ=YEARLY;BYYEARDAY=$rule")
def startDate = new Date(start)
def endDate = new Date(end)
def expectedDates = []
expected.each {
expectedDates << new Date(it)
}

expect:
recur.getDates(startDate, endDate, Value.DATE) == expectedDates

where:
rule | start | end || expected
'2,365,366' | '20110101' | '20131231' || ['20110102', '20111231', '20120102', '20121230', '20121231', '20130102']
'-1,-365,-366' | '20110101' | '20131231' || ['20110101', '20111231', '20120101', '20120102', '20121231', '20130101']
'2,32' | '20110101' | '20131231' || ['20110102', '20110201', '20120102', '20120201', '20130102', '20130201']
}

@Unroll
def 'verify recurrence rule in different locales: #rule'() {
setup: 'override platform default locale'
Expand Down

0 comments on commit a29dd04

Please sign in to comment.