Home | History | Annotate | Download | only in calendarcommon2
      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 
     17 package com.android.calendarcommon2;
     18 
     19 import android.text.TextUtils;
     20 import android.text.format.Time;
     21 import android.util.Log;
     22 import android.util.TimeFormatException;
     23 
     24 import java.util.Calendar;
     25 import java.util.HashMap;
     26 
     27 /**
     28  * Event recurrence utility functions.
     29  */
     30 public 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