1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.calendarcommon2;
18
19import android.text.TextUtils;
20import android.text.format.Time;
21import android.util.Log;
22import android.util.TimeFormatException;
23
24import java.util.Calendar;
25import java.util.HashMap;
26
27/**
28 * Event recurrence utility functions.
29 */
30public class EventRecurrence {
31    private static String TAG = "EventRecur";
32
33    public static final int SECONDLY = 1;
34    public static final int MINUTELY = 2;
35    public static final int HOURLY = 3;
36    public static final int DAILY = 4;
37    public static final int WEEKLY = 5;
38    public static final int MONTHLY = 6;
39    public static final int YEARLY = 7;
40
41    public static final int SU = 0x00010000;
42    public static final int MO = 0x00020000;
43    public static final int TU = 0x00040000;
44    public static final int WE = 0x00080000;
45    public static final int TH = 0x00100000;
46    public static final int FR = 0x00200000;
47    public static final int SA = 0x00400000;
48
49    public Time      startDate;     // set by setStartDate(), not parse()
50
51    public int       freq;          // SECONDLY, MINUTELY, etc.
52    public String    until;
53    public int       count;
54    public int       interval;
55    public int       wkst;          // SU, MO, TU, etc.
56
57    /* lists with zero entries may be null references */
58    public int[]     bysecond;
59    public int       bysecondCount;
60    public int[]     byminute;
61    public int       byminuteCount;
62    public int[]     byhour;
63    public int       byhourCount;
64    public int[]     byday;
65    public int[]     bydayNum;
66    public int       bydayCount;
67    public int[]     bymonthday;
68    public int       bymonthdayCount;
69    public int[]     byyearday;
70    public int       byyeardayCount;
71    public int[]     byweekno;
72    public int       byweeknoCount;
73    public int[]     bymonth;
74    public int       bymonthCount;
75    public int[]     bysetpos;
76    public int       bysetposCount;
77
78    /** maps a part string to a parser object */
79    private static HashMap<String,PartParser> sParsePartMap;
80    static {
81        sParsePartMap = new HashMap<String,PartParser>();
82        sParsePartMap.put("FREQ", new ParseFreq());
83        sParsePartMap.put("UNTIL", new ParseUntil());
84        sParsePartMap.put("COUNT", new ParseCount());
85        sParsePartMap.put("INTERVAL", new ParseInterval());
86        sParsePartMap.put("BYSECOND", new ParseBySecond());
87        sParsePartMap.put("BYMINUTE", new ParseByMinute());
88        sParsePartMap.put("BYHOUR", new ParseByHour());
89        sParsePartMap.put("BYDAY", new ParseByDay());
90        sParsePartMap.put("BYMONTHDAY", new ParseByMonthDay());
91        sParsePartMap.put("BYYEARDAY", new ParseByYearDay());
92        sParsePartMap.put("BYWEEKNO", new ParseByWeekNo());
93        sParsePartMap.put("BYMONTH", new ParseByMonth());
94        sParsePartMap.put("BYSETPOS", new ParseBySetPos());
95        sParsePartMap.put("WKST", new ParseWkst());
96    }
97
98    /* values for bit vector that keeps track of what we have already seen */
99    private static final int PARSED_FREQ = 1 << 0;
100    private static final int PARSED_UNTIL = 1 << 1;
101    private static final int PARSED_COUNT = 1 << 2;
102    private static final int PARSED_INTERVAL = 1 << 3;
103    private static final int PARSED_BYSECOND = 1 << 4;
104    private static final int PARSED_BYMINUTE = 1 << 5;
105    private static final int PARSED_BYHOUR = 1 << 6;
106    private static final int PARSED_BYDAY = 1 << 7;
107    private static final int PARSED_BYMONTHDAY = 1 << 8;
108    private static final int PARSED_BYYEARDAY = 1 << 9;
109    private static final int PARSED_BYWEEKNO = 1 << 10;
110    private static final int PARSED_BYMONTH = 1 << 11;
111    private static final int PARSED_BYSETPOS = 1 << 12;
112    private static final int PARSED_WKST = 1 << 13;
113
114    /** maps a FREQ value to an integer constant */
115    private static final HashMap<String,Integer> sParseFreqMap = new HashMap<String,Integer>();
116    static {
117        sParseFreqMap.put("SECONDLY", SECONDLY);
118        sParseFreqMap.put("MINUTELY", MINUTELY);
119        sParseFreqMap.put("HOURLY", HOURLY);
120        sParseFreqMap.put("DAILY", DAILY);
121        sParseFreqMap.put("WEEKLY", WEEKLY);
122        sParseFreqMap.put("MONTHLY", MONTHLY);
123        sParseFreqMap.put("YEARLY", YEARLY);
124    }
125
126    /** maps a two-character weekday string to an integer constant */
127    private static final HashMap<String,Integer> sParseWeekdayMap = new HashMap<String,Integer>();
128    static {
129        sParseWeekdayMap.put("SU", SU);
130        sParseWeekdayMap.put("MO", MO);
131        sParseWeekdayMap.put("TU", TU);
132        sParseWeekdayMap.put("WE", WE);
133        sParseWeekdayMap.put("TH", TH);
134        sParseWeekdayMap.put("FR", FR);
135        sParseWeekdayMap.put("SA", SA);
136    }
137
138    /** If set, allow lower-case recurrence rule strings.  Minor performance impact. */
139    private static final boolean ALLOW_LOWER_CASE = true;
140
141    /** If set, validate the value of UNTIL parts.  Minor performance impact. */
142    private static final boolean VALIDATE_UNTIL = false;
143
144    /** If set, require that only one of {UNTIL,COUNT} is present.  Breaks compat w/ old parser. */
145    private static final boolean ONLY_ONE_UNTIL_COUNT = false;
146
147
148    /**
149     * Thrown when a recurrence string provided can not be parsed according
150     * to RFC2445.
151     */
152    public static class InvalidFormatException extends RuntimeException {
153        InvalidFormatException(String s) {
154            super(s);
155        }
156    }
157
158
159    public void setStartDate(Time date) {
160        startDate = date;
161    }
162
163    /**
164     * Converts one of the Calendar.SUNDAY constants to the SU, MO, etc.
165     * constants.  btw, I think we should switch to those here too, to
166     * get rid of this function, if possible.
167     */
168    public static int calendarDay2Day(int day)
169    {
170        switch (day)
171        {
172            case Calendar.SUNDAY:
173                return SU;
174            case Calendar.MONDAY:
175                return MO;
176            case Calendar.TUESDAY:
177                return TU;
178            case Calendar.WEDNESDAY:
179                return WE;
180            case Calendar.THURSDAY:
181                return TH;
182            case Calendar.FRIDAY:
183                return FR;
184            case Calendar.SATURDAY:
185                return SA;
186            default:
187                throw new RuntimeException("bad day of week: " + day);
188        }
189    }
190
191    public static int timeDay2Day(int day)
192    {
193        switch (day)
194        {
195            case Time.SUNDAY:
196                return SU;
197            case Time.MONDAY:
198                return MO;
199            case Time.TUESDAY:
200                return TU;
201            case Time.WEDNESDAY:
202                return WE;
203            case Time.THURSDAY:
204                return TH;
205            case Time.FRIDAY:
206                return FR;
207            case Time.SATURDAY:
208                return SA;
209            default:
210                throw new RuntimeException("bad day of week: " + day);
211        }
212    }
213    public static int day2TimeDay(int day)
214    {
215        switch (day)
216        {
217            case SU:
218                return Time.SUNDAY;
219            case MO:
220                return Time.MONDAY;
221            case TU:
222                return Time.TUESDAY;
223            case WE:
224                return Time.WEDNESDAY;
225            case TH:
226                return Time.THURSDAY;
227            case FR:
228                return Time.FRIDAY;
229            case SA:
230                return Time.SATURDAY;
231            default:
232                throw new RuntimeException("bad day of week: " + day);
233        }
234    }
235
236    /**
237     * Converts one of the SU, MO, etc. constants to the Calendar.SUNDAY
238     * constants.  btw, I think we should switch to those here too, to
239     * get rid of this function, if possible.
240     */
241    public static int day2CalendarDay(int day)
242    {
243        switch (day)
244        {
245            case SU:
246                return Calendar.SUNDAY;
247            case MO:
248                return Calendar.MONDAY;
249            case TU:
250                return Calendar.TUESDAY;
251            case WE:
252                return Calendar.WEDNESDAY;
253            case TH:
254                return Calendar.THURSDAY;
255            case FR:
256                return Calendar.FRIDAY;
257            case SA:
258                return Calendar.SATURDAY;
259            default:
260                throw new RuntimeException("bad day of week: " + day);
261        }
262    }
263
264    /**
265     * Converts one of the internal day constants (SU, MO, etc.) to the
266     * two-letter string representing that constant.
267     *
268     * @param day one the internal constants SU, MO, etc.
269     * @return the two-letter string for the day ("SU", "MO", etc.)
270     *
271     * @throws IllegalArgumentException Thrown if the day argument is not one of
272     * the defined day constants.
273     */
274    private static String day2String(int day) {
275        switch (day) {
276        case SU:
277            return "SU";
278        case MO:
279            return "MO";
280        case TU:
281            return "TU";
282        case WE:
283            return "WE";
284        case TH:
285            return "TH";
286        case FR:
287            return "FR";
288        case SA:
289            return "SA";
290        default:
291            throw new IllegalArgumentException("bad day argument: " + day);
292        }
293    }
294
295    private static void appendNumbers(StringBuilder s, String label,
296                                        int count, int[] values)
297    {
298        if (count > 0) {
299            s.append(label);
300            count--;
301            for (int i=0; i<count; i++) {
302                s.append(values[i]);
303                s.append(",");
304            }
305            s.append(values[count]);
306        }
307    }
308
309    private void appendByDay(StringBuilder s, int i)
310    {
311        int n = this.bydayNum[i];
312        if (n != 0) {
313            s.append(n);
314        }
315
316        String str = day2String(this.byday[i]);
317        s.append(str);
318    }
319
320    @Override
321    public String toString()
322    {
323        StringBuilder s = new StringBuilder();
324
325        s.append("FREQ=");
326        switch (this.freq)
327        {
328            case SECONDLY:
329                s.append("SECONDLY");
330                break;
331            case MINUTELY:
332                s.append("MINUTELY");
333                break;
334            case HOURLY:
335                s.append("HOURLY");
336                break;
337            case DAILY:
338                s.append("DAILY");
339                break;
340            case WEEKLY:
341                s.append("WEEKLY");
342                break;
343            case MONTHLY:
344                s.append("MONTHLY");
345                break;
346            case YEARLY:
347                s.append("YEARLY");
348                break;
349        }
350
351        if (!TextUtils.isEmpty(this.until)) {
352            s.append(";UNTIL=");
353            s.append(until);
354        }
355
356        if (this.count != 0) {
357            s.append(";COUNT=");
358            s.append(this.count);
359        }
360
361        if (this.interval != 0) {
362            s.append(";INTERVAL=");
363            s.append(this.interval);
364        }
365
366        if (this.wkst != 0) {
367            s.append(";WKST=");
368            s.append(day2String(this.wkst));
369        }
370
371        appendNumbers(s, ";BYSECOND=", this.bysecondCount, this.bysecond);
372        appendNumbers(s, ";BYMINUTE=", this.byminuteCount, this.byminute);
373        appendNumbers(s, ";BYSECOND=", this.byhourCount, this.byhour);
374
375        // day
376        int count = this.bydayCount;
377        if (count > 0) {
378            s.append(";BYDAY=");
379            count--;
380            for (int i=0; i<count; i++) {
381                appendByDay(s, i);
382                s.append(",");
383            }
384            appendByDay(s, count);
385        }
386
387        appendNumbers(s, ";BYMONTHDAY=", this.bymonthdayCount, this.bymonthday);
388        appendNumbers(s, ";BYYEARDAY=", this.byyeardayCount, this.byyearday);
389        appendNumbers(s, ";BYWEEKNO=", this.byweeknoCount, this.byweekno);
390        appendNumbers(s, ";BYMONTH=", this.bymonthCount, this.bymonth);
391        appendNumbers(s, ";BYSETPOS=", this.bysetposCount, this.bysetpos);
392
393        return s.toString();
394    }
395
396    public boolean repeatsOnEveryWeekDay() {
397        if (this.freq != WEEKLY) {
398            return false;
399        }
400
401        int count = this.bydayCount;
402        if (count != 5) {
403            return false;
404        }
405
406        for (int i = 0 ; i < count ; i++) {
407            int day = byday[i];
408            if (day == SU || day == SA) {
409                return false;
410            }
411        }
412
413        return true;
414    }
415
416    /**
417     * Determines whether this rule specifies a simple monthly rule by weekday, such as
418     * "FREQ=MONTHLY;BYDAY=3TU" (the 3rd Tuesday of every month).
419     * <p>
420     * Negative days, e.g. "FREQ=MONTHLY;BYDAY=-1TU" (the last Tuesday of every month),
421     * will cause "false" to be returned.
422     * <p>
423     * Rules that fire every week, such as "FREQ=MONTHLY;BYDAY=TU" (every Tuesday of every
424     * month) will cause "false" to be returned.  (Note these are usually expressed as
425     * WEEKLY rules, and hence are uncommon.)
426     *
427     * @return true if this rule is of the appropriate form
428     */
429    public boolean repeatsMonthlyOnDayCount() {
430        if (this.freq != MONTHLY) {
431            return false;
432        }
433
434        if (bydayCount != 1 || bymonthdayCount != 0) {
435            return false;
436        }
437
438        if (bydayNum[0] <= 0) {
439            return false;
440        }
441
442        return true;
443    }
444
445    /**
446     * Determines whether two integer arrays contain identical elements.
447     * <p>
448     * The native implementation over-allocated the arrays (and may have stuff left over from
449     * a previous run), so we can't just check the arrays -- the separately-maintained count
450     * field also matters.  We assume that a null array will have a count of zero, and that the
451     * array can hold as many elements as the associated count indicates.
452     * <p>
453     * TODO: replace this with Arrays.equals() when the old parser goes away.
454     */
455    private static boolean arraysEqual(int[] array1, int count1, int[] array2, int count2) {
456        if (count1 != count2) {
457            return false;
458        }
459
460        for (int i = 0; i < count1; i++) {
461            if (array1[i] != array2[i])
462                return false;
463        }
464
465        return true;
466    }
467
468    @Override
469    public boolean equals(Object obj) {
470        if (this == obj) {
471            return true;
472        }
473        if (!(obj instanceof EventRecurrence)) {
474            return false;
475        }
476
477        EventRecurrence er = (EventRecurrence) obj;
478        return  (startDate == null ?
479                        er.startDate == null : Time.compare(startDate, er.startDate) == 0) &&
480                freq == er.freq &&
481                (until == null ? er.until == null : until.equals(er.until)) &&
482                count == er.count &&
483                interval == er.interval &&
484                wkst == er.wkst &&
485                arraysEqual(bysecond, bysecondCount, er.bysecond, er.bysecondCount) &&
486                arraysEqual(byminute, byminuteCount, er.byminute, er.byminuteCount) &&
487                arraysEqual(byhour, byhourCount, er.byhour, er.byhourCount) &&
488                arraysEqual(byday, bydayCount, er.byday, er.bydayCount) &&
489                arraysEqual(bydayNum, bydayCount, er.bydayNum, er.bydayCount) &&
490                arraysEqual(bymonthday, bymonthdayCount, er.bymonthday, er.bymonthdayCount) &&
491                arraysEqual(byyearday, byyeardayCount, er.byyearday, er.byyeardayCount) &&
492                arraysEqual(byweekno, byweeknoCount, er.byweekno, er.byweeknoCount) &&
493                arraysEqual(bymonth, bymonthCount, er.bymonth, er.bymonthCount) &&
494                arraysEqual(bysetpos, bysetposCount, er.bysetpos, er.bysetposCount);
495    }
496
497    @Override public int hashCode() {
498        // We overrode equals, so we must override hashCode().  Nobody seems to need this though.
499        throw new UnsupportedOperationException();
500    }
501
502    /**
503     * Resets parser-modified fields to their initial state.  Does not alter startDate.
504     * <p>
505     * The original parser always set all of the "count" fields, "wkst", and "until",
506     * essentially allowing the same object to be used multiple times by calling parse().
507     * It's unclear whether this behavior was intentional.  For now, be paranoid and
508     * preserve the existing behavior by resetting the fields.
509     * <p>
510     * We don't need to touch the integer arrays; they will either be ignored or
511     * overwritten.  The "startDate" field is not set by the parser, so we ignore it here.
512     */
513    private void resetFields() {
514        until = null;
515        freq = count = interval = bysecondCount = byminuteCount = byhourCount =
516            bydayCount = bymonthdayCount = byyeardayCount = byweeknoCount = bymonthCount =
517            bysetposCount = 0;
518    }
519
520    /**
521     * Parses an rfc2445 recurrence rule string into its component pieces.  Attempting to parse
522     * malformed input will result in an EventRecurrence.InvalidFormatException.
523     *
524     * @param recur The recurrence rule to parse (in un-folded form).
525     */
526    public void parse(String recur) {
527        /*
528         * From RFC 2445 section 4.3.10:
529         *
530         * recur = "FREQ"=freq *(
531         *       ; either UNTIL or COUNT may appear in a 'recur',
532         *       ; but UNTIL and COUNT MUST NOT occur in the same 'recur'
533         *
534         *       ( ";" "UNTIL" "=" enddate ) /
535         *       ( ";" "COUNT" "=" 1*DIGIT ) /
536         *
537         *       ; the rest of these keywords are optional,
538         *       ; but MUST NOT occur more than once
539         *
540         *       ( ";" "INTERVAL" "=" 1*DIGIT )          /
541         *       ( ";" "BYSECOND" "=" byseclist )        /
542         *       ( ";" "BYMINUTE" "=" byminlist )        /
543         *       ( ";" "BYHOUR" "=" byhrlist )           /
544         *       ( ";" "BYDAY" "=" bywdaylist )          /
545         *       ( ";" "BYMONTHDAY" "=" bymodaylist )    /
546         *       ( ";" "BYYEARDAY" "=" byyrdaylist )     /
547         *       ( ";" "BYWEEKNO" "=" bywknolist )       /
548         *       ( ";" "BYMONTH" "=" bymolist )          /
549         *       ( ";" "BYSETPOS" "=" bysplist )         /
550         *       ( ";" "WKST" "=" weekday )              /
551         *       ( ";" x-name "=" text )
552         *       )
553         *
554         *  The rule parts are not ordered in any particular sequence.
555         *
556         * Examples:
557         *   FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU
558         *   FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8
559         *
560         * Strategy:
561         * (1) Split the string at ';' boundaries to get an array of rule "parts".
562         * (2) For each part, find substrings for left/right sides of '=' (name/value).
563         * (3) Call a <name>-specific parsing function to parse the <value> into an
564         *     output field.
565         *
566         * By keeping track of which names we've seen in a bit vector, we can verify the
567         * constraints indicated above (FREQ appears first, none of them appear more than once --
568         * though x-[name] would require special treatment), and we have either UNTIL or COUNT
569         * but not both.
570         *
571         * In general, RFC 2445 property names (e.g. "FREQ") and enumerations ("TU") must
572         * be handled in a case-insensitive fashion, but case may be significant for other
573         * properties.  We don't have any case-sensitive values in RRULE, except possibly
574         * for the custom "X-" properties, but we ignore those anyway.  Thus, we can trivially
575         * convert the entire string to upper case and then use simple comparisons.
576         *
577         * Differences from previous version:
578         * - allows lower-case property and enumeration values [optional]
579         * - enforces that FREQ appears first
580         * - enforces that only one of UNTIL and COUNT may be specified
581         * - allows (but ignores) X-* parts
582         * - improved validation on various values (e.g. UNTIL timestamps)
583         * - error messages are more specific
584         *
585         * TODO: enforce additional constraints listed in RFC 5545, notably the "N/A" entries
586         * in section 3.3.10.  For example, if FREQ=WEEKLY, we should reject a rule that
587         * includes a BYMONTHDAY part.
588         */
589
590        /* TODO: replace with "if (freq != 0) throw" if nothing requires this */
591        resetFields();
592
593        int parseFlags = 0;
594        String[] parts;
595        if (ALLOW_LOWER_CASE) {
596            parts = recur.toUpperCase().split(";");
597        } else {
598            parts = recur.split(";");
599        }
600        for (String part : parts) {
601            // allow empty part (e.g., double semicolon ";;")
602            if (TextUtils.isEmpty(part)) {
603                continue;
604            }
605            int equalIndex = part.indexOf('=');
606            if (equalIndex <= 0) {
607                /* no '=' or no LHS */
608                throw new InvalidFormatException("Missing LHS in " + part);
609            }
610
611            String lhs = part.substring(0, equalIndex);
612            String rhs = part.substring(equalIndex + 1);
613            if (rhs.length() == 0) {
614                throw new InvalidFormatException("Missing RHS in " + part);
615            }
616
617            /*
618             * In lieu of a "switch" statement that allows string arguments, we use a
619             * map from strings to parsing functions.
620             */
621            PartParser parser = sParsePartMap.get(lhs);
622            if (parser == null) {
623                if (lhs.startsWith("X-")) {
624                    //Log.d(TAG, "Ignoring custom part " + lhs);
625                    continue;
626                }
627                throw new InvalidFormatException("Couldn't find parser for " + lhs);
628            } else {
629                int flag = parser.parsePart(rhs, this);
630                if ((parseFlags & flag) != 0) {
631                    throw new InvalidFormatException("Part " + lhs + " was specified twice");
632                }
633                parseFlags |= flag;
634            }
635        }
636
637        // If not specified, week starts on Monday.
638        if ((parseFlags & PARSED_WKST) == 0) {
639            wkst = MO;
640        }
641
642        // FREQ is mandatory.
643        if ((parseFlags & PARSED_FREQ) == 0) {
644            throw new InvalidFormatException("Must specify a FREQ value");
645        }
646
647        // Can't have both UNTIL and COUNT.
648        if ((parseFlags & (PARSED_UNTIL | PARSED_COUNT)) == (PARSED_UNTIL | PARSED_COUNT)) {
649            if (ONLY_ONE_UNTIL_COUNT) {
650                throw new InvalidFormatException("Must not specify both UNTIL and COUNT: " + recur);
651            } else {
652                Log.w(TAG, "Warning: rrule has both UNTIL and COUNT: " + recur);
653            }
654        }
655    }
656
657    /**
658     * Base class for the RRULE part parsers.
659     */
660    abstract static class PartParser {
661        /**
662         * Parses a single part.
663         *
664         * @param value The right-hand-side of the part.
665         * @param er The EventRecurrence into which the result is stored.
666         * @return A bit value indicating which part was parsed.
667         */
668        public abstract int parsePart(String value, EventRecurrence er);
669
670        /**
671         * Parses an integer, with range-checking.
672         *
673         * @param str The string to parse.
674         * @param minVal Minimum allowed value.
675         * @param maxVal Maximum allowed value.
676         * @param allowZero Is 0 allowed?
677         * @return The parsed value.
678         */
679        public static int parseIntRange(String str, int minVal, int maxVal, boolean allowZero) {
680            try {
681                if (str.charAt(0) == '+') {
682                    // Integer.parseInt does not allow a leading '+', so skip it manually.
683                    str = str.substring(1);
684                }
685                int val = Integer.parseInt(str);
686                if (val < minVal || val > maxVal || (val == 0 && !allowZero)) {
687                    throw new InvalidFormatException("Integer value out of range: " + str);
688                }
689                return val;
690            } catch (NumberFormatException nfe) {
691                throw new InvalidFormatException("Invalid integer value: " + str);
692            }
693        }
694
695        /**
696         * Parses a comma-separated list of integers, with range-checking.
697         *
698         * @param listStr The string to parse.
699         * @param minVal Minimum allowed value.
700         * @param maxVal Maximum allowed value.
701         * @param allowZero Is 0 allowed?
702         * @return A new array with values, sized to hold the exact number of elements.
703         */
704        public static int[] parseNumberList(String listStr, int minVal, int maxVal,
705                boolean allowZero) {
706            int[] values;
707
708            if (listStr.indexOf(",") < 0) {
709                // Common case: only one entry, skip split() overhead.
710                values = new int[1];
711                values[0] = parseIntRange(listStr, minVal, maxVal, allowZero);
712            } else {
713                String[] valueStrs = listStr.split(",");
714                int len = valueStrs.length;
715                values = new int[len];
716                for (int i = 0; i < len; i++) {
717                    values[i] = parseIntRange(valueStrs[i], minVal, maxVal, allowZero);
718                }
719            }
720            return values;
721        }
722   }
723
724    /** parses FREQ={SECONDLY,MINUTELY,...} */
725    private static class ParseFreq extends PartParser {
726        @Override public int parsePart(String value, EventRecurrence er) {
727            Integer freq = sParseFreqMap.get(value);
728            if (freq == null) {
729                throw new InvalidFormatException("Invalid FREQ value: " + value);
730            }
731            er.freq = freq;
732            return PARSED_FREQ;
733        }
734    }
735    /** parses UNTIL=enddate, e.g. "19970829T021400" */
736    private static class ParseUntil extends PartParser {
737        @Override public int parsePart(String value, EventRecurrence er) {
738            if (VALIDATE_UNTIL) {
739                try {
740                    // Parse the time to validate it.  The result isn't retained.
741                    Time until = new Time();
742                    until.parse(value);
743                } catch (TimeFormatException tfe) {
744                    throw new InvalidFormatException("Invalid UNTIL value: " + value);
745                }
746            }
747            er.until = value;
748            return PARSED_UNTIL;
749        }
750    }
751    /** parses COUNT=[non-negative-integer] */
752    private static class ParseCount extends PartParser {
753        @Override public int parsePart(String value, EventRecurrence er) {
754            er.count = parseIntRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
755            if (er.count < 0) {
756                Log.d(TAG, "Invalid Count. Forcing COUNT to 1 from " + value);
757                er.count = 1; // invalid count. assume one time recurrence.
758            }
759            return PARSED_COUNT;
760        }
761    }
762    /** parses INTERVAL=[non-negative-integer] */
763    private static class ParseInterval extends PartParser {
764        @Override public int parsePart(String value, EventRecurrence er) {
765            er.interval = parseIntRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
766            if (er.interval < 1) {
767                Log.d(TAG, "Invalid Interval. Forcing INTERVAL to 1 from " + value);
768                er.interval = 1;
769            }
770            return PARSED_INTERVAL;
771        }
772    }
773    /** parses BYSECOND=byseclist */
774    private static class ParseBySecond extends PartParser {
775        @Override public int parsePart(String value, EventRecurrence er) {
776            int[] bysecond = parseNumberList(value, 0, 59, true);
777            er.bysecond = bysecond;
778            er.bysecondCount = bysecond.length;
779            return PARSED_BYSECOND;
780        }
781    }
782    /** parses BYMINUTE=byminlist */
783    private static class ParseByMinute extends PartParser {
784        @Override public int parsePart(String value, EventRecurrence er) {
785            int[] byminute = parseNumberList(value, 0, 59, true);
786            er.byminute = byminute;
787            er.byminuteCount = byminute.length;
788            return PARSED_BYMINUTE;
789        }
790    }
791    /** parses BYHOUR=byhrlist */
792    private static class ParseByHour extends PartParser {
793        @Override public int parsePart(String value, EventRecurrence er) {
794            int[] byhour = parseNumberList(value, 0, 23, true);
795            er.byhour = byhour;
796            er.byhourCount = byhour.length;
797            return PARSED_BYHOUR;
798        }
799    }
800    /** parses BYDAY=bywdaylist, e.g. "1SU,-1SU" */
801    private static class ParseByDay extends PartParser {
802        @Override public int parsePart(String value, EventRecurrence er) {
803            int[] byday;
804            int[] bydayNum;
805            int bydayCount;
806
807            if (value.indexOf(",") < 0) {
808                /* only one entry, skip split() overhead */
809                bydayCount = 1;
810                byday = new int[1];
811                bydayNum = new int[1];
812                parseWday(value, byday, bydayNum, 0);
813            } else {
814                String[] wdays = value.split(",");
815                int len = wdays.length;
816                bydayCount = len;
817                byday = new int[len];
818                bydayNum = new int[len];
819                for (int i = 0; i < len; i++) {
820                    parseWday(wdays[i], byday, bydayNum, i);
821                }
822            }
823            er.byday = byday;
824            er.bydayNum = bydayNum;
825            er.bydayCount = bydayCount;
826            return PARSED_BYDAY;
827        }
828
829        /** parses [int]weekday, putting the pieces into parallel array entries */
830        private static void parseWday(String str, int[] byday, int[] bydayNum, int index) {
831            int wdayStrStart = str.length() - 2;
832            String wdayStr;
833
834            if (wdayStrStart > 0) {
835                /* number is included; parse it out and advance to weekday */
836                String numPart = str.substring(0, wdayStrStart);
837                int num = parseIntRange(numPart, -53, 53, false);
838                bydayNum[index] = num;
839                wdayStr = str.substring(wdayStrStart);
840            } else {
841                /* just the weekday string */
842                wdayStr = str;
843            }
844            Integer wday = sParseWeekdayMap.get(wdayStr);
845            if (wday == null) {
846                throw new InvalidFormatException("Invalid BYDAY value: " + str);
847            }
848            byday[index] = wday;
849        }
850    }
851    /** parses BYMONTHDAY=bymodaylist */
852    private static class ParseByMonthDay extends PartParser {
853        @Override public int parsePart(String value, EventRecurrence er) {
854            int[] bymonthday = parseNumberList(value, -31, 31, false);
855            er.bymonthday = bymonthday;
856            er.bymonthdayCount = bymonthday.length;
857            return PARSED_BYMONTHDAY;
858        }
859    }
860    /** parses BYYEARDAY=byyrdaylist */
861    private static class ParseByYearDay extends PartParser {
862        @Override public int parsePart(String value, EventRecurrence er) {
863            int[] byyearday = parseNumberList(value, -366, 366, false);
864            er.byyearday = byyearday;
865            er.byyeardayCount = byyearday.length;
866            return PARSED_BYYEARDAY;
867        }
868    }
869    /** parses BYWEEKNO=bywknolist */
870    private static class ParseByWeekNo extends PartParser {
871        @Override public int parsePart(String value, EventRecurrence er) {
872            int[] byweekno = parseNumberList(value, -53, 53, false);
873            er.byweekno = byweekno;
874            er.byweeknoCount = byweekno.length;
875            return PARSED_BYWEEKNO;
876        }
877    }
878    /** parses BYMONTH=bymolist */
879    private static class ParseByMonth extends PartParser {
880        @Override public int parsePart(String value, EventRecurrence er) {
881            int[] bymonth = parseNumberList(value, 1, 12, false);
882            er.bymonth = bymonth;
883            er.bymonthCount = bymonth.length;
884            return PARSED_BYMONTH;
885        }
886    }
887    /** parses BYSETPOS=bysplist */
888    private static class ParseBySetPos extends PartParser {
889        @Override public int parsePart(String value, EventRecurrence er) {
890            int[] bysetpos = parseNumberList(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
891            er.bysetpos = bysetpos;
892            er.bysetposCount = bysetpos.length;
893            return PARSED_BYSETPOS;
894        }
895    }
896    /** parses WKST={SU,MO,...} */
897    private static class ParseWkst extends PartParser {
898        @Override public int parsePart(String value, EventRecurrence er) {
899            Integer wkst = sParseWeekdayMap.get(value);
900            if (wkst == null) {
901                throw new InvalidFormatException("Invalid WKST value: " + value);
902            }
903            er.wkst = wkst;
904            return PARSED_WKST;
905        }
906    }
907}
908