in com/android/calendarcommon2/RecurrenceProcessor.java [733:1125]
public void expand(Time dtstart,
EventRecurrence r,
long rangeStartDateValue,
long rangeEndDateValue,
boolean add,
TreeSet<Long> out) throws DateException {
unsafeNormalize(dtstart);
long dtstartDateValue = normDateTimeComparisonValue(dtstart);
int count = 0;
// add the dtstart instance to the recurrence, if within range.
// For example, if dtstart is Mar 1, 2010 and the range is Jan 1 - Apr 1,
// then add it here and increment count. If the range is earlier or later,
// then don't add it here. In that case, count will be incremented later
// inside the loop. It is important that count gets incremented exactly
// once here or in the loop for dtstart.
//
// NOTE: if DTSTART is not synchronized with the recurrence rule, the first instance
// we return will not fit the RRULE pattern.
if (add && dtstartDateValue >= rangeStartDateValue
&& dtstartDateValue < rangeEndDateValue) {
out.add(dtstartDateValue);
++count;
}
Time iterator = mIterator;
Time until = mUntil;
StringBuilder sb = mStringBuilder;
Time generated = mGenerated;
DaySet days = mDays;
try {
days.setRecurrence(r);
if (rangeEndDateValue == Long.MAX_VALUE && r.until == null && r.count == 0) {
throw new DateException(
"No range end provided for a recurrence that has no UNTIL or COUNT.");
}
// the top-level frequency
int freqField;
int freqAmount = r.interval;
int freq = r.freq;
switch (freq)
{
case EventRecurrence.SECONDLY:
freqField = Time.SECOND;
break;
case EventRecurrence.MINUTELY:
freqField = Time.MINUTE;
break;
case EventRecurrence.HOURLY:
freqField = Time.HOUR;
break;
case EventRecurrence.DAILY:
freqField = Time.MONTH_DAY;
break;
case EventRecurrence.WEEKLY:
freqField = Time.MONTH_DAY;
freqAmount = 7 * r.interval;
if (freqAmount <= 0) {
freqAmount = 7;
}
break;
case EventRecurrence.MONTHLY:
freqField = Time.MONTH;
break;
case EventRecurrence.YEARLY:
freqField = Time.YEAR;
break;
default:
throw new DateException("bad freq=" + freq);
}
if (freqAmount <= 0) {
freqAmount = 1;
}
int bymonthCount = r.bymonthCount;
boolean usebymonth = useBYX(freq, EventRecurrence.MONTHLY, bymonthCount);
boolean useDays = freq >= EventRecurrence.WEEKLY &&
(r.bydayCount > 0 || r.bymonthdayCount > 0);
int byhourCount = r.byhourCount;
boolean usebyhour = useBYX(freq, EventRecurrence.HOURLY, byhourCount);
int byminuteCount = r.byminuteCount;
boolean usebyminute = useBYX(freq, EventRecurrence.MINUTELY, byminuteCount);
int bysecondCount = r.bysecondCount;
boolean usebysecond = useBYX(freq, EventRecurrence.SECONDLY, bysecondCount);
// initialize the iterator
iterator.set(dtstart);
if (freqField == Time.MONTH) {
if (useDays) {
// if it's monthly, and we're going to be generating
// days, set the iterator day field to 1 because sometimes
// we'll skip months if it's greater than 28.
// XXX Do we generate days for MONTHLY w/ BYHOUR? If so,
// we need to do this then too.
iterator.monthDay = 1;
}
}
long untilDateValue;
if (r.until != null) {
// Ensure that the "until" date string is specified in UTC.
String untilStr = r.until;
// 15 is length of date-time without trailing Z e.g. "20090204T075959"
// A string such as 20090204 is a valid UNTIL (see RFC 2445) and the
// Z should not be added.
if (untilStr.length() == 15) {
untilStr = untilStr + 'Z';
}
// The parse() method will set the timezone to UTC
until.parse(untilStr);
// We need the "until" year/month/day values to be in the same
// timezone as all the generated dates so that we can compare them
// using the values returned by normDateTimeComparisonValue().
until.switchTimezone(dtstart.timezone);
untilDateValue = normDateTimeComparisonValue(until);
} else {
untilDateValue = Long.MAX_VALUE;
}
sb.ensureCapacity(15);
sb.setLength(15); // TODO: pay attention to whether or not the event
// is an all-day one.
if (SPEW) {
Log.i(TAG, "expand called w/ rangeStart=" + rangeStartDateValue
+ " rangeEnd=" + rangeEndDateValue);
}
// go until the end of the range or we're done with this event
boolean eventEnded = false;
int failsafe = 0; // Avoid infinite loops
events: {
while (true) {
int monthIndex = 0;
if (failsafe++ > MAX_ALLOWED_ITERATIONS) { // Give up after about 1 second of processing
Log.w(TAG, "Recurrence processing stuck with r=" + r + " rangeStart="
+ rangeStartDateValue + " rangeEnd=" + rangeEndDateValue);
break;
}
unsafeNormalize(iterator);
int iteratorYear = iterator.year;
int iteratorMonth = iterator.month + 1;
int iteratorDay = iterator.monthDay;
int iteratorHour = iterator.hour;
int iteratorMinute = iterator.minute;
int iteratorSecond = iterator.second;
// year is never expanded -- there is no BYYEAR
generated.set(iterator);
if (SPEW) Log.i(TAG, "year=" + generated.year);
do { // month
int month = usebymonth
? r.bymonth[monthIndex]
: iteratorMonth;
month--;
if (SPEW) Log.i(TAG, " month=" + month);
int dayIndex = 1;
int lastDayToExamine = 0;
// Use this to handle weeks that overlap the end of the month.
// Keep the year and month that days is for, and generate it
// when needed in the loop
if (useDays) {
// Determine where to start and end, don't worry if this happens
// to be before dtstart or after the end, because that will be
// filtered in the inner loop
if (freq == EventRecurrence.WEEKLY) {
/*
* iterator.weekDay indicates the day of the week (0-6, SU-SA).
* Because dayIndex might start in the middle of a week, and we're
* interested in treating a week as a unit, we want to move
* backward to the start of the week. (This could make the
* dayIndex negative, which will be corrected by normalization
* later on.)
*
* The day that starts the week is determined by WKST, which
* defaults to MO.
*
* Example: dayIndex is Tuesday the 8th, and weeks start on
* Thursdays. Tuesday is day 2, Thursday is day 4, so we
* want to move back (2 - 4 + 7) % 7 = 5 days to the previous
* Thursday. If weeks started on Mondays, we would only
* need to move back (2 - 1 + 7) % 7 = 1 day.
*/
int weekStartAdj = (iterator.weekDay -
EventRecurrence.day2TimeDay(r.wkst) + 7) % 7;
dayIndex = iterator.monthDay - weekStartAdj;
lastDayToExamine = dayIndex + 6;
} else {
lastDayToExamine = generated
.getActualMaximum(Time.MONTH_DAY);
}
if (SPEW) Log.i(TAG, "dayIndex=" + dayIndex
+ " lastDayToExamine=" + lastDayToExamine
+ " days=" + days);
}
do { // day
int day;
if (useDays) {
if (!days.get(iterator, dayIndex)) {
dayIndex++;
continue;
} else {
day = dayIndex;
}
} else {
day = iteratorDay;
}
if (SPEW) Log.i(TAG, " day=" + day);
// hour
int hourIndex = 0;
do {
int hour = usebyhour
? r.byhour[hourIndex]
: iteratorHour;
if (SPEW) Log.i(TAG, " hour=" + hour + " usebyhour=" + usebyhour);
// minute
int minuteIndex = 0;
do {
int minute = usebyminute
? r.byminute[minuteIndex]
: iteratorMinute;
if (SPEW) Log.i(TAG, " minute=" + minute);
// second
int secondIndex = 0;
do {
int second = usebysecond
? r.bysecond[secondIndex]
: iteratorSecond;
if (SPEW) Log.i(TAG, " second=" + second);
// we do this here each time, because if we distribute it, we find the
// month advancing extra times, as we set the month to the 32nd, 33rd, etc.
// days.
generated.set(second, minute, hour, day, month, iteratorYear);
unsafeNormalize(generated);
long genDateValue = normDateTimeComparisonValue(generated);
// sometimes events get generated (BYDAY, BYHOUR, etc.) that
// are before dtstart. Filter these. I believe this is correct,
// but Google Calendar doesn't seem to always do this.
if (genDateValue >= dtstartDateValue) {
// filter and then add
// TODO: we don't check for stop conditions (like
// passing the "end" date) unless the filter
// allows the event. Could stop sooner.
int filtered = filter(r, generated);
if (0 == filtered) {
// increase the count as long
// as this isn't the same
// as the first instance
// specified by the DTSTART
// (for RRULEs -- additive).
// This condition must be the complement of the
// condition for incrementing count at the
// beginning of the method, so if we don't
// increment count there, we increment it here.
// For example, if add is set and dtstartDateValue
// is inside the start/end range, then it was added
// and count was incremented at the beginning.
// If dtstartDateValue is outside the range or add
// is not set, then we must increment count here.
if (!(dtstartDateValue == genDateValue
&& add
&& dtstartDateValue >= rangeStartDateValue
&& dtstartDateValue < rangeEndDateValue)) {
++count;
}
// one reason we can stop is that
// we're past the until date
if (genDateValue > untilDateValue) {
if (SPEW) {
Log.i(TAG, "stopping b/c until="
+ untilDateValue
+ " generated="
+ genDateValue);
}
break events;
}
// or we're past rangeEnd
if (genDateValue >= rangeEndDateValue) {
if (SPEW) {
Log.i(TAG, "stopping b/c rangeEnd="
+ rangeEndDateValue
+ " generated=" + generated);
}
break events;
}
if (genDateValue >= rangeStartDateValue) {
if (SPEW) {
Log.i(TAG, "adding date=" + generated + " filtered=" + filtered);
}
if (add) {
out.add(genDateValue);
} else {
out.remove(genDateValue);
}
}
// another is that count is high enough
if (r.count > 0 && r.count == count) {
//Log.i(TAG, "stopping b/c count=" + count);
break events;
}
}
}
secondIndex++;
} while (usebysecond && secondIndex < bysecondCount);
minuteIndex++;
} while (usebyminute && minuteIndex < byminuteCount);
hourIndex++;
} while (usebyhour && hourIndex < byhourCount);
dayIndex++;
} while (useDays && dayIndex <= lastDayToExamine);
monthIndex++;
} while (usebymonth && monthIndex < bymonthCount);
// Add freqAmount to freqField until we get another date that we want.
// We don't want to "generate" dates with the iterator.
// XXX: We do this for days, because there is a varying number of days
// per month
int oldDay = iterator.monthDay;
generated.set(iterator); // just using generated as a temporary.
int n = 1;
while (true) {
int value = freqAmount * n;
switch (freqField) {
case Time.SECOND:
iterator.second += value;
break;
case Time.MINUTE:
iterator.minute += value;
break;
case Time.HOUR:
iterator.hour += value;
break;
case Time.MONTH_DAY:
iterator.monthDay += value;
break;
case Time.MONTH:
iterator.month += value;
break;
case Time.YEAR:
iterator.year += value;
break;
case Time.WEEK_DAY:
iterator.monthDay += value;
break;
case Time.YEAR_DAY:
iterator.monthDay += value;
break;
default:
throw new RuntimeException("bad field=" + freqField);
}
unsafeNormalize(iterator);
if (freqField != Time.YEAR && freqField != Time.MONTH) {
break;
}
if (iterator.monthDay == oldDay) {
break;
}
n++;
iterator.set(generated);
}
}
}
}
catch (DateException e) {
Log.w(TAG, "DateException with r=" + r + " rangeStart=" + rangeStartDateValue
+ " rangeEnd=" + rangeEndDateValue);
throw e;
}
catch (RuntimeException t) {
Log.w(TAG, "RuntimeException with r=" + r + " rangeStart=" + rangeStartDateValue
+ " rangeEnd=" + rangeEndDateValue);
throw t;
}
}