public static RecurrenceIterator createRecurrenceIterator(
RRule rrule, DateValue dtStart, TimeZone tzid) {
assert null != tzid;
assert null != dtStart;
Frequency freq = rrule.getFreq();
Weekday wkst = rrule.getWkSt();
DateValue untilUtc = rrule.getUntil();
int count = rrule.getCount();
int interval = rrule.getInterval();
WeekdayNum[] byDay = rrule.getByDay().toArray(new WeekdayNum[0]);
int[] byMonth = rrule.getByMonth();
int[] byMonthDay = rrule.getByMonthDay();
int[] byWeekNo = rrule.getByWeekNo();
int[] byYearDay = rrule.getByYearDay();
int[] bySetPos = rrule.getBySetPos();
int[] byHour = rrule.getByHour();
int[] byMinute = rrule.getByMinute();
int[] bySecond = rrule.getBySecond();
if (interval <= 0) { interval = 1; }
if (null == wkst) {
wkst = Weekday.MO;
}
// Optimize out BYSETPOS where possible.
if (bySetPos.length != 0) {
switch (freq) {
case HOURLY:
// ;BYHOUR=3,6,9;BYSETPOS=-1,1
// is equivalent to
// ;BYHOUR=3,9
if (byHour.length != 0 && byMinute.length <= 1
&& bySecond.length <= 1) {
byHour = filterBySetPos(byHour, bySetPos);
}
// Handling bySetPos for rules that are more frequent than daily
// tends to lead to large amounts of processor being used before other
// work limiting features can kick in since there many seconds between
// dtStart and where the year limit kicks in.
// There are no known use cases for the use of bySetPos with hourly
// minutely and secondly rules so we just ignore it.
bySetPos = NO_INTS;
break;
case MINUTELY:
// ;BYHOUR=3,6,9;BYSETPOS=-1,1
// is equivalent to
// ;BYHOUR=3,9
if (byMinute.length != 0 && bySecond.length <= 1) {
byMinute = filterBySetPos(byMinute, bySetPos);
}
// See bySetPos handling comment above.
bySetPos = NO_INTS;
break;
case SECONDLY:
// ;BYHOUR=3,6,9;BYSETPOS=-1,1
// is equivalent to
// ;BYHOUR=3,9
if (bySecond.length != 0) {
bySecond = filterBySetPos(bySecond, bySetPos);
}
// See bySetPos handling comment above.
bySetPos = NO_INTS;
break;
default:
}
}
DateValue start = dtStart;
if (bySetPos.length != 0) {
// Roll back till the beginning of the period to make sure that any
// positive indices are indexed properly.
// The actual iterator implementation is responsible for anything
// < dtStart.
switch (freq) {
case YEARLY:
start = dtStart instanceof TimeValue
? new DateTimeValueImpl(start.year(), 1, 1, 0, 0, 0)
: new DateValueImpl(start.year(), 1, 1);
break;
case MONTHLY:
start = dtStart instanceof TimeValue
? new DateTimeValueImpl(start.year(), start.month(), 1, 0, 0, 0)
: new DateValueImpl(start.year(), start.month(), 1);
break;
case WEEKLY:
int d = (7 + wkst.ordinal() - Weekday.valueOf(dtStart).ordinal()) % 7;
start = TimeUtils.add(dtStart, new DateValueImpl(0, 0, -d));
break;
default: break;
}
}
// recurrences are implemented as a sequence of periodic generators.
// First a year is generated, and then months, and within months, days
ThrottledGenerator yearGenerator = Generators.serialYearGenerator(
freq == Frequency.YEARLY ? interval : 1, dtStart);
Generator monthGenerator = null;
Generator dayGenerator = null;
Generator secondGenerator = null;
Generator minuteGenerator = null;
Generator hourGenerator = null;
// When multiple generators are specified for a period, they act as a union
// operator. We could have multiple generators (for day say) and then
// run each and merge the results, but some generators are more efficient
// than others, so to avoid generating 53 sundays and throwing away all but
// 1 for RRULE:FREQ=YEARLY;BYDAY=TU;BYWEEKNO=1, we reimplement some of the
// more prolific generators as filters.
// TODO(msamuel): don't need a list here
List<Predicate<? super DateValue>> filters =
new ArrayList<Predicate<? super DateValue>>();
switch (freq) {
case SECONDLY:
if (bySecond.length == 0 || interval != 1) {
secondGenerator = Generators.serialSecondGenerator(interval, dtStart);
if (bySecond.length != 0) {
filters.add(Filters.bySecondFilter(bySecond));
}
}
break;
case MINUTELY:
if (byMinute.length == 0 || interval != 1) {
minuteGenerator = Generators.serialMinuteGenerator(interval, dtStart);
if (byMinute.length != 0) {
filters.add(Filters.byMinuteFilter(byMinute));
}
}
break;
case HOURLY:
if (byHour.length == 0 || interval != 1) {
hourGenerator = Generators.serialHourGenerator(interval, dtStart);
if (byHour.length != 0) {
filters.add(Filters.byHourFilter(bySecond));
}
}
break;
case DAILY:
break;
case WEEKLY:
// week is not considered a period because a week may span multiple
// months &| years. There are no week generators, but so a filter is
// used to make sure that FREQ=WEEKLY;INTERVAL=2 only generates dates
// within the proper week.
if (0 != byDay.length) {
dayGenerator = Generators.byDayGenerator(byDay, false, start);
byDay = NO_DAYS;
if (interval > 1) {
filters.add(Filters.weekIntervalFilter(interval, wkst, dtStart));
}
} else {
dayGenerator = Generators.serialDayGenerator(interval * 7, dtStart);
}
break;
case YEARLY:
if (0 != byYearDay.length) {
// The BYYEARDAY rule part specifies a COMMA separated list of days of
// the year. Valid values are 1 to 366 or -366 to -1. For example, -1
// represents the last day of the year (December 31st) and -306
// represents the 306th to the last day of the year (March 1st).
dayGenerator = Generators.byYearDayGenerator(byYearDay, start);
break;
}
// $FALL-THROUGH$
case MONTHLY:
if (0 != byMonthDay.length) {
// The BYMONTHDAY rule part specifies a COMMA separated list of days
// of the month. Valid values are 1 to 31 or -31 to -1. For example,
// -10 represents the tenth to the last day of the month.
dayGenerator = Generators.byMonthDayGenerator(byMonthDay, start);
byMonthDay = NO_INTS;
} else if (0 != byWeekNo.length && Frequency.YEARLY == freq) {
// The BYWEEKNO rule part specifies a COMMA separated list of ordinals
// specifying weeks of the year. This rule part is only valid for
// YEARLY rules.
dayGenerator = Generators.byWeekNoGenerator(byWeekNo, wkst, start);
byWeekNo = NO_INTS;
} else if (0 != byDay.length) {
// Each BYDAY value can also be preceded by a positive (n) or negative
// (-n) integer. If present, this indicates the nth occurrence of the
// specific day within the MONTHLY or YEARLY RRULE. For example,
// within a MONTHLY rule, +1MO (or simply 1MO) represents the first
// Monday within the month, whereas -1MO represents the last Monday of
// the month. If an integer modifier is not present, it means all days
// of this type within the specified frequency. For example, within a
// MONTHLY rule, MO represents all Mondays within the month.
dayGenerator = Generators.byDayGenerator(
byDay, Frequency.YEARLY == freq && 0 == byMonth.length, start);
byDay = NO_DAYS;
} else {
if (Frequency.YEARLY == freq) {
monthGenerator = Generators.byMonthGenerator(
new int[] { dtStart.month() }, start);
}
dayGenerator = Generators.byMonthDayGenerator(
new int[] { dtStart.day() }, start);
}
break;
}
if (secondGenerator == null) {
secondGenerator = Generators.bySecondGenerator(bySecond, start);
}
if (minuteGenerator == null) {
if (byMinute.length == 0 && freq.compareTo(Frequency.MINUTELY) < 0) {
minuteGenerator = Generators.serialMinuteGenerator(1, dtStart);
} else {
minuteGenerator = Generators.byMinuteGenerator(byMinute, start);
}
}
if (hourGenerator == null) {
if (byHour.length == 0 && freq.compareTo(Frequency.HOURLY) < 0) {
hourGenerator = Generators.serialHourGenerator(1, dtStart);
} else {
hourGenerator = Generators.byHourGenerator(byHour, start);
}
}
if (dayGenerator == null) {
boolean dailyOrMoreOften = freq.compareTo(Frequency.DAILY) <= 0;
if (byMonthDay.length != 0) {
dayGenerator = Generators.byMonthDayGenerator(byMonthDay, start);
byMonthDay = NO_INTS;
} else if (byDay.length != 0) {
dayGenerator = Generators.byDayGenerator(