RecurrenceProcessorpublic class RecurrenceProcessor extends Object
Fields Summary |
---|
private android.text.format.Time | mIterator | private android.text.format.Time | mUntil | private StringBuilder | mStringBuilder | private android.text.format.Time | mGenerated | private DaySet | mDays | private static final int | MAX_ALLOWED_ITERATIONS | private static final String | TAG | private static final boolean | SPEW | private static final int | USE_ITERATOR | private static final int | USE_BYLIST | private static final int[] | DAYS_PER_MONTH | private static final int[] | DAYS_IN_YEAR_PRECEDING_MONTH |
Constructors Summary |
---|
public RecurrenceProcessor()
|
Methods Summary |
---|
public long[] | expand(android.text.format.Time dtstart, android.pim.RecurrenceSet recur, long rangeStartMillis, long rangeEndMillis)Expands the recurrence within the given range using the given dtstart
value. Returns an array of longs where each element is a date in UTC
milliseconds. The return value is never null. If there are no dates
then an array of length zero is returned.Da
String timezone = dtstart.timezone;
mIterator.clear(timezone);
mGenerated.clear(timezone);
// We don't need to clear the mUntil (and it wouldn't do any good to
// do so) because the "until" date string is specified in UTC and that
// sets the timezone in the mUntil Time object.
mIterator.set(rangeStartMillis);
long rangeStartDateValue = normDateTimeComparisonValue(mIterator);
long rangeEndDateValue;
if (rangeEndMillis != -1) {
mIterator.set(rangeEndMillis);
rangeEndDateValue = normDateTimeComparisonValue(mIterator);
} else {
rangeEndDateValue = Long.MAX_VALUE;
}
TreeSet<Long> dtSet = new TreeSet<Long>();
if (recur.rrules != null) {
for (EventRecurrence rrule : recur.rrules) {
expand(dtstart, rrule, rangeStartDateValue,
rangeEndDateValue, true /* add */, dtSet);
}
}
if (recur.rdates != null) {
for (long dt : recur.rdates) {
// The dates are stored as milliseconds. We need to convert
// them to year/month/day values in the local timezone.
mIterator.set(dt);
long dtvalue = normDateTimeComparisonValue(mIterator);
dtSet.add(dtvalue);
}
}
if (recur.exrules != null) {
for (EventRecurrence exrule : recur.exrules) {
expand(dtstart, exrule, rangeStartDateValue,
rangeEndDateValue, false /* remove */, dtSet);
}
}
if (recur.exdates != null) {
for (long dt : recur.exdates) {
// The dates are stored as milliseconds. We need to convert
// them to year/month/day values in the local timezone.
mIterator.set(dt);
long dtvalue = normDateTimeComparisonValue(mIterator);
dtSet.remove(dtvalue);
}
}
if (dtSet.isEmpty()) {
// this can happen if the recurrence does not occur within the
// expansion window.
return new long[0];
}
// The values in dtSet are represented in a special form that is useful
// for fast comparisons and that is easy to generate from year/month/day
// values. We need to convert these to UTC milliseconds and also to
// ensure that the dates are valid.
int len = dtSet.size();
long[] dates = new long[len];
int i = 0;
for (Long val: dtSet) {
setTimeFromLongValue(mIterator, val);
dates[i++] = mIterator.toMillis(true /* ignore isDst */);
}
return dates;
| public void | expand(android.text.format.Time dtstart, android.pim.EventRecurrence r, long rangeStartDateValue, long rangeEndDateValue, boolean add, java.util.TreeSet out)Run the recurrence algorithm. Processes events defined in the local
timezone of the event. Return a list of iCalendar DATETIME
strings containing the start date/times of the occurrences; the output
times are defined in the local timezone of the event.
If you want all of the events, pass null for rangeEnd. If you pass
null for rangeEnd, and the event doesn't have a COUNT or UNTIL field,
you'll get a DateException.
unsafeNormalize(dtstart);
long dtstartDateValue = normDateTimeComparisonValue(dtstart);
int count = 0;
// add the dtstart instance to the recurrence, if within range.
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 N, i, v;
int a[];
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
throw new DateException("Recurrence processing stuck: " + r.toString());
}
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) {
int dow = iterator.weekDay;
dayIndex = iterator.monthDay - dow;
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
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).
if (!add) {
++count;
} else if (dtstartDateValue != genDateValue) {
++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;
}
| private static int | filter(android.pim.EventRecurrence r, android.text.format.Time iterator)Filter out the ones for events whose BYxxx rule is for
a period greater than or equal to the period of the FREQ.
Returns 0 if the event should not be filtered out
Returns something else (a rule number which is useful for debugging)
if the event should not be returned
boolean found;
int freq = r.freq;
if (EventRecurrence.MONTHLY >= freq) {
// BYMONTH
if (r.bymonthCount > 0) {
found = listContains(r.bymonth, r.bymonthCount,
iterator.month + 1);
if (!found) {
return 1;
}
}
}
if (EventRecurrence.WEEKLY >= freq) {
// BYWEEK -- this is just a guess. I wonder how many events
// acutally use BYWEEKNO.
if (r.byweeknoCount > 0) {
found = listContains(r.byweekno, r.byweeknoCount,
iterator.getWeekNumber(),
iterator.getActualMaximum(Time.WEEK_NUM));
if (!found) {
return 2;
}
}
}
if (EventRecurrence.DAILY >= freq) {
// BYYEARDAY
if (r.byyeardayCount > 0) {
found = listContains(r.byyearday, r.byyeardayCount,
iterator.yearDay, iterator.getActualMaximum(Time.YEAR_DAY));
if (!found) {
return 3;
}
}
// BYMONTHDAY
if (r.bymonthdayCount > 0 ) {
found = listContains(r.bymonthday, r.bymonthdayCount,
iterator.monthDay,
iterator.getActualMaximum(Time.MONTH_DAY));
if (!found) {
return 4;
}
}
// BYDAY -- when filtering, we ignore the number field, because it
// only is meaningful when creating more events.
byday:
if (r.bydayCount > 0) {
int a[] = r.byday;
int N = r.bydayCount;
int v = EventRecurrence.timeDay2Day(iterator.weekDay);
for (int i=0; i<N; i++) {
if (a[i] == v) {
break byday;
}
}
return 5;
}
}
if (EventRecurrence.HOURLY >= freq) {
// BYHOUR
found = listContains(r.byhour, r.byhourCount,
iterator.hour,
iterator.getActualMaximum(Time.HOUR));
if (!found) {
return 6;
}
}
if (EventRecurrence.MINUTELY >= freq) {
// BYMINUTE
found = listContains(r.byminute, r.byminuteCount,
iterator.minute,
iterator.getActualMaximum(Time.MINUTE));
if (!found) {
return 7;
}
}
if (EventRecurrence.SECONDLY >= freq) {
// BYSECOND
found = listContains(r.bysecond, r.bysecondCount,
iterator.second,
iterator.getActualMaximum(Time.SECOND));
if (!found) {
return 8;
}
}
// BYSETPOS -- we might have to do this by postprocessing
// the list
// if we got to here, we didn't filter it out
return 0;
| int | generateByList(int count, int freq, int byFreq)Return whether we should make this list from the BYxxx list or
from the component of the iterator.
if (byFreq >= freq) {
return USE_ITERATOR;
} else {
if (count == 0) {
return USE_ITERATOR;
} else {
return USE_BYLIST;
}
}
| public long | getLastOccurence(android.text.format.Time dtstart, android.pim.RecurrenceSet recur)Returns the time (millis since epoch) of the last occurrence,
or -1 if the event repeats forever. If there are no occurrences
(because the exrule or exdates cancel all the occurrences) and the
event does not repeat forever, then 0 is returned.
This computes a conservative estimate of the last occurrence. That is,
the time of the actual last occurrence might be earlier than the time
returned by this method.
long lastTime = -1;
boolean hasCount = false;
// first see if there are any "until"s specified. if so, use the latest
// until / rdate.
if (recur.rrules != null) {
for (EventRecurrence rrule : recur.rrules) {
if (rrule.count != 0) {
hasCount = true;
} else if (rrule.until != null) {
// according to RFC 2445, until must be in UTC.
mIterator.parse(rrule.until);
long untilTime = mIterator.toMillis(false /* use isDst */);
if (untilTime > lastTime) {
lastTime = untilTime;
}
} else {
// This rrule has no "count" or "until" so it repeats
// forever.
return -1;
}
}
if (lastTime != -1 && recur.rdates != null) {
for (long dt : recur.rdates) {
if (dt > lastTime) {
lastTime = dt;
}
}
}
// If there were only "until"s and no "count"s, then return the
// last "until" date or "rdate".
if (lastTime != -1 && !hasCount) {
return lastTime;
}
} else if (recur.rdates != null &&
recur.exrules == null && recur.exdates == null) {
// if there are only rdates, we can just pick the last one.
for (long dt : recur.rdates) {
if (dt > lastTime) {
lastTime = dt;
}
}
return lastTime;
}
// Expand the complete recurrence if there were any counts specified,
// or if there were rdates specified.
if (hasCount || recur.rdates != null) {
// The expansion might not contain any dates if the exrule or
// exdates cancel all the generated dates.
long[] dates = expand(dtstart, recur,
dtstart.toMillis(false /* use isDst */) /* range start */,
-1 /* range end */);
// The expansion might not contain any dates if exrule or exdates
// cancel all the generated dates.
if (dates.length == 0) {
return 0;
}
return dates[dates.length - 1];
}
return -1;
| static boolean | isLeapYear(int year)Returns true if the given year is a leap year.
return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0));
| private static boolean | listContains(int[] a, int N, int v)a -- list of values
N -- number of values to use in a
v -- value to check for
for (int i=0; i<N; i++) {
if (a[i] == v) {
return true;
}
}
return false;
| private static boolean | listContains(int[] a, int N, int v, int max)a -- list of values
N -- number of values to use in a
v -- value to check for
max -- if a value in a is negative, add that negative value
to max and compare that instead; this is how we deal with
negative numbers being offsets from the end value
for (int i=0; i<N; i++) {
int w = a[i];
if (w > 0) {
if (w == v) {
return true;
}
} else {
max += w; // w is negative
if (max == v) {
return true;
}
}
}
return false;
| static int | monthLength(int year, int month)Returns the number of days in the given month of the given year.
int n = DAYS_PER_MONTH[month];
if (n != 28) {
return n;
}
return isLeapYear(year) ? 29 : 28;
| private static final long | normDateTimeComparisonValue(android.text.format.Time normalized)Converts a normalized Time value to a 64-bit long. The mapping of Time
values to longs provides a total ordering on the Time values so that
two Time values can be compared efficiently by comparing their 64-bit
long values. This is faster than converting the Time values to UTC
millliseconds.
// 37 bits for the year, 4 bits for the month, 5 bits for the monthDay,
// 5 bits for the hour, 6 bits for the minute, 6 bits for the second.
return ((long)normalized.year << 26) + (normalized.month << 22)
+ (normalized.monthDay << 17) + (normalized.hour << 12)
+ (normalized.minute << 6) + normalized.second;
| private static final void | setTimeFromLongValue(android.text.format.Time date, long val)
date.year = (int) (val >> 26);
date.month = (int) (val >> 22) & 0xf;
date.monthDay = (int) (val >> 17) & 0x1f;
date.hour = (int) (val >> 12) & 0x1f;
date.minute = (int) (val >> 6) & 0x3f;
date.second = (int) (val & 0x3f);
| static void | unsafeNormalize(android.text.format.Time date)Normalizes the date fields to give a valid date, but if the time falls
in the invalid window during a transition out of Daylight Saving Time
when time jumps forward an hour, then the "normalized" value will be
invalid.
This method also computes the weekDay and yearDay fields.
This method does not modify the fields isDst, or gmtOff.
int second = date.second;
int minute = date.minute;
int hour = date.hour;
int monthDay = date.monthDay;
int month = date.month;
int year = date.year;
int addMinutes = ((second < 0) ? (second - 59) : second) / 60;
second -= addMinutes * 60;
minute += addMinutes;
int addHours = ((minute < 0) ? (minute - 59) : minute) / 60;
minute -= addHours * 60;
hour += addHours;
int addDays = ((hour < 0) ? (hour - 23) : hour) / 24;
hour -= addDays * 24;
monthDay += addDays;
// We want to make "monthDay" positive. We do this by subtracting one
// from the year and adding a year's worth of days to "monthDay" in
// the following loop while "monthDay" <= 0.
while (monthDay <= 0) {
// If month is after Feb, then add this year's length so that we
// include this year's leap day, if any.
// Otherwise (the month is Feb or earlier), add last year's length.
// Subtract one from the year in either case. This gives the same
// effective date but makes monthDay (the day of the month) much
// larger. Eventually (usually in one iteration) monthDay will
// be positive.
int days = month > 1 ? yearLength(year) : yearLength(year - 1);
monthDay += days;
year -= 1;
}
// At this point, monthDay >= 1. Normalize the month to the range [0,11].
if (month < 0) {
int years = (month + 1) / 12 - 1;
year += years;
month -= 12 * years;
} else if (month >= 12) {
int years = month / 12;
year += years;
month -= 12 * years;
}
// At this point, month is in the range [0,11] and monthDay >= 1.
// Now loop until the monthDay is in the correct range for the month.
while (true) {
// On January, check if we can jump forward a whole year.
if (month == 0) {
int yearLength = yearLength(year);
if (monthDay > yearLength) {
year++;
monthDay -= yearLength;
}
}
int monthLength = monthLength(year, month);
if (monthDay > monthLength) {
monthDay -= monthLength;
month++;
if (month >= 12) {
month -= 12;
year++;
}
} else break;
}
// At this point, monthDay <= the length of the current month and is
// in the range [1,31].
date.second = second;
date.minute = minute;
date.hour = hour;
date.monthDay = monthDay;
date.month = month;
date.year = year;
date.weekDay = weekDay(year, month, monthDay);
date.yearDay = yearDay(year, month, monthDay);
| private static boolean | useBYX(int freq, int freqConstant, int count)
return freq > freqConstant && count > 0;
| static int | weekDay(int year, int month, int day)Computes the weekday, a number in the range [0,6] where Sunday=0, from
the given year, month, and day.
if (month <= 1) {
month += 12;
year -= 1;
}
return (day + (13 * month - 14) / 5 + year + year/4 - year/100 + year/400) % 7;
| static int | yearDay(int year, int month, int day)Computes the 0-based "year day", given the year, month, and day.
int yearDay = DAYS_IN_YEAR_PRECEDING_MONTH[month] + day - 1;
if (month >= 2 && isLeapYear(year)) {
yearDay += 1;
}
return yearDay;
| static int | yearLength(int year)Returns the number of days in the given year.
return isLeapYear(year) ? 366 : 365;
|
|