diff options
Diffstat (limited to 'src/com/android/calendarcommon2/RecurrenceProcessor.java')
-rw-r--r-- | src/com/android/calendarcommon2/RecurrenceProcessor.java | 1314 |
1 files changed, 1314 insertions, 0 deletions
diff --git a/src/com/android/calendarcommon2/RecurrenceProcessor.java b/src/com/android/calendarcommon2/RecurrenceProcessor.java new file mode 100644 index 0000000..641a161 --- /dev/null +++ b/src/com/android/calendarcommon2/RecurrenceProcessor.java @@ -0,0 +1,1314 @@ +/* //device/content/providers/pim/RecurrenceProcessor.java +** +** Copyright 2006, The Android Open Source Project +** +** 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 com.android.calendarcommon2; + +import android.text.format.Time; +import android.util.Log; + +import java.util.TreeSet; + +public class RecurrenceProcessor +{ + // these are created once and reused. + private Time mIterator = new Time(Time.TIMEZONE_UTC); + private Time mUntil = new Time(Time.TIMEZONE_UTC); + private StringBuilder mStringBuilder = new StringBuilder(); + private Time mGenerated = new Time(Time.TIMEZONE_UTC); + private DaySet mDays = new DaySet(false); + // Give up after this many loops. This is roughly 1 second of expansion. + private static final int MAX_ALLOWED_ITERATIONS = 2000; + + public RecurrenceProcessor() + { + } + + private static final String TAG = "RecurrenceProcessor"; + + private static final boolean SPEW = false; + + /** + * 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. + * + * @param dtstart the time of the first occurrence + * @param recur the recurrence + * @return an estimate of the time (in UTC milliseconds) of the last + * occurrence, which may be greater than the actual last occurrence + * @throws DateException + */ + public long getLastOccurence(Time dtstart, + RecurrenceSet recur) throws DateException { + return getLastOccurence(dtstart, null /* no limit */, 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. + * + * @param dtstart the time of the first occurrence + * @param maxtime the max possible time of the last occurrence. null means no limit + * @param recur the recurrence + * @return an estimate of the time (in UTC milliseconds) of the last + * occurrence, which may be greater than the actual last occurrence + * @throws DateException + */ + public long getLastOccurence(Time dtstart, Time maxtime, + RecurrenceSet recur) throws DateException { + 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; + } + } + } + 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 || maxtime != 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 */, + (maxtime != null) ? + maxtime.toMillis(false /* use isDst */) : -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; + } + + /** + * a -- list of values + * N -- number of values to use in a + * v -- value to check for + */ + private static boolean listContains(int[] a, int N, int v) + { + for (int i=0; i<N; i++) { + if (a[i] == v) { + return true; + } + } + return false; + } + + /** + * 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 + */ + private static boolean listContains(int[] a, int N, int v, int max) + { + 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; + } + + /** + * 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 + */ + private static int filter(EventRecurrence r, Time iterator) + { + 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; + } + } + + if (r.bysetposCount > 0) { +bysetpos: + // BYSETPOS - we only handle rules like FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1 + if (freq == EventRecurrence.MONTHLY && r.bydayCount > 0) { + // Check for stuff like BYDAY=1TU + for (int i = r.bydayCount - 1; i >= 0; i--) { + if (r.bydayNum[i] != 0) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "BYSETPOS not supported with these rules: " + r); + } + break bysetpos; + } + } + if (!filterMonthlySetPos(r, iterator)) { + // Not allowed, filter it out. + return 9; + } + } else { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "BYSETPOS not supported with these rules: " + r); + } + } + // BYSETPOS was defined but we don't know how to handle it. Do no filtering based + // on it. + } + + // if we got to here, we didn't filter it out + return 0; + } + + /** + * Filters out instances that don't match the BYSETPOS clause of a monthly recurrence rule. + * This is an awkward and inefficient way to go about it. + * + * @returns true if this instance should be kept + */ + private static boolean filterMonthlySetPos(EventRecurrence r, Time instance) { + /* + * Compute the day of the week for the first day of the month. "instance" has a + * day number and a DotW, so we compute the DotW of the 1st from that. Note DotW + * is 0-6, where 0=SUNDAY. + * + * The basic calculation is to take the instance's "day of the week" number, subtract + * (day of the month - 1) mod 7, and then make sure it's positive. We can simplify + * that with some algebra. + */ + int dotw = (instance.weekDay - instance.monthDay + 36) % 7; + + /* + * The byday[] values are specified as bits, so we can just OR them all + * together. + */ + int bydayMask = 0; + for (int i = 0; i < r.bydayCount; i++) { + bydayMask |= r.byday[i]; + } + + /* + * Generate a set according to the BYDAY rules. For each day of the month, determine + * if its day of the week is included. If so, append it to the day set. + */ + int maxDay = instance.getActualMaximum(Time.MONTH_DAY); + int daySet[] = new int[maxDay]; + int daySetLength = 0; + + for (int md = 1; md <= maxDay; md++) { + // For each month day, see if it's part of the set. (This makes some assumptions + // about the exact form of the DotW constants.) + int dayBit = EventRecurrence.SU << dotw; + if ((bydayMask & dayBit) != 0) { + daySet[daySetLength++] = md; + } + + dotw++; + if (dotw == 7) + dotw = 0; + } + + /* + * Now walk through the BYSETPOS list and see if the instance is equal to any of the + * specified daySet entries. + */ + for (int i = r.bysetposCount - 1; i >= 0; i--) { + int index = r.bysetpos[i]; + if (index > 0) { + if (index > daySetLength) { + continue; // out of range + } + if (daySet[index-1] == instance.monthDay) { + return true; + } + } else if (index < 0) { + if (daySetLength + index < 0) { + continue; // out of range + } + if (daySet[daySetLength + index] == instance.monthDay) { + return true; + } + } else { + // should have been caught by parser + throw new RuntimeException("invalid bysetpos value"); + } + } + + return false; + } + + + private static final int USE_ITERATOR = 0; + private static final int USE_BYLIST = 1; + + /** + * Return whether we should make this list from the BYxxx list or + * from the component of the iterator. + */ + int generateByList(int count, int freq, int byFreq) + { + if (byFreq >= freq) { + return USE_ITERATOR; + } else { + if (count == 0) { + return USE_ITERATOR; + } else { + return USE_BYLIST; + } + } + } + + private static boolean useBYX(int freq, int freqConstant, int count) + { + return freq > freqConstant && count > 0; + } + + public static class DaySet + { + public DaySet(boolean zulu) + { + mTime = new Time(Time.TIMEZONE_UTC); + } + + void setRecurrence(EventRecurrence r) + { + mYear = 0; + mMonth = -1; + mR = r; + } + + boolean get(Time iterator, int day) + { + int realYear = iterator.year; + int realMonth = iterator.month; + + Time t = null; + + if (SPEW) { + Log.i(TAG, "get called with iterator=" + iterator + + " " + iterator.month + + "/" + iterator.monthDay + + "/" + iterator.year + " day=" + day); + } + if (day < 1 || day > 28) { + // if might be past the end of the month, we need to normalize it + t = mTime; + t.set(day, realMonth, realYear); + unsafeNormalize(t); + realYear = t.year; + realMonth = t.month; + day = t.monthDay; + if (SPEW) { + Log.i(TAG, "normalized t=" + t + " " + t.month + + "/" + t.monthDay + + "/" + t.year); + } + } + + /* + if (true || SPEW) { + Log.i(TAG, "set t=" + t + " " + realMonth + "/" + day + "/" + realYear); + } + */ + if (realYear != mYear || realMonth != mMonth) { + if (t == null) { + t = mTime; + t.set(day, realMonth, realYear); + unsafeNormalize(t); + if (SPEW) { + Log.i(TAG, "set t=" + t + " " + t.month + + "/" + t.monthDay + + "/" + t.year + + " realMonth=" + realMonth + " mMonth=" + mMonth); + } + } + mYear = realYear; + mMonth = realMonth; + mDays = generateDaysList(t, mR); + if (SPEW) { + Log.i(TAG, "generated days list"); + } + } + return (mDays & (1<<day)) != 0; + } + + /** + * Fill in a bit set containing the days of the month on which this + * will occur. + * + * Only call this if the r.freq > DAILY. Otherwise, we should be + * processing the BYDAY, BYMONTHDAY, etc. as filters instead. + * + * monthOffset may be -1, 0 or 1 + */ + private static int generateDaysList(Time generated, EventRecurrence r) + { + int days = 0; + + int i, count, v; + int[] byday, bydayNum, bymonthday; + int j, lastDayThisMonth; + int first; // Time.SUNDAY, etc + int k; + + lastDayThisMonth = generated.getActualMaximum(Time.MONTH_DAY); + + // BYDAY + count = r.bydayCount; + if (count > 0) { + // calculate the day of week for the first of this month (first) + j = generated.monthDay; + while (j >= 8) { + j -= 7; + } + first = generated.weekDay; + if (first >= j) { + first = first - j + 1; + } else { + first = first - j + 8; + } + + // What to do if the event is weekly: + // This isn't ideal, but we'll generate a month's worth of events + // and the code that calls this will only use the ones that matter + // for the current week. + byday = r.byday; + bydayNum = r.bydayNum; + for (i=0; i<count; i++) { + v = bydayNum[i]; + j = EventRecurrence.day2TimeDay(byday[i]) - first + 1; + if (j <= 0) { + j += 7; + } + if (v == 0) { + // v is 0, each day in the month/week + for (; j<=lastDayThisMonth; j+=7) { + if (SPEW) Log.i(TAG, "setting " + j + " for rule " + + v + "/" + EventRecurrence.day2TimeDay(byday[i])); + days |= 1 << j; + } + } + else if (v > 0) { + // v is positive, count from the beginning of the month + // -1 b/c the first one should add 0 + j += 7*(v-1); + if (j <= lastDayThisMonth) { + if (SPEW) Log.i(TAG, "setting " + j + " for rule " + + v + "/" + EventRecurrence.day2TimeDay(byday[i])); + // if it's impossible, we drop it + days |= 1 << j; + } + } + else { + // v is negative, count from the end of the month + // find the last one + for (; j<=lastDayThisMonth; j+=7) { + } + // v is negative + // should do +1 b/c the last one should add 0, but we also + // skipped the j -= 7 b/c the loop to find the last one + // overshot by one week + j += 7*v; + if (j >= 1) { + if (SPEW) Log.i(TAG, "setting " + j + " for rule " + + v + "/" + EventRecurrence.day2TimeDay(byday[i])); + days |= 1 << j; + } + } + } + } + + // BYMONTHDAY + // Q: What happens if we have BYMONTHDAY and BYDAY? + // A: I didn't see it in the spec, so in lieu of that, we'll + // intersect the two. That seems reasonable to me. + if (r.freq > EventRecurrence.WEEKLY) { + count = r.bymonthdayCount; + if (count != 0) { + bymonthday = r.bymonthday; + if (r.bydayCount == 0) { + for (i=0; i<count; i++) { + v = bymonthday[i]; + if (v >= 0) { + days |= 1 << v; + } else { + j = lastDayThisMonth + v + 1; // v is negative + if (j >= 1 && j <= lastDayThisMonth) { + days |= 1 << j; + } + } + } + } else { + // This is O(lastDayThisMonth*count), which is really + // O(count) with a decent sized constant. + for (j=1; j<=lastDayThisMonth; j++) { + next_day : { + if ((days&(1<<j)) != 0) { + for (i=0; i<count; i++) { + if (bymonthday[i] == j) { + break next_day; + } + } + days &= ~(1<<j); + } + } + } + } + } + } + return days; + } + + private EventRecurrence mR; + private int mDays; + private Time mTime; + private int mYear; + private int mMonth; + } + + /** + * 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. + * + * @param dtstart a Time object representing the first occurrence + * @param recur the recurrence rules, including RRULE, RDATES, EXRULE, and + * EXDATES + * @param rangeStartMillis the beginning of the range to expand, in UTC + * milliseconds + * @param rangeEndMillis the non-inclusive end of the range to expand, in + * UTC milliseconds; use -1 for the entire range. + * @return an array of dates, each date is in UTC milliseconds + * @throws DateException + * @throws android.util.TimeFormatException if recur cannot be parsed + */ + public long[] expand(Time dtstart, + RecurrenceSet recur, + long rangeStartMillis, + long rangeEndMillis) throws DateException { + 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; + } + + /** + * 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 Long.MAX_VALUE for rangeEndDateValue. If you pass + * Long.MAX_VALUE for rangeEnd, and the event doesn't have a COUNT or UNTIL field, + * you'll get a DateException. + * + * @param dtstart the dtstart date as defined in RFC2445. This + * {@link Time} should be in the timezone of the event. + * @param r the parsed recurrence, as defiend in RFC2445 + * @param rangeStartDateValue the first date-time you care about, inclusive + * @param rangeEndDateValue the last date-time you care about, not inclusive (so + * if you care about everything up through and including + * Dec 22 1995, set last to Dec 23, 1995 00:00:00 + * @param add Whether or not we should add to out, or remove from out. + * @param out the TreeSet you'd like to fill with the events + * @throws DateException + * @throws android.util.TimeFormatException if r cannot be parsed. + */ + 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 + 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) { + /* + * 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; + } + } + + /** + * 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. + * <p> + * This method also computes the weekDay and yearDay fields. + * + * <p> + * This method does not modify the fields isDst, or gmtOff. + */ + static void unsafeNormalize(Time date) { + 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); + } + + /** + * Returns true if the given year is a leap year. + * + * @param year the given year to test + * @return true if the given year is a leap year. + */ + static boolean isLeapYear(int year) { + return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)); + } + + /** + * Returns the number of days in the given year. + * + * @param year the given year + * @return the number of days in the given year. + */ + static int yearLength(int year) { + return isLeapYear(year) ? 366 : 365; + } + + private static final int[] DAYS_PER_MONTH = { 31, 28, 31, 30, 31, 30, 31, + 31, 30, 31, 30, 31 }; + private static final int[] DAYS_IN_YEAR_PRECEDING_MONTH = { 0, 31, 59, 90, + 120, 151, 180, 212, 243, 273, 304, 334 }; + + /** + * Returns the number of days in the given month of the given year. + * + * @param year the given year. + * @param month the given month in the range [0,11] + * @return the number of days in the given month of the given year. + */ + static int monthLength(int year, int month) { + int n = DAYS_PER_MONTH[month]; + if (n != 28) { + return n; + } + return isLeapYear(year) ? 29 : 28; + } + + /** + * Computes the weekday, a number in the range [0,6] where Sunday=0, from + * the given year, month, and day. + * + * @param year the year + * @param month the 0-based month in the range [0,11] + * @param day the 1-based day of the month in the range [1,31] + * @return the weekday, a number in the range [0,6] where Sunday=0 + */ + static int weekDay(int year, int month, int day) { + if (month <= 1) { + month += 12; + year -= 1; + } + return (day + (13 * month - 14) / 5 + year + year/4 - year/100 + year/400) % 7; + } + + /** + * Computes the 0-based "year day", given the year, month, and day. + * + * @param year the year + * @param month the 0-based month in the range [0,11] + * @param day the 1-based day in the range [1,31] + * @return the 0-based "year day", the number of days into the year + */ + static int yearDay(int year, int month, int day) { + int yearDay = DAYS_IN_YEAR_PRECEDING_MONTH[month] + day - 1; + if (month >= 2 && isLeapYear(year)) { + yearDay += 1; + } + return yearDay; + } + + /** + * 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. + * + * @param normalized a Time object whose date and time fields have been + * normalized + * @return a 64-bit long value that can be used for comparing and ordering + * dates and times represented by Time objects + */ + private static final long normDateTimeComparisonValue(Time normalized) { + // 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(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); + } +} |