1 /* //device/content/providers/pim/RecurrenceProcessor.java
2 **
3 ** Copyright 2006, The Android Open Source Project
4 **
5 ** Licensed under the Apache License, Version 2.0 (the "License");
6 ** you may not use this file except in compliance with the License.
7 ** You may obtain a copy of the License at
8 **
9 **     http://www.apache.org/licenses/LICENSE-2.0
10 **
11 ** Unless required by applicable law or agreed to in writing, software
12 ** distributed under the License is distributed on an "AS IS" BASIS,
13 ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 ** See the License for the specific language governing permissions and
15 ** limitations under the License.
16 */
17 
18 package com.android.calendarcommon2;
19 
20 import android.util.Log;
21 
22 import java.util.TreeSet;
23 
24 public class RecurrenceProcessor
25 {
26     // these are created once and reused.
27     private Time mIterator = new Time(Time.TIMEZONE_UTC);
28     private Time mUntil = new Time(Time.TIMEZONE_UTC);
29     private StringBuilder mStringBuilder = new StringBuilder();
30     private Time mGenerated = new Time(Time.TIMEZONE_UTC);
31     private DaySet mDays = new DaySet(false);
32     // Give up after this many loops.  This is roughly 1 second of expansion.
33     private static final int MAX_ALLOWED_ITERATIONS = 2000;
34 
RecurrenceProcessor()35     public RecurrenceProcessor()
36     {
37     }
38 
39     private static final String TAG = "RecurrenceProcessor";
40 
41     private static final boolean SPEW = false;
42 
43     /**
44      * Returns the time (millis since epoch) of the last occurrence,
45      * or -1 if the event repeats forever.  If there are no occurrences
46      * (because the exrule or exdates cancel all the occurrences) and the
47      * event does not repeat forever, then 0 is returned.
48      *
49      * This computes a conservative estimate of the last occurrence. That is,
50      * the time of the actual last occurrence might be earlier than the time
51      * returned by this method.
52      *
53      * @param dtstart the time of the first occurrence
54      * @param recur the recurrence
55      * @return an estimate of the time (in UTC milliseconds) of the last
56      * occurrence, which may be greater than the actual last occurrence
57      * @throws DateException
58      */
getLastOccurence(Time dtstart, RecurrenceSet recur)59     public long getLastOccurence(Time dtstart,
60                                  RecurrenceSet recur) throws DateException {
61         return getLastOccurence(dtstart, null /* no limit */, recur);
62     }
63 
64     /**
65      * Returns the time (millis since epoch) of the last occurrence,
66      * or -1 if the event repeats forever.  If there are no occurrences
67      * (because the exrule or exdates cancel all the occurrences) and the
68      * event does not repeat forever, then 0 is returned.
69      *
70      * This computes a conservative estimate of the last occurrence. That is,
71      * the time of the actual last occurrence might be earlier than the time
72      * returned by this method.
73      *
74      * @param dtstart the time of the first occurrence
75      * @param maxtime the max possible time of the last occurrence. null means no limit
76      * @param recur the recurrence
77      * @return an estimate of the time (in UTC milliseconds) of the last
78      * occurrence, which may be greater than the actual last occurrence
79      * @throws DateException
80      */
getLastOccurence(Time dtstart, Time maxtime, RecurrenceSet recur)81     public long getLastOccurence(Time dtstart, Time maxtime,
82                                  RecurrenceSet recur) throws DateException {
83         long lastTime = -1;
84         boolean hasCount = false;
85 
86         // first see if there are any "until"s specified.  if so, use the latest
87         // until / rdate.
88         if (recur.rrules != null) {
89             for (EventRecurrence rrule : recur.rrules) {
90                 if (rrule.count != 0) {
91                     hasCount = true;
92                 } else if (rrule.until != null) {
93                     // according to RFC 2445, until must be in UTC.
94                     mIterator.parse(rrule.until);
95                     long untilTime = mIterator.toMillis();
96                     if (untilTime > lastTime) {
97                         lastTime = untilTime;
98                     }
99                 }
100             }
101             if (lastTime != -1 && recur.rdates != null) {
102                 for (long dt : recur.rdates) {
103                     if (dt > lastTime) {
104                         lastTime = dt;
105                     }
106                 }
107             }
108 
109             // If there were only "until"s and no "count"s, then return the
110             // last "until" date or "rdate".
111             if (lastTime != -1 && !hasCount) {
112                 return lastTime;
113             }
114         } else if (recur.rdates != null &&
115                    recur.exrules == null && recur.exdates == null) {
116             // if there are only rdates, we can just pick the last one.
117             for (long dt : recur.rdates) {
118                 if (dt > lastTime) {
119                     lastTime = dt;
120                 }
121             }
122             return lastTime;
123         }
124 
125         // Expand the complete recurrence if there were any counts specified,
126         // or if there were rdates specified.
127         if (hasCount || recur.rdates != null || maxtime != null) {
128             // The expansion might not contain any dates if the exrule or
129             // exdates cancel all the generated dates.
130             long[] dates = expand(dtstart, recur,
131                     dtstart.toMillis() /* range start */,
132                     (maxtime != null) ? maxtime.toMillis() : -1 /* range end */);
133 
134             // The expansion might not contain any dates if exrule or exdates
135             // cancel all the generated dates.
136             if (dates.length == 0) {
137                 return 0;
138             }
139             return dates[dates.length - 1];
140         }
141         return -1;
142     }
143 
144     /**
145      * a -- list of values
146      * N -- number of values to use in a
147      * v -- value to check for
148      */
listContains(int[] a, int N, int v)149     private static boolean listContains(int[] a, int N, int v)
150     {
151         for (int i=0; i<N; i++) {
152             if (a[i] == v) {
153                 return true;
154             }
155         }
156         return false;
157     }
158 
159     /**
160      * a -- list of values
161      * N -- number of values to use in a
162      * v -- value to check for
163      * max -- if a value in a is negative, add that negative value
164      *        to max and compare that instead; this is how we deal with
165      *        negative numbers being offsets from the end value
166      */
listContains(int[] a, int N, int v, int max)167     private static boolean listContains(int[] a, int N, int v, int max)
168     {
169         for (int i=0; i<N; i++) {
170             int w = a[i];
171             if (w > 0) {
172                 if (w == v) {
173                     return true;
174                 }
175             } else {
176                 max += w; // w is negative
177                 if (max == v) {
178                     return true;
179                 }
180             }
181         }
182         return false;
183     }
184 
185     /**
186      * Filter out the ones for events whose BYxxx rule is for
187      * a period greater than or equal to the period of the FREQ.
188      *
189      * Returns 0 if the event should not be filtered out
190      * Returns something else (a rule number which is useful for debugging)
191      * if the event should not be returned
192      */
filter(EventRecurrence r, Time iterator)193     private static int filter(EventRecurrence r, Time iterator)
194     {
195         boolean found;
196         int freq = r.freq;
197 
198         if (EventRecurrence.MONTHLY >= freq) {
199             // BYMONTH
200             if (r.bymonthCount > 0) {
201                 found = listContains(r.bymonth, r.bymonthCount,
202                         iterator.getMonth() + 1);
203                 if (!found) {
204                     return 1;
205                 }
206             }
207         }
208         if (EventRecurrence.WEEKLY >= freq) {
209             // BYWEEK -- this is just a guess.  I wonder how many events
210             // acutally use BYWEEKNO.
211             if (r.byweeknoCount > 0) {
212                 found = listContains(r.byweekno, r.byweeknoCount,
213                                 iterator.getWeekNumber(),
214                                 iterator.getActualMaximum(Time.WEEK_NUM));
215                 if (!found) {
216                     return 2;
217                 }
218             }
219         }
220         if (EventRecurrence.DAILY >= freq) {
221             // BYYEARDAY
222             if (r.byyeardayCount > 0) {
223                 found = listContains(r.byyearday, r.byyeardayCount,
224                                 iterator.getYearDay(), iterator.getActualMaximum(Time.YEAR_DAY));
225                 if (!found) {
226                     return 3;
227                 }
228             }
229             // BYMONTHDAY
230             if (r.bymonthdayCount > 0 ) {
231                 found = listContains(r.bymonthday, r.bymonthdayCount,
232                                 iterator.getDay(),
233                                 iterator.getActualMaximum(Time.MONTH_DAY));
234                 if (!found) {
235                     return 4;
236                 }
237             }
238             // BYDAY -- when filtering, we ignore the number field, because it
239             // only is meaningful when creating more events.
240 byday:
241             if (r.bydayCount > 0) {
242                 int a[] = r.byday;
243                 int N = r.bydayCount;
244                 int v = EventRecurrence.timeDay2Day(iterator.getWeekDay());
245                 for (int i=0; i<N; i++) {
246                     if (a[i] == v) {
247                         break byday;
248                     }
249                 }
250                 return 5;
251             }
252         }
253         if (EventRecurrence.HOURLY >= freq) {
254             // BYHOUR
255             found = listContains(r.byhour, r.byhourCount,
256                             iterator.getHour(),
257                             iterator.getActualMaximum(Time.HOUR));
258             if (!found) {
259                 return 6;
260             }
261         }
262         if (EventRecurrence.MINUTELY >= freq) {
263             // BYMINUTE
264             found = listContains(r.byminute, r.byminuteCount,
265                             iterator.getMinute(),
266                             iterator.getActualMaximum(Time.MINUTE));
267             if (!found) {
268                 return 7;
269             }
270         }
271         if (EventRecurrence.SECONDLY >= freq) {
272             // BYSECOND
273             found = listContains(r.bysecond, r.bysecondCount,
274                             iterator.getSecond(),
275                             iterator.getActualMaximum(Time.SECOND));
276             if (!found) {
277                 return 8;
278             }
279         }
280 
281         if (r.bysetposCount > 0) {
282 bysetpos:
283             // BYSETPOS - we only handle rules like FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1
284             if (freq == EventRecurrence.MONTHLY && r.bydayCount > 0) {
285                 // Check for stuff like BYDAY=1TU
286                 for (int i = r.bydayCount - 1; i >= 0; i--) {
287                     if (r.bydayNum[i] != 0) {
288                         if (Log.isLoggable(TAG, Log.VERBOSE)) {
289                             Log.v(TAG, "BYSETPOS not supported with these rules: " + r);
290                         }
291                         break bysetpos;
292                     }
293                 }
294                 if (!filterMonthlySetPos(r, iterator)) {
295                     // Not allowed, filter it out.
296                     return 9;
297                 }
298             } else {
299                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
300                     Log.v(TAG, "BYSETPOS not supported with these rules: " + r);
301                 }
302             }
303             // BYSETPOS was defined but we don't know how to handle it.  Do no filtering based
304             // on it.
305         }
306 
307         // if we got to here, we didn't filter it out
308         return 0;
309     }
310 
311     /**
312      * Filters out instances that don't match the BYSETPOS clause of a monthly recurrence rule.
313      * This is an awkward and inefficient way to go about it.
314      *
315      * @returns true if this instance should be kept
316      */
filterMonthlySetPos(EventRecurrence r, Time instance)317     private static boolean filterMonthlySetPos(EventRecurrence r, Time instance) {
318         /*
319          * Compute the day of the week for the first day of the month.  "instance" has a
320          * day number and a DotW, so we compute the DotW of the 1st from that.  Note DotW
321          * is 0-6, where 0=SUNDAY.
322          *
323          * The basic calculation is to take the instance's "day of the week" number, subtract
324          * (day of the month - 1) mod 7, and then make sure it's positive.  We can simplify
325          * that with some algebra.
326          */
327         int dotw = (instance.getWeekDay() - instance.getDay() + 36) % 7;
328 
329         /*
330          * The byday[] values are specified as bits, so we can just OR them all
331          * together.
332          */
333         int bydayMask = 0;
334         for (int i = 0; i < r.bydayCount; i++) {
335             bydayMask |= r.byday[i];
336         }
337 
338         /*
339          * Generate a set according to the BYDAY rules.  For each day of the month, determine
340          * if its day of the week is included.  If so, append it to the day set.
341          */
342         int maxDay = instance.getActualMaximum(Time.MONTH_DAY);
343         int daySet[] = new int[maxDay];
344         int daySetLength = 0;
345 
346         for (int md = 1; md <= maxDay; md++) {
347             // For each month day, see if it's part of the set.  (This makes some assumptions
348             // about the exact form of the DotW constants.)
349             int dayBit = EventRecurrence.SU << dotw;
350             if ((bydayMask & dayBit) != 0) {
351                 daySet[daySetLength++] = md;
352             }
353 
354             dotw++;
355             if (dotw == 7)
356                 dotw = 0;
357         }
358 
359         /*
360          * Now walk through the BYSETPOS list and see if the instance is equal to any of the
361          * specified daySet entries.
362          */
363         for (int i = r.bysetposCount - 1; i >= 0; i--) {
364             int index = r.bysetpos[i];
365             if (index > 0) {
366                 if (index > daySetLength) {
367                     continue;  // out of range
368                 }
369                 if (daySet[index-1] == instance.getDay()) {
370                     return true;
371                 }
372             } else if (index < 0) {
373                 if (daySetLength + index < 0) {
374                     continue;  // out of range
375                 }
376                 if (daySet[daySetLength + index] == instance.getDay()) {
377                     return true;
378                 }
379             } else {
380                 // should have been caught by parser
381                 throw new RuntimeException("invalid bysetpos value");
382             }
383         }
384 
385         return false;
386     }
387 
388 
389     private static final int USE_ITERATOR = 0;
390     private static final int USE_BYLIST = 1;
391 
392     /**
393      * Return whether we should make this list from the BYxxx list or
394      * from the component of the iterator.
395      */
generateByList(int count, int freq, int byFreq)396     int generateByList(int count, int freq, int byFreq)
397     {
398         if (byFreq >= freq) {
399             return USE_ITERATOR;
400         } else {
401             if (count == 0) {
402                 return USE_ITERATOR;
403             } else {
404                 return USE_BYLIST;
405             }
406         }
407     }
408 
useBYX(int freq, int freqConstant, int count)409     private static boolean useBYX(int freq, int freqConstant, int count)
410     {
411         return freq > freqConstant && count > 0;
412     }
413 
414     public static class DaySet
415     {
DaySet(boolean zulu)416         public DaySet(boolean zulu)
417         {
418             mTime = new Time(Time.TIMEZONE_UTC);
419         }
420 
setRecurrence(EventRecurrence r)421         void setRecurrence(EventRecurrence r)
422         {
423             mYear = 0;
424             mMonth = -1;
425             mR = r;
426         }
427 
get(Time iterator, int day)428         boolean get(Time iterator, int day)
429         {
430             int realYear = iterator.getYear();
431             int realMonth = iterator.getMonth();
432 
433             Time t = null;
434 
435             if (SPEW) {
436                 Log.i(TAG, "get called with iterator=" + iterator
437                         + " " + iterator.getMonth()
438                         + "/" + iterator.getDay()
439                         + "/" + iterator.getYear() + " day=" + day);
440             }
441             if (day < 1 || day > 28) {
442                 // if might be past the end of the month, we need to normalize it
443                 t = mTime;
444                 t.set(day, realMonth, realYear);
445                 unsafeNormalize(t);
446                 realYear = t.getYear();
447                 realMonth = t.getMonth();
448                 day = t.getDay();
449                 if (SPEW) {
450                     Log.i(TAG, "normalized t=" + t + " " + t.getMonth()
451                             + "/" + t.getDay()
452                             + "/" + t.getYear());
453                 }
454             }
455 
456             /*
457             if (true || SPEW) {
458                 Log.i(TAG, "set t=" + t + " " + realMonth + "/" + day + "/" + realYear);
459             }
460             */
461             if (realYear != mYear || realMonth != mMonth) {
462                 if (t == null) {
463                     t = mTime;
464                     t.set(day, realMonth, realYear);
465                     unsafeNormalize(t);
466                     if (SPEW) {
467                         Log.i(TAG, "set t=" + t + " " + t.getMonth()
468                                 + "/" + t.getDay()
469                                 + "/" + t.getYear()
470                                 + " realMonth=" + realMonth + " mMonth=" + mMonth);
471                     }
472                 }
473                 mYear = realYear;
474                 mMonth = realMonth;
475                 mDays = generateDaysList(t, mR);
476                 if (SPEW) {
477                     Log.i(TAG, "generated days list");
478                 }
479             }
480             return (mDays & (1<<day)) != 0;
481         }
482 
483         /**
484          * Fill in a bit set containing the days of the month on which this
485          * will occur.
486          *
487          * Only call this if the r.freq > DAILY.  Otherwise, we should be
488          * processing the BYDAY, BYMONTHDAY, etc. as filters instead.
489          *
490          * monthOffset may be -1, 0 or 1
491          */
generateDaysList(Time generated, EventRecurrence r)492         private static int generateDaysList(Time generated, EventRecurrence r)
493         {
494             int days = 0;
495 
496             int i, count, v;
497             int[] byday, bydayNum, bymonthday;
498             int j, lastDayThisMonth;
499             int first; // Time.SUNDAY, etc
500             int k;
501 
502             lastDayThisMonth = generated.getActualMaximum(Time.MONTH_DAY);
503 
504             // BYDAY
505             count = r.bydayCount;
506             if (count > 0) {
507                 // calculate the day of week for the first of this month (first)
508                 j = generated.getDay();
509                 while (j >= 8) {
510                     j -= 7;
511                 }
512                 first = generated.getWeekDay();
513                 if (first >= j) {
514                     first = first - j + 1;
515                 } else {
516                     first = first - j + 8;
517                 }
518 
519                 // What to do if the event is weekly:
520                 // This isn't ideal, but we'll generate a month's worth of events
521                 // and the code that calls this will only use the ones that matter
522                 // for the current week.
523                 byday = r.byday;
524                 bydayNum = r.bydayNum;
525                 for (i=0; i<count; i++) {
526                     v = bydayNum[i];
527                     j = EventRecurrence.day2TimeDay(byday[i]) - first + 1;
528                     if (j <= 0) {
529                         j += 7;
530                     }
531                     if (v == 0) {
532                         // v is 0, each day in the month/week
533                         for (; j<=lastDayThisMonth; j+=7) {
534                             if (SPEW) Log.i(TAG, "setting " + j + " for rule "
535                                     + v + "/" + EventRecurrence.day2TimeDay(byday[i]));
536                             days |= 1 << j;
537                         }
538                     }
539                     else if (v > 0) {
540                         // v is positive, count from the beginning of the month
541                         // -1 b/c the first one should add 0
542                         j += 7*(v-1);
543                         if (j <= lastDayThisMonth) {
544                             if (SPEW) Log.i(TAG, "setting " + j + " for rule "
545                                     + v + "/" + EventRecurrence.day2TimeDay(byday[i]));
546                             // if it's impossible, we drop it
547                             days |= 1 << j;
548                         }
549                     }
550                     else {
551                         // v is negative, count from the end of the month
552                         // find the last one
553                         for (; j<=lastDayThisMonth; j+=7) {
554                         }
555                         // v is negative
556                         // should do +1 b/c the last one should add 0, but we also
557                         // skipped the j -= 7 b/c the loop to find the last one
558                         // overshot by one week
559                         j += 7*v;
560                         if (j >= 1) {
561                             if (SPEW) Log.i(TAG, "setting " + j + " for rule "
562                                     + v + "/" + EventRecurrence.day2TimeDay(byday[i]));
563                             days |= 1 << j;
564                         }
565                     }
566                 }
567             }
568 
569             // BYMONTHDAY
570             // Q: What happens if we have BYMONTHDAY and BYDAY?
571             // A: I didn't see it in the spec, so in lieu of that, we'll
572             // intersect the two.  That seems reasonable to me.
573             if (r.freq > EventRecurrence.WEEKLY) {
574                 count = r.bymonthdayCount;
575                 if (count != 0) {
576                     bymonthday = r.bymonthday;
577                     if (r.bydayCount == 0) {
578                         for (i=0; i<count; i++) {
579                             v = bymonthday[i];
580                             if (v >= 0) {
581                                 days |= 1 << v;
582                             } else {
583                                 j = lastDayThisMonth + v + 1; // v is negative
584                                 if (j >= 1 && j <= lastDayThisMonth) {
585                                     days |= 1 << j;
586                                 }
587                             }
588                         }
589                     } else {
590                         // This is O(lastDayThisMonth*count), which is really
591                         // O(count) with a decent sized constant.
592                         for (j=1; j<=lastDayThisMonth; j++) {
593                             next_day : {
594                                 if ((days&(1<<j)) != 0) {
595                                     for (i=0; i<count; i++) {
596                                         if (bymonthday[i] == j) {
597                                             break next_day;
598                                         }
599                                     }
600                                     days &= ~(1<<j);
601                                 }
602                             }
603                         }
604                     }
605                 }
606             }
607             return days;
608         }
609 
610         private EventRecurrence mR;
611         private int mDays;
612         private Time mTime;
613         private int mYear;
614         private int mMonth;
615     }
616 
617     /**
618      * Expands the recurrence within the given range using the given dtstart
619      * value. Returns an array of longs where each element is a date in UTC
620      * milliseconds. The return value is never null.  If there are no dates
621      * then an array of length zero is returned.
622      *
623      * @param dtstart a Time object representing the first occurrence
624      * @param recur the recurrence rules, including RRULE, RDATES, EXRULE, and
625      * EXDATES
626      * @param rangeStartMillis the beginning of the range to expand, in UTC
627      * milliseconds
628      * @param rangeEndMillis the non-inclusive end of the range to expand, in
629      * UTC milliseconds; use -1 for the entire range.
630      * @return an array of dates, each date is in UTC milliseconds
631      * @throws DateException
632      * @throws IllegalArgumentException if recur cannot be parsed
633      */
expand(Time dtstart, RecurrenceSet recur, long rangeStartMillis, long rangeEndMillis)634     public long[] expand(Time dtstart,
635             RecurrenceSet recur,
636             long rangeStartMillis,
637             long rangeEndMillis) throws DateException {
638         String timezone = dtstart.getTimezone();
639         mIterator.clear(timezone);
640         mGenerated.clear(timezone);
641 
642         // We don't need to clear the mUntil (and it wouldn't do any good to
643         // do so) because the "until" date string is specified in UTC and that
644         // sets the timezone in the mUntil Time object.
645 
646         mIterator.set(rangeStartMillis);
647         long rangeStartDateValue = normDateTimeComparisonValue(mIterator);
648 
649         long rangeEndDateValue;
650         if (rangeEndMillis != -1) {
651             mIterator.set(rangeEndMillis);
652             rangeEndDateValue = normDateTimeComparisonValue(mIterator);
653         } else {
654             rangeEndDateValue = Long.MAX_VALUE;
655         }
656 
657         TreeSet<Long> dtSet = new TreeSet<Long>();
658 
659         if (recur.rrules != null) {
660             for (EventRecurrence rrule : recur.rrules) {
661                 expand(dtstart, rrule, rangeStartDateValue,
662                         rangeEndDateValue, true /* add */, dtSet);
663             }
664         }
665         if (recur.rdates != null) {
666             for (long dt : recur.rdates) {
667                 // The dates are stored as milliseconds. We need to convert
668                 // them to year/month/day values in the local timezone.
669                 mIterator.set(dt);
670                 long dtvalue = normDateTimeComparisonValue(mIterator);
671                 dtSet.add(dtvalue);
672             }
673         }
674         if (recur.exrules != null) {
675             for (EventRecurrence exrule : recur.exrules) {
676                 expand(dtstart, exrule, rangeStartDateValue,
677                         rangeEndDateValue, false /* remove */, dtSet);
678             }
679         }
680         if (recur.exdates != null) {
681             for (long dt : recur.exdates) {
682                 // The dates are stored as milliseconds. We need to convert
683                 // them to year/month/day values in the local timezone.
684                 mIterator.set(dt);
685                 long dtvalue = normDateTimeComparisonValue(mIterator);
686                 dtSet.remove(dtvalue);
687             }
688         }
689         if (dtSet.isEmpty()) {
690             // this can happen if the recurrence does not occur within the
691             // expansion window.
692             return new long[0];
693         }
694 
695         // The values in dtSet are represented in a special form that is useful
696         // for fast comparisons and that is easy to generate from year/month/day
697         // values. We need to convert these to UTC milliseconds and also to
698         // ensure that the dates are valid.
699         int len = dtSet.size();
700         long[] dates = new long[len];
701         int i = 0;
702         for (Long val: dtSet) {
703             setTimeFromLongValue(mIterator, val);
704             dates[i++] = mIterator.toMillis();
705         }
706         return dates;
707     }
708 
709     /**
710      * Run the recurrence algorithm.  Processes events defined in the local
711      * timezone of the event.  Return a list of iCalendar DATETIME
712      * strings containing the start date/times of the occurrences; the output
713      * times are defined in the local timezone of the event.
714      *
715      * If you want all of the events, pass Long.MAX_VALUE for rangeEndDateValue.  If you pass
716      * Long.MAX_VALUE for rangeEnd, and the event doesn't have a COUNT or UNTIL field,
717      * you'll get a DateException.
718      *
719      * @param dtstart the dtstart date as defined in RFC2445.  This
720      * {@link Time} should be in the timezone of the event.
721      * @param r the parsed recurrence, as defiend in RFC2445
722      * @param rangeStartDateValue the first date-time you care about, inclusive
723      * @param rangeEndDateValue the last date-time you care about, not inclusive (so
724      *                  if you care about everything up through and including
725      *                  Dec 22 1995, set last to Dec 23, 1995 00:00:00
726      * @param add Whether or not we should add to out, or remove from out.
727      * @param out the TreeSet you'd like to fill with the events
728      * @throws DateException
729      * @throws IllegalArgumentException if r cannot be parsed.
730      */
expand(Time dtstart, EventRecurrence r, long rangeStartDateValue, long rangeEndDateValue, boolean add, TreeSet<Long> out)731     public void expand(Time dtstart,
732             EventRecurrence r,
733             long rangeStartDateValue,
734             long rangeEndDateValue,
735             boolean add,
736             TreeSet<Long> out) throws DateException {
737         unsafeNormalize(dtstart);
738         long dtstartDateValue = normDateTimeComparisonValue(dtstart);
739         int count = 0;
740 
741         // add the dtstart instance to the recurrence, if within range.
742         // For example, if dtstart is Mar 1, 2010 and the range is Jan 1 - Apr 1,
743         // then add it here and increment count.  If the range is earlier or later,
744         // then don't add it here.  In that case, count will be incremented later
745         // inside  the loop.  It is important that count gets incremented exactly
746         // once here or in the loop for dtstart.
747         //
748         // NOTE: if DTSTART is not synchronized with the recurrence rule, the first instance
749         //       we return will not fit the RRULE pattern.
750         if (add && dtstartDateValue >= rangeStartDateValue
751                 && dtstartDateValue < rangeEndDateValue) {
752             out.add(dtstartDateValue);
753             ++count;
754         }
755 
756         Time iterator = mIterator;
757         Time until = mUntil;
758         StringBuilder sb = mStringBuilder;
759         Time generated = mGenerated;
760         DaySet days = mDays;
761 
762         try {
763 
764             days.setRecurrence(r);
765             if (rangeEndDateValue == Long.MAX_VALUE && r.until == null && r.count == 0) {
766                 throw new DateException(
767                         "No range end provided for a recurrence that has no UNTIL or COUNT.");
768             }
769 
770             // the top-level frequency
771             int freqField;
772             int freqAmount = r.interval;
773             int freq = r.freq;
774             switch (freq)
775             {
776                 case EventRecurrence.SECONDLY:
777                     freqField = Time.SECOND;
778                     break;
779                 case EventRecurrence.MINUTELY:
780                     freqField = Time.MINUTE;
781                     break;
782                 case EventRecurrence.HOURLY:
783                     freqField = Time.HOUR;
784                     break;
785                 case EventRecurrence.DAILY:
786                     freqField = Time.MONTH_DAY;
787                     break;
788                 case EventRecurrence.WEEKLY:
789                     freqField = Time.MONTH_DAY;
790                     freqAmount = 7 * r.interval;
791                     if (freqAmount <= 0) {
792                         freqAmount = 7;
793                     }
794                     break;
795                 case EventRecurrence.MONTHLY:
796                     freqField = Time.MONTH;
797                     break;
798                 case EventRecurrence.YEARLY:
799                     freqField = Time.YEAR;
800                     break;
801                 default:
802                     throw new DateException("bad freq=" + freq);
803             }
804             if (freqAmount <= 0) {
805                 freqAmount = 1;
806             }
807 
808             int bymonthCount = r.bymonthCount;
809             boolean usebymonth = useBYX(freq, EventRecurrence.MONTHLY, bymonthCount);
810             boolean useDays = freq >= EventRecurrence.WEEKLY &&
811                                  (r.bydayCount > 0 || r.bymonthdayCount > 0);
812             int byhourCount = r.byhourCount;
813             boolean usebyhour = useBYX(freq, EventRecurrence.HOURLY, byhourCount);
814             int byminuteCount = r.byminuteCount;
815             boolean usebyminute = useBYX(freq, EventRecurrence.MINUTELY, byminuteCount);
816             int bysecondCount = r.bysecondCount;
817             boolean usebysecond = useBYX(freq, EventRecurrence.SECONDLY, bysecondCount);
818 
819             // initialize the iterator
820             iterator.set(dtstart);
821             if (freqField == Time.MONTH) {
822                 if (useDays) {
823                     // if it's monthly, and we're going to be generating
824                     // days, set the iterator day field to 1 because sometimes
825                     // we'll skip months if it's greater than 28.
826                     // XXX Do we generate days for MONTHLY w/ BYHOUR?  If so,
827                     // we need to do this then too.
828                     iterator.setDay(1);
829                 }
830             }
831 
832             long untilDateValue;
833             if (r.until != null) {
834                 // Ensure that the "until" date string is specified in UTC.
835                 String untilStr = r.until;
836                 // 15 is length of date-time without trailing Z e.g. "20090204T075959"
837                 // A string such as 20090204 is a valid UNTIL (see RFC 2445) and the
838                 // Z should not be added.
839                 if (untilStr.length() == 15) {
840                     untilStr = untilStr + 'Z';
841                 }
842                 // The parse() method will set the timezone to UTC
843                 until.parse(untilStr);
844 
845                 // We need the "until" year/month/day values to be in the same
846                 // timezone as all the generated dates so that we can compare them
847                 // using the values returned by normDateTimeComparisonValue().
848                 until.switchTimezone(dtstart.getTimezone());
849                 untilDateValue = normDateTimeComparisonValue(until);
850             } else {
851                 untilDateValue = Long.MAX_VALUE;
852             }
853 
854             sb.ensureCapacity(15);
855             sb.setLength(15); // TODO: pay attention to whether or not the event
856             // is an all-day one.
857 
858             if (SPEW) {
859                 Log.i(TAG, "expand called w/ rangeStart=" + rangeStartDateValue
860                         + " rangeEnd=" + rangeEndDateValue);
861             }
862 
863             // go until the end of the range or we're done with this event
864             boolean eventEnded = false;
865             int failsafe = 0; // Avoid infinite loops
866             events: {
867                 while (true) {
868                     int monthIndex = 0;
869                     if (failsafe++ > MAX_ALLOWED_ITERATIONS) { // Give up after about 1 second of processing
870                         Log.w(TAG, "Recurrence processing stuck with r=" + r + " rangeStart="
871                                   + rangeStartDateValue + " rangeEnd=" + rangeEndDateValue);
872                         break;
873                     }
874 
875                     unsafeNormalize(iterator);
876 
877                     int iteratorYear = iterator.getYear();
878                     int iteratorMonth = iterator.getMonth() + 1;
879                     int iteratorDay = iterator.getDay();
880                     int iteratorHour = iterator.getHour();
881                     int iteratorMinute = iterator.getMinute();
882                     int iteratorSecond = iterator.getSecond();
883 
884                     // year is never expanded -- there is no BYYEAR
885                     generated.set(iterator);
886 
887                     if (SPEW) Log.i(TAG, "year=" + generated.getYear());
888 
889                     do { // month
890                         int month = usebymonth
891                                         ? r.bymonth[monthIndex]
892                                         : iteratorMonth;
893                         month--;
894                         if (SPEW) Log.i(TAG, "  month=" + month);
895 
896                         int dayIndex = 1;
897                         int lastDayToExamine = 0;
898 
899                         // Use this to handle weeks that overlap the end of the month.
900                         // Keep the year and month that days is for, and generate it
901                         // when needed in the loop
902                         if (useDays) {
903                             // Determine where to start and end, don't worry if this happens
904                             // to be before dtstart or after the end, because that will be
905                             // filtered in the inner loop
906                             if (freq == EventRecurrence.WEEKLY) {
907                                 /*
908                                  * iterator.weekDay indicates the day of the week (0-6, SU-SA).
909                                  * Because dayIndex might start in the middle of a week, and we're
910                                  * interested in treating a week as a unit, we want to move
911                                  * backward to the start of the week.  (This could make the
912                                  * dayIndex negative, which will be corrected by normalization
913                                  * later on.)
914                                  *
915                                  * The day that starts the week is determined by WKST, which
916                                  * defaults to MO.
917                                  *
918                                  * Example: dayIndex is Tuesday the 8th, and weeks start on
919                                  * Thursdays.  Tuesday is day 2, Thursday is day 4, so we
920                                  * want to move back (2 - 4 + 7) % 7 = 5 days to the previous
921                                  * Thursday.  If weeks started on Mondays, we would only
922                                  * need to move back (2 - 1 + 7) % 7 = 1 day.
923                                  */
924                                 int weekStartAdj = (iterator.getWeekDay() -
925                                         EventRecurrence.day2TimeDay(r.wkst) + 7) % 7;
926                                 dayIndex = iterator.getDay() - weekStartAdj;
927                                 lastDayToExamine = dayIndex + 6;
928                             } else {
929                                 lastDayToExamine = generated
930                                     .getActualMaximum(Time.MONTH_DAY);
931                             }
932                             if (SPEW) Log.i(TAG, "dayIndex=" + dayIndex
933                                     + " lastDayToExamine=" + lastDayToExamine
934                                     + " days=" + days);
935                         }
936 
937                         do { // day
938                             int day;
939                             if (useDays) {
940                                 if (!days.get(iterator, dayIndex)) {
941                                     dayIndex++;
942                                     continue;
943                                 } else {
944                                     day = dayIndex;
945                                 }
946                             } else {
947                                 day = iteratorDay;
948                             }
949                             if (SPEW) Log.i(TAG, "    day=" + day);
950 
951                             // hour
952                             int hourIndex = 0;
953                             do {
954                                 int hour = usebyhour
955                                                 ? r.byhour[hourIndex]
956                                                 : iteratorHour;
957                                 if (SPEW) Log.i(TAG, "      hour=" + hour + " usebyhour=" + usebyhour);
958 
959                                 // minute
960                                 int minuteIndex = 0;
961                                 do {
962                                     int minute = usebyminute
963                                                     ? r.byminute[minuteIndex]
964                                                     : iteratorMinute;
965                                     if (SPEW) Log.i(TAG, "        minute=" + minute);
966 
967                                     // second
968                                     int secondIndex = 0;
969                                     do {
970                                         int second = usebysecond
971                                                         ? r.bysecond[secondIndex]
972                                                         : iteratorSecond;
973                                         if (SPEW) Log.i(TAG, "          second=" + second);
974 
975                                         // we do this here each time, because if we distribute it, we find the
976                                         // month advancing extra times, as we set the month to the 32nd, 33rd, etc.
977                                         // days.
978                                         generated.set(second, minute, hour, day, month, iteratorYear);
979                                         unsafeNormalize(generated);
980 
981                                         long genDateValue = normDateTimeComparisonValue(generated);
982                                         // sometimes events get generated (BYDAY, BYHOUR, etc.) that
983                                         // are before dtstart.  Filter these.  I believe this is correct,
984                                         // but Google Calendar doesn't seem to always do this.
985                                         if (genDateValue >= dtstartDateValue) {
986                                             // filter and then add
987                                             // TODO: we don't check for stop conditions (like
988                                             //       passing the "end" date) unless the filter
989                                             //       allows the event.  Could stop sooner.
990                                             int filtered = filter(r, generated);
991                                             if (0 == filtered) {
992 
993                                                 // increase the count as long
994                                                 // as this isn't the same
995                                                 // as the first instance
996                                                 // specified by the DTSTART
997                                                 // (for RRULEs -- additive).
998                                                 // This condition must be the complement of the
999                                                 // condition for incrementing count at the
1000                                                 // beginning of the method, so if we don't
1001                                                 // increment count there, we increment it here.
1002                                                 // For example, if add is set and dtstartDateValue
1003                                                 // is inside the start/end range, then it was added
1004                                                 // and count was incremented at the beginning.
1005                                                 // If dtstartDateValue is outside the range or add
1006                                                 // is not set, then we must increment count here.
1007                                                 if (!(dtstartDateValue == genDateValue
1008                                                         && add
1009                                                         && dtstartDateValue >= rangeStartDateValue
1010                                                         && dtstartDateValue < rangeEndDateValue)) {
1011                                                     ++count;
1012                                                 }
1013                                                 // one reason we can stop is that
1014                                                 // we're past the until date
1015                                                 if (genDateValue > untilDateValue) {
1016                                                     if (SPEW) {
1017                                                         Log.i(TAG, "stopping b/c until="
1018                                                             + untilDateValue
1019                                                             + " generated="
1020                                                             + genDateValue);
1021                                                     }
1022                                                     break events;
1023                                                 }
1024                                                 // or we're past rangeEnd
1025                                                 if (genDateValue >= rangeEndDateValue) {
1026                                                     if (SPEW) {
1027                                                         Log.i(TAG, "stopping b/c rangeEnd="
1028                                                                 + rangeEndDateValue
1029                                                                 + " generated=" + generated);
1030                                                     }
1031                                                     break events;
1032                                                 }
1033 
1034                                                 if (genDateValue >= rangeStartDateValue) {
1035                                                     if (SPEW) {
1036                                                         Log.i(TAG, "adding date=" + generated + " filtered=" + filtered);
1037                                                     }
1038                                                     if (add) {
1039                                                         out.add(genDateValue);
1040                                                     } else {
1041                                                         out.remove(genDateValue);
1042                                                     }
1043                                                 }
1044                                                 // another is that count is high enough
1045                                                 if (r.count > 0 && r.count == count) {
1046                                                     //Log.i(TAG, "stopping b/c count=" + count);
1047                                                     break events;
1048                                                 }
1049                                             }
1050                                         }
1051                                         secondIndex++;
1052                                     } while (usebysecond && secondIndex < bysecondCount);
1053                                     minuteIndex++;
1054                                 } while (usebyminute && minuteIndex < byminuteCount);
1055                                 hourIndex++;
1056                             } while (usebyhour && hourIndex < byhourCount);
1057                             dayIndex++;
1058                         } while (useDays && dayIndex <= lastDayToExamine);
1059                         monthIndex++;
1060                     } while (usebymonth && monthIndex < bymonthCount);
1061 
1062                     // Add freqAmount to freqField until we get another date that we want.
1063                     // We don't want to "generate" dates with the iterator.
1064                     // XXX: We do this for days, because there is a varying number of days
1065                     // per month
1066                     int oldDay = iterator.getDay();
1067                     generated.set(iterator);  // just using generated as a temporary.
1068                     int n = 1;
1069                     while (true) {
1070                         int value = freqAmount * n;
1071                         switch (freqField) {
1072                             case Time.SECOND:
1073                             case Time.MINUTE:
1074                             case Time.HOUR:
1075                             case Time.MONTH_DAY:
1076                             case Time.MONTH:
1077                             case Time.YEAR:
1078                             case Time.WEEK_DAY:
1079                             case Time.YEAR_DAY:
1080                                 iterator.add(freqField, value);
1081                                 break;
1082                             default:
1083                                 throw new RuntimeException("bad field=" + freqField);
1084                         }
1085 
1086                         unsafeNormalize(iterator);
1087                         if (freqField != Time.YEAR && freqField != Time.MONTH) {
1088                             break;
1089                         }
1090                         if (iterator.getDay() == oldDay) {
1091                             break;
1092                         }
1093                         n++;
1094                         iterator.set(generated);
1095                     }
1096                 }
1097             }
1098         }
1099         catch (DateException e) {
1100             Log.w(TAG, "DateException with r=" + r + " rangeStart=" + rangeStartDateValue
1101                     + " rangeEnd=" + rangeEndDateValue);
1102             throw e;
1103         }
1104         catch (RuntimeException t) {
1105             Log.w(TAG, "RuntimeException with r=" + r + " rangeStart=" + rangeStartDateValue
1106                     + " rangeEnd=" + rangeEndDateValue);
1107             throw t;
1108         }
1109     }
1110 
1111     /**
1112      * Normalizes the date fields to give a valid date, but if the time falls
1113      * in the invalid window during a transition out of Daylight Saving Time
1114      * when time jumps forward an hour, then the "normalized" value will be
1115      * invalid.
1116      * <p>
1117      * This method also computes the weekDay and yearDay fields.
1118      *
1119      * <p>
1120      * This method does not modify the fields isDst, or gmtOff.
1121      */
unsafeNormalize(Time date)1122     static void unsafeNormalize(Time date) {
1123         int second = date.getSecond();
1124         int minute = date.getMinute();
1125         int hour = date.getHour();
1126         int monthDay = date.getDay();
1127         int month = date.getMonth();
1128         int year = date.getYear();
1129 
1130         int addMinutes = ((second < 0) ? (second - 59) : second) / 60;
1131         second -= addMinutes * 60;
1132         minute += addMinutes;
1133         int addHours = ((minute < 0) ? (minute - 59) : minute) / 60;
1134         minute -= addHours * 60;
1135         hour += addHours;
1136         int addDays = ((hour < 0) ? (hour - 23) : hour) / 24;
1137         hour -= addDays * 24;
1138         monthDay += addDays;
1139 
1140         // We want to make "monthDay" positive. We do this by subtracting one
1141         // from the year and adding a year's worth of days to "monthDay" in
1142         // the following loop while "monthDay" <= 0.
1143         while (monthDay <= 0) {
1144             // If month is after Feb, then add this year's length so that we
1145             // include this year's leap day, if any.
1146             // Otherwise (the month is Feb or earlier), add last year's length.
1147             // Subtract one from the year in either case. This gives the same
1148             // effective date but makes monthDay (the day of the month) much
1149             // larger. Eventually (usually in one iteration) monthDay will
1150             // be positive.
1151             int days = month > 1 ? yearLength(year) : yearLength(year - 1);
1152             monthDay += days;
1153             year -= 1;
1154         }
1155         // At this point, monthDay >= 1. Normalize the month to the range [0,11].
1156         if (month < 0) {
1157             int years = (month + 1) / 12 - 1;
1158             year += years;
1159             month -= 12 * years;
1160         } else if (month >= 12) {
1161             int years = month / 12;
1162             year += years;
1163             month -= 12 * years;
1164         }
1165         // At this point, month is in the range [0,11] and monthDay >= 1.
1166         // Now loop until the monthDay is in the correct range for the month.
1167         while (true) {
1168             // On January, check if we can jump forward a whole year.
1169             if (month == 0) {
1170                 int yearLength = yearLength(year);
1171                 if (monthDay > yearLength) {
1172                     year++;
1173                     monthDay -= yearLength;
1174                 }
1175             }
1176             int monthLength = monthLength(year, month);
1177             if (monthDay > monthLength) {
1178                 monthDay -= monthLength;
1179                 month++;
1180                 if (month >= 12) {
1181                     month -= 12;
1182                     year++;
1183                 }
1184             } else break;
1185         }
1186         // At this point, monthDay <= the length of the current month and is
1187         // in the range [1,31].
1188 
1189         date.setSecond(second);
1190         date.setMinute(minute);
1191         date.setHour(hour);
1192         date.setDay(monthDay);
1193         date.setMonth(month);
1194         date.setYear(year);
1195         date.setWeekDay(weekDay(year, month, monthDay));
1196         date.setYearDay(yearDay(year, month, monthDay));
1197     }
1198 
1199     /**
1200      * Returns true if the given year is a leap year.
1201      *
1202      * @param year the given year to test
1203      * @return true if the given year is a leap year.
1204      */
isLeapYear(int year)1205     static boolean isLeapYear(int year) {
1206         return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0));
1207     }
1208 
1209     /**
1210      * Returns the number of days in the given year.
1211      *
1212      * @param year the given year
1213      * @return the number of days in the given year.
1214      */
yearLength(int year)1215     static int yearLength(int year) {
1216         return isLeapYear(year) ? 366 : 365;
1217     }
1218 
1219     private static final int[] DAYS_PER_MONTH = { 31, 28, 31, 30, 31, 30, 31,
1220             31, 30, 31, 30, 31 };
1221     private static final int[] DAYS_IN_YEAR_PRECEDING_MONTH = { 0, 31, 59, 90,
1222         120, 151, 181, 212, 243, 273, 304, 334 };
1223 
1224     /**
1225      * Returns the number of days in the given month of the given year.
1226      *
1227      * @param year the given year.
1228      * @param month the given month in the range [0,11]
1229      * @return the number of days in the given month of the given year.
1230      */
monthLength(int year, int month)1231     static int monthLength(int year, int month) {
1232         int n = DAYS_PER_MONTH[month];
1233         if (n != 28) {
1234             return n;
1235         }
1236         return isLeapYear(year) ? 29 : 28;
1237     }
1238 
1239     /**
1240      * Computes the weekday, a number in the range [0,6] where Sunday=0, from
1241      * the given year, month, and day.
1242      *
1243      * @param year the year
1244      * @param month the 0-based month in the range [0,11]
1245      * @param day the 1-based day of the month in the range [1,31]
1246      * @return the weekday, a number in the range [0,6] where Sunday=0
1247      */
weekDay(int year, int month, int day)1248     static int weekDay(int year, int month, int day) {
1249         if (month <= 1) {
1250             month += 12;
1251             year -= 1;
1252         }
1253         return (day + (13 * month - 14) / 5 + year + year/4 - year/100 + year/400) % 7;
1254     }
1255 
1256     /**
1257      * Computes the 0-based "year day", given the year, month, and day.
1258      *
1259      * @param year the year
1260      * @param month the 0-based month in the range [0,11]
1261      * @param day the 1-based day in the range [1,31]
1262      * @return the 0-based "year day", the number of days into the year
1263      */
yearDay(int year, int month, int day)1264     static int yearDay(int year, int month, int day) {
1265         int yearDay = DAYS_IN_YEAR_PRECEDING_MONTH[month] + day - 1;
1266         if (month >= 2 && isLeapYear(year)) {
1267             yearDay += 1;
1268         }
1269         return yearDay;
1270     }
1271 
1272     /**
1273      * Converts a normalized Time value to a 64-bit long. The mapping of Time
1274      * values to longs provides a total ordering on the Time values so that
1275      * two Time values can be compared efficiently by comparing their 64-bit
1276      * long values.  This is faster than converting the Time values to UTC
1277      * millliseconds.
1278      *
1279      * @param normalized a Time object whose date and time fields have been
1280      * normalized
1281      * @return a 64-bit long value that can be used for comparing and ordering
1282      * dates and times represented by Time objects
1283      */
normDateTimeComparisonValue(Time normalized)1284     private static final long normDateTimeComparisonValue(Time normalized) {
1285         // 37 bits for the year, 4 bits for the month, 5 bits for the monthDay,
1286         // 5 bits for the hour, 6 bits for the minute, 6 bits for the second.
1287         return ((long)normalized.getYear() << 26) + (normalized.getMonth() << 22)
1288                 + (normalized.getDay() << 17) + (normalized.getHour() << 12)
1289                 + (normalized.getMinute() << 6) + normalized.getSecond();
1290     }
1291 
setTimeFromLongValue(Time date, long val)1292     private static final void setTimeFromLongValue(Time date, long val) {
1293         date.setYear((int) (val >> 26));
1294         date.setMonth((int) (val >> 22) & 0xf);
1295         date.setDay((int) (val >> 17) & 0x1f);
1296         date.setHour((int) (val >> 12) & 0x1f);
1297         date.setMinute((int) (val >> 6) & 0x3f);
1298         date.setSecond((int) (val & 0x3f));
1299     }
1300 }
1301