content: /*
   * Copyright (C) 2006 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.calendarcommon2;
  
  import android.text.TextUtils;
  import android.text.format.Time;
  import android.util.Log;
  import android.util.TimeFormatException;
  
  import java.util.Calendar;
  import java.util.HashMap;
  
  /**
   * Event recurrence utility functions.
   */
  public class EventRecurrence {
      private static String TAG = "EventRecur";
  
      public static final int SECONDLY = 1;
      public static final int MINUTELY = 2;
      public static final int HOURLY = 3;
      public static final int DAILY = 4;
      public static final int WEEKLY = 5;
      public static final int MONTHLY = 6;
      public static final int YEARLY = 7;
  
      public static final int SU = 0x00010000;
      public static final int MO = 0x00020000;
      public static final int TU = 0x00040000;
      public static final int WE = 0x00080000;
      public static final int TH = 0x00100000;
      public static final int FR = 0x00200000;
      public static final int SA = 0x00400000;
  
      public Time      startDate;     // set by setStartDate(), not parse()
  
      public int       freq;          // SECONDLY, MINUTELY, etc.
      public String    until;
      public int       count;
      public int       interval;
      public int       wkst;          // SU, MO, TU, etc.
  
      /* lists with zero entries may be null references */
      public int[]     bysecond;
      public int       bysecondCount;
      public int[]     byminute;
      public int       byminuteCount;
      public int[]     byhour;
      public int       byhourCount;
      public int[]     byday;
      public int[]     bydayNum;
      public int       bydayCount;
      public int[]     bymonthday;
      public int       bymonthdayCount;
      public int[]     byyearday;
      public int       byyeardayCount;
      public int[]     byweekno;
      public int       byweeknoCount;
      public int[]     bymonth;
      public int       bymonthCount;
      public int[]     bysetpos;
      public int       bysetposCount;
  
      /** maps a part string to a parser object */
      private static HashMap<String,PartParser> sParsePartMap;
      static {
          sParsePartMap = new HashMap<String,PartParser>();
          sParsePartMap.put("FREQ", new ParseFreq());
          sParsePartMap.put("UNTIL", new ParseUntil());
          sParsePartMap.put("COUNT", new ParseCount());
          sParsePartMap.put("INTERVAL", new ParseInterval());
          sParsePartMap.put("BYSECOND", new ParseBySecond());
          sParsePartMap.put("BYMINUTE", new ParseByMinute());
          sParsePartMap.put("BYHOUR", new ParseByHour());
          sParsePartMap.put("BYDAY", new ParseByDay());
          sParsePartMap.put("BYMONTHDAY", new ParseByMonthDay());
          sParsePartMap.put("BYYEARDAY", new ParseByYearDay());
          sParsePartMap.put("BYWEEKNO", new ParseByWeekNo());
          sParsePartMap.put("BYMONTH", new ParseByMonth());
          sParsePartMap.put("BYSETPOS", new ParseBySetPos());
          sParsePartMap.put("WKST", new ParseWkst());
      }
  
      /* values for bit vector that keeps track of what we have already seen */
      private static final int PARSED_FREQ = 1 << 0;
      private static final int PARSED_UNTIL = 1 << 1;
      private static final int PARSED_COUNT = 1 << 2;
      private static final int PARSED_INTERVAL = 1 << 3;
      private static final int PARSED_BYSECOND = 1 << 4;
      private static final int PARSED_BYMINUTE = 1 << 5;
      private static final int PARSED_BYHOUR = 1 << 6;
      private static final int PARSED_BYDAY = 1 << 7;
      private static final int PARSED_BYMONTHDAY = 1 << 8;
      private static final int PARSED_BYYEARDAY = 1 << 9;
      private static final int PARSED_BYWEEKNO = 1 << 10;
      private static final int PARSED_BYMONTH = 1 << 11;
      private static final int PARSED_BYSETPOS = 1 << 12;
      private static final int PARSED_WKST = 1 << 13;
  
      /** maps a FREQ value to an integer constant */
      private static final HashMap<String,Integer> sParseFreqMap = new HashMap<String,Integer>();
      static {
          sParseFreqMap.put("SECONDLY", SECONDLY);
          sParseFreqMap.put("MINUTELY", MINUTELY);
          sParseFreqMap.put("HOURLY", HOURLY);
          sParseFreqMap.put("DAILY", DAILY);
          sParseFreqMap.put("WEEKLY", WEEKLY);
          sParseFreqMap.put("MONTHLY", MONTHLY);
          sParseFreqMap.put("YEARLY", YEARLY);
      }
  
      /** maps a two-character weekday string to an integer constant */
      private static final HashMap<String,Integer> sParseWeekdayMap = new HashMap<String,Integer>();
      static {
          sParseWeekdayMap.put("SU", SU);
          sParseWeekdayMap.put("MO", MO);
          sParseWeekdayMap.put("TU", TU);
          sParseWeekdayMap.put("WE", WE);
          sParseWeekdayMap.put("TH", TH);
          sParseWeekdayMap.put("FR", FR);
          sParseWeekdayMap.put("SA", SA);
      }
  
      /** If set, allow lower-case recurrence rule strings.  Minor performance impact. */
      private static final boolean ALLOW_LOWER_CASE = true;
  
      /** If set, validate the value of UNTIL parts.  Minor performance impact. */
      private static final boolean VALIDATE_UNTIL = false;
  
      /** If set, require that only one of {UNTIL,COUNT} is present.  Breaks compat w/ old parser. */
      private static final boolean ONLY_ONE_UNTIL_COUNT = false;
  
  
      /**
       * Thrown when a recurrence string provided can not be parsed according
       * to RFC2445.
       */
      public static class InvalidFormatException extends RuntimeException {
          InvalidFormatException(String s) {
              super(s);
          }
      }
  
  
      public void setStartDate(Time date) {
          startDate = date;
      }
  
      /**
       * Converts one of the Calendar.SUNDAY constants to the SU, MO, etc.
       * constants.  btw, I think we should switch to those here too, to
       * get rid of this function, if possible.
       */
      public static int calendarDay2Day(int day)
      {
          switch (day)
          {
              case Calendar.SUNDAY:
                  return SU;
              case Calendar.MONDAY:
                  return MO;
              case Calendar.TUESDAY:
                  return TU;
              case Calendar.WEDNESDAY:
                  return WE;
              case Calendar.THURSDAY:
                  return TH;
              case Calendar.FRIDAY:
                  return FR;
              case Calendar.SATURDAY:
                  return SA;
              default:
                  throw new RuntimeException("bad day of week: " + day);
          }
      }
  
      public static int timeDay2Day(int day)
      {
          switch (day)
          {
              case Time.SUNDAY:
                  return SU;
              case Time.MONDAY:
                  return MO;
              case Time.TUESDAY:
                  return TU;
              case Time.WEDNESDAY:
                  return WE;
              case Time.THURSDAY:
                  return TH;
              case Time.FRIDAY:
                  return FR;
              case Time.SATURDAY:
                  return SA;
              default:
                  throw new RuntimeException("bad day of week: " + day);
          }
      }
      public static int day2TimeDay(int day)
      {
          switch (day)
          {
              case SU:
                  return Time.SUNDAY;
              case MO:
                  return Time.MONDAY;
              case TU:
                  return Time.TUESDAY;
              case WE:
                  return Time.WEDNESDAY;
              case TH:
                  return Time.THURSDAY;
              case FR:
                  return Time.FRIDAY;
              case SA:
                  return Time.SATURDAY;
              default:
                  throw new RuntimeException("bad day of week: " + day);
          }
      }
  
      /**
       * Converts one of the SU, MO, etc. constants to the Calendar.SUNDAY
       * constants.  btw, I think we should switch to those here too, to
       * get rid of this function, if possible.
       */
      public static int day2CalendarDay(int day)
      {
          switch (day)
          {
              case SU:
                  return Calendar.SUNDAY;
              case MO:
                  return Calendar.MONDAY;
              case TU:
                  return Calendar.TUESDAY;
              case WE:
                  return Calendar.WEDNESDAY;
              case TH:
                  return Calendar.THURSDAY;
              case FR:
                  return Calendar.FRIDAY;
              case SA:
                  return Calendar.SATURDAY;
              default:
                  throw new RuntimeException("bad day of week: " + day);
          }
      }
  
      /**
       * Converts one of the internal day constants (SU, MO, etc.) to the
       * two-letter string representing that constant.
       *
       * @param day one the internal constants SU, MO, etc.
       * @return the two-letter string for the day ("SU", "MO", etc.)
       *
       * @throws IllegalArgumentException Thrown if the day argument is not one of
       * the defined day constants.
       */
      private static String day2String(int day) {
          switch (day) {
          case SU:
              return "SU";
          case MO:
              return "MO";
          case TU:
              return "TU";
          case WE:
              return "WE";
          case TH:
              return "TH";
          case FR:
              return "FR";
          case SA:
              return "SA";
          default:
              throw new IllegalArgumentException("bad day argument: " + day);
          }
      }
  
      private static void appendNumbers(StringBuilder s, String label,
                                          int count, int[] values)
      {
          if (count > 0) {
              s.append(label);
              count--;
              for (int i=0; i<count; i++) {
                  s.append(values[i]);
                  s.append(",");
              }
              s.append(values[count]);
          }
      }
  
      private void appendByDay(StringBuilder s, int i)
      {
          int n = this.bydayNum[i];
          if (n != 0) {
              s.append(n);
          }
  
          String str = day2String(this.byday[i]);
          s.append(str);
      }
  
      @Override
      public String toString()
      {
          StringBuilder s = new StringBuilder();
  
          s.append("FREQ=");
          switch (this.freq)
          {
              case SECONDLY:
                  s.append("SECONDLY");
                  break;
              case MINUTELY:
                  s.append("MINUTELY");
                  break;
              case HOURLY:
                  s.append("HOURLY");
                  break;
              case DAILY:
                  s.append("DAILY");
                  break;
              case WEEKLY:
                  s.append("WEEKLY");
                  break;
              case MONTHLY:
                  s.append("MONTHLY");
                  break;
              case YEARLY:
                  s.append("YEARLY");
                  break;
          }
  
          if (!TextUtils.isEmpty(this.until)) {
              s.append(";UNTIL=");
              s.append(until);
          }
  
          if (this.count != 0) {
              s.append(";COUNT=");
              s.append(this.count);
          }
  
          if (this.interval != 0) {
              s.append(";INTERVAL=");
              s.append(this.interval);
          }
  
          if (this.wkst != 0) {
              s.append(";WKST=");
              s.append(day2String(this.wkst));
          }
  
          appendNumbers(s, ";BYSECOND=", this.bysecondCount, this.bysecond);
          appendNumbers(s, ";BYMINUTE=", this.byminuteCount, this.byminute);
          appendNumbers(s, ";BYSECOND=", this.byhourCount, this.byhour);
  
          // day
          int count = this.bydayCount;
          if (count > 0) {
              s.append(";BYDAY=");
              count--;
              for (int i=0; i<count; i++) {
                  appendByDay(s, i);
                  s.append(",");
              }
              appendByDay(s, count);
          }
  
          appendNumbers(s, ";BYMONTHDAY=", this.bymonthdayCount, this.bymonthday);
          appendNumbers(s, ";BYYEARDAY=", this.byyeardayCount, this.byyearday);
          appendNumbers(s, ";BYWEEKNO=", this.byweeknoCount, this.byweekno);
          appendNumbers(s, ";BYMONTH=", this.bymonthCount, this.bymonth);
          appendNumbers(s, ";BYSETPOS=", this.bysetposCount, this.bysetpos);
  
          return s.toString();
      }
  
      public boolean repeatsOnEveryWeekDay() {
          if (this.freq != WEEKLY) {
              return false;
          }
  
          int count = this.bydayCount;
          if (count != 5) {
              return false;
          }
  
          for (int i = 0 ; i < count ; i++) {
              int day = byday[i];
              if (day == SU || day == SA) {
                  return false;
              }
          }
  
          return true;
      }
  
      /**
       * Determines whether this rule specifies a simple monthly rule by weekday, such as
       * "FREQ=MONTHLY;BYDAY=3TU" (the 3rd Tuesday of every month).
       * <p>
       * Negative days, e.g. "FREQ=MONTHLY;BYDAY=-1TU" (the last Tuesday of every month),
       * will cause "false" to be returned.
       * <p>
       * Rules that fire every week, such as "FREQ=MONTHLY;BYDAY=TU" (every Tuesday of every
       * month) will cause "false" to be returned.  (Note these are usually expressed as
       * WEEKLY rules, and hence are uncommon.)
       *
       * @return true if this rule is of the appropriate form
       */
      public boolean repeatsMonthlyOnDayCount() {
          if (this.freq != MONTHLY) {
              return false;
          }
  
          if (bydayCount != 1 || bymonthdayCount != 0) {
              return false;
          }
  
          if (bydayNum[0] <= 0) {
              return false;
          }
  
          return true;
      }
  
      /**
       * Determines whether two integer arrays contain identical elements.
       * <p>
       * The native implementation over-allocated the arrays (and may have stuff left over from
       * a previous run), so we can't just check the arrays -- the separately-maintained count
       * field also matters.  We assume that a null array will have a count of zero, and that the
       * array can hold as many elements as the associated count indicates.
       * <p>
       * TODO: replace this with Arrays.equals() when the old parser goes away.
       */
      private static boolean arraysEqual(int[] array1, int count1, int[] array2, int count2) {
          if (count1 != count2) {
              return false;
          }
  
          for (int i = 0; i < count1; i++) {
              if (array1[i] != array2[i])
                  return false;
          }
  
          return true;
      }
  
      @Override
      public boolean equals(Object obj) {
          if (this == obj) {
              return true;
          }
          if (!(obj instanceof EventRecurrence)) {
              return false;
          }
  
          EventRecurrence er = (EventRecurrence) obj;
          return  (startDate == null ?
                          er.startDate == null : Time.compare(startDate, er.startDate) == 0) &&
                  freq == er.freq &&
                  (until == null ? er.until == null : until.equals(er.until)) &&
                  count == er.count &&
                  interval == er.interval &&
                  wkst == er.wkst &&
                  arraysEqual(bysecond, bysecondCount, er.bysecond, er.bysecondCount) &&
                  arraysEqual(byminute, byminuteCount, er.byminute, er.byminuteCount) &&
                  arraysEqual(byhour, byhourCount, er.byhour, er.byhourCount) &&
                  arraysEqual(byday, bydayCount, er.byday, er.bydayCount) &&
                  arraysEqual(bydayNum, bydayCount, er.bydayNum, er.bydayCount) &&
                  arraysEqual(bymonthday, bymonthdayCount, er.bymonthday, er.bymonthdayCount) &&
                  arraysEqual(byyearday, byyeardayCount, er.byyearday, er.byyeardayCount) &&
                  arraysEqual(byweekno, byweeknoCount, er.byweekno, er.byweeknoCount) &&
                  arraysEqual(bymonth, bymonthCount, er.bymonth, er.bymonthCount) &&
                  arraysEqual(bysetpos, bysetposCount, er.bysetpos, er.bysetposCount);
      }
  
      @Override public int hashCode() {
          // We overrode equals, so we must override hashCode().  Nobody seems to need this though.
          throw new UnsupportedOperationException();
      }
  
      /**
       * Resets parser-modified fields to their initial state.  Does not alter startDate.
       * <p>
       * The original parser always set all of the "count" fields, "wkst", and "until",
       * essentially allowing the same object to be used multiple times by calling parse().
       * It's unclear whether this behavior was intentional.  For now, be paranoid and
       * preserve the existing behavior by resetting the fields.
       * <p>
       * We don't need to touch the integer arrays; they will either be ignored or
       * overwritten.  The "startDate" field is not set by the parser, so we ignore it here.
       */
      private void resetFields() {
          until = null;
          freq = count = interval = bysecondCount = byminuteCount = byhourCount =
              bydayCount = bymonthdayCount = byyeardayCount = byweeknoCount = bymonthCount =
              bysetposCount = 0;
      }
  
      /**
       * Parses an rfc2445 recurrence rule string into its component pieces.  Attempting to parse
       * malformed input will result in an EventRecurrence.InvalidFormatException.
       *
       * @param recur The recurrence rule to parse (in un-folded form).
       */
      public void parse(String recur) {
          /*
           * From RFC 2445 section 4.3.10:
           *
           * recur = "FREQ"=freq *(
           *       ; either UNTIL or COUNT may appear in a 'recur',
           *       ; but UNTIL and COUNT MUST NOT occur in the same 'recur'
           *
           *       ( ";" "UNTIL" "=" enddate ) /
           *       ( ";" "COUNT" "=" 1*DIGIT ) /
           *
           *       ; the rest of these keywords are optional,
           *       ; but MUST NOT occur more than once
           *
           *       ( ";" "INTERVAL" "=" 1*DIGIT )          /
           *       ( ";" "BYSECOND" "=" byseclist )        /
           *       ( ";" "BYMINUTE" "=" byminlist )        /
           *       ( ";" "BYHOUR" "=" byhrlist )           /
           *       ( ";" "BYDAY" "=" bywdaylist )          /
           *       ( ";" "BYMONTHDAY" "=" bymodaylist )    /
           *       ( ";" "BYYEARDAY" "=" byyrdaylist )     /
           *       ( ";" "BYWEEKNO" "=" bywknolist )       /
           *       ( ";" "BYMONTH" "=" bymolist )          /
           *       ( ";" "BYSETPOS" "=" bysplist )         /
           *       ( ";" "WKST" "=" weekday )              /
           *       ( ";" x-name "=" text )
           *       )
           *
           *  The rule parts are not ordered in any particular sequence.
           *
           * Examples:
           *   FREQ=MONTHLY;INTERVAL=2;COUNT=10;BYDAY=1SU,-1SU
           *   FREQ=YEARLY;INTERVAL=4;BYMONTH=11;BYDAY=TU;BYMONTHDAY=2,3,4,5,6,7,8
           *
           * Strategy:
           * (1) Split the string at ';' boundaries to get an array of rule "parts".
           * (2) For each part, find substrings for left/right sides of '=' (name/value).
           * (3) Call a <name>-specific parsing function to parse the <value> into an
           *     output field.
           *
           * By keeping track of which names we've seen in a bit vector, we can verify the
           * constraints indicated above (FREQ appears first, none of them appear more than once --
           * though x-[name] would require special treatment), and we have either UNTIL or COUNT
           * but not both.
           *
           * In general, RFC 2445 property names (e.g. "FREQ") and enumerations ("TU") must
           * be handled in a case-insensitive fashion, but case may be significant for other
           * properties.  We don't have any case-sensitive values in RRULE, except possibly
           * for the custom "X-" properties, but we ignore those anyway.  Thus, we can trivially
           * convert the entire string to upper case and then use simple comparisons.
           *
           * Differences from previous version:
           * - allows lower-case property and enumeration values [optional]
           * - enforces that FREQ appears first
           * - enforces that only one of UNTIL and COUNT may be specified
           * - allows (but ignores) X-* parts
           * - improved validation on various values (e.g. UNTIL timestamps)
           * - error messages are more specific
           *
           * TODO: enforce additional constraints listed in RFC 5545, notably the "N/A" entries
           * in section 3.3.10.  For example, if FREQ=WEEKLY, we should reject a rule that
           * includes a BYMONTHDAY part.
           */
  
          /* TODO: replace with "if (freq != 0) throw" if nothing requires this */
          resetFields();
  
          int parseFlags = 0;
          String[] parts;
          if (ALLOW_LOWER_CASE) {
              parts = recur.toUpperCase().split(";");
          } else {
              parts = recur.split(";");
          }
          for (String part : parts) {
              // allow empty part (e.g., double semicolon ";;")
              if (TextUtils.isEmpty(part)) {
                  continue;
              }
              int equalIndex = part.indexOf('=');
              if (equalIndex <= 0) {
                  /* no '=' or no LHS */
                  throw new InvalidFormatException("Missing LHS in " + part);
              }
  
              String lhs = part.substring(0, equalIndex);
              String rhs = part.substring(equalIndex + 1);
              if (rhs.length() == 0) {
                  throw new InvalidFormatException("Missing RHS in " + part);
              }
  
              /*
               * In lieu of a "switch" statement that allows string arguments, we use a
               * map from strings to parsing functions.
               */
              PartParser parser = sParsePartMap.get(lhs);
              if (parser == null) {
                  if (lhs.startsWith("X-")) {
                      //Log.d(TAG, "Ignoring custom part " + lhs);
                      continue;
                  }
                  throw new InvalidFormatException("Couldn't find parser for " + lhs);
              } else {
                  int flag = parser.parsePart(rhs, this);
                  if ((parseFlags & flag) != 0) {
                      throw new InvalidFormatException("Part " + lhs + " was specified twice");
                  }
                  parseFlags |= flag;
              }
          }
  
          // If not specified, week starts on Monday.
          if ((parseFlags & PARSED_WKST) == 0) {
              wkst = MO;
          }
  
          // FREQ is mandatory.
          if ((parseFlags & PARSED_FREQ) == 0) {
              throw new InvalidFormatException("Must specify a FREQ value");
          }
  
          // Can't have both UNTIL and COUNT.
          if ((parseFlags & (PARSED_UNTIL | PARSED_COUNT)) == (PARSED_UNTIL | PARSED_COUNT)) {
              if (ONLY_ONE_UNTIL_COUNT) {
                  throw new InvalidFormatException("Must not specify both UNTIL and COUNT: " + recur);
              } else {
                  Log.w(TAG, "Warning: rrule has both UNTIL and COUNT: " + recur);
              }
          }
      }
  
      /**
       * Base class for the RRULE part parsers.
       */
      abstract static class PartParser {
          /**
           * Parses a single part.
           *
           * @param value The right-hand-side of the part.
           * @param er The EventRecurrence into which the result is stored.
           * @return A bit value indicating which part was parsed.
           */
          public abstract int parsePart(String value, EventRecurrence er);
  
          /**
           * Parses an integer, with range-checking.
           *
           * @param str The string to parse.
           * @param minVal Minimum allowed value.
           * @param maxVal Maximum allowed value.
           * @param allowZero Is 0 allowed?
           * @return The parsed value.
           */
          public static int parseIntRange(String str, int minVal, int maxVal, boolean allowZero) {
              try {
                  if (str.charAt(0) == '+') {
                      // Integer.parseInt does not allow a leading '+', so skip it manually.
                      str = str.substring(1);
                  }
                  int val = Integer.parseInt(str);
                  if (val < minVal || val > maxVal || (val == 0 && !allowZero)) {
                      throw new InvalidFormatException("Integer value out of range: " + str);
                  }
                  return val;
              } catch (NumberFormatException nfe) {
                  throw new InvalidFormatException("Invalid integer value: " + str);
              }
          }
  
          /**
           * Parses a comma-separated list of integers, with range-checking.
           *
           * @param listStr The string to parse.
           * @param minVal Minimum allowed value.
           * @param maxVal Maximum allowed value.
           * @param allowZero Is 0 allowed?
           * @return A new array with values, sized to hold the exact number of elements.
           */
          public static int[] parseNumberList(String listStr, int minVal, int maxVal,
                  boolean allowZero) {
              int[] values;
  
              if (listStr.indexOf(",") < 0) {
                  // Common case: only one entry, skip split() overhead.
                  values = new int[1];
                  values[0] = parseIntRange(listStr, minVal, maxVal, allowZero);
              } else {
                  String[] valueStrs = listStr.split(",");
                  int len = valueStrs.length;
                  values = new int[len];
                  for (int i = 0; i < len; i++) {
                      values[i] = parseIntRange(valueStrs[i], minVal, maxVal, allowZero);
                  }
              }
              return values;
          }
     }
  
      /** parses FREQ={SECONDLY,MINUTELY,...} */
      private static class ParseFreq extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              Integer freq = sParseFreqMap.get(value);
              if (freq == null) {
                  throw new InvalidFormatException("Invalid FREQ value: " + value);
              }
              er.freq = freq;
              return PARSED_FREQ;
          }
      }
      /** parses UNTIL=enddate, e.g. "19970829T021400" */
      private static class ParseUntil extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              if (VALIDATE_UNTIL) {
                  try {
                      // Parse the time to validate it.  The result isn't retained.
                      Time until = new Time();
                      until.parse(value);
                  } catch (TimeFormatException tfe) {
                      throw new InvalidFormatException("Invalid UNTIL value: " + value);
                  }
              }
              er.until = value;
              return PARSED_UNTIL;
          }
      }
      /** parses COUNT=[non-negative-integer] */
      private static class ParseCount extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              er.count = parseIntRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
              if (er.count < 0) {
                  Log.d(TAG, "Invalid Count. Forcing COUNT to 1 from " + value);
                  er.count = 1; // invalid count. assume one time recurrence.
              }
              return PARSED_COUNT;
          }
      }
      /** parses INTERVAL=[non-negative-integer] */
      private static class ParseInterval extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              er.interval = parseIntRange(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
              if (er.interval < 1) {
                  Log.d(TAG, "Invalid Interval. Forcing INTERVAL to 1 from " + value);
                  er.interval = 1;
              }
              return PARSED_INTERVAL;
          }
      }
      /** parses BYSECOND=byseclist */
      private static class ParseBySecond extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              int[] bysecond = parseNumberList(value, 0, 59, true);
              er.bysecond = bysecond;
              er.bysecondCount = bysecond.length;
              return PARSED_BYSECOND;
          }
      }
      /** parses BYMINUTE=byminlist */
      private static class ParseByMinute extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              int[] byminute = parseNumberList(value, 0, 59, true);
              er.byminute = byminute;
              er.byminuteCount = byminute.length;
              return PARSED_BYMINUTE;
          }
      }
      /** parses BYHOUR=byhrlist */
      private static class ParseByHour extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              int[] byhour = parseNumberList(value, 0, 23, true);
              er.byhour = byhour;
              er.byhourCount = byhour.length;
              return PARSED_BYHOUR;
          }
      }
      /** parses BYDAY=bywdaylist, e.g. "1SU,-1SU" */
      private static class ParseByDay extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              int[] byday;
              int[] bydayNum;
              int bydayCount;
  
              if (value.indexOf(",") < 0) {
                  /* only one entry, skip split() overhead */
                  bydayCount = 1;
                  byday = new int[1];
                  bydayNum = new int[1];
                  parseWday(value, byday, bydayNum, 0);
              } else {
                  String[] wdays = value.split(",");
                  int len = wdays.length;
                  bydayCount = len;
                  byday = new int[len];
                  bydayNum = new int[len];
                  for (int i = 0; i < len; i++) {
                      parseWday(wdays[i], byday, bydayNum, i);
                  }
              }
              er.byday = byday;
              er.bydayNum = bydayNum;
              er.bydayCount = bydayCount;
              return PARSED_BYDAY;
          }
  
          /** parses [int]weekday, putting the pieces into parallel array entries */
          private static void parseWday(String str, int[] byday, int[] bydayNum, int index) {
              int wdayStrStart = str.length() - 2;
              String wdayStr;
  
              if (wdayStrStart > 0) {
                  /* number is included; parse it out and advance to weekday */
                  String numPart = str.substring(0, wdayStrStart);
                  int num = parseIntRange(numPart, -53, 53, false);
                  bydayNum[index] = num;
                  wdayStr = str.substring(wdayStrStart);
              } else {
                  /* just the weekday string */
                  wdayStr = str;
              }
              Integer wday = sParseWeekdayMap.get(wdayStr);
              if (wday == null) {
                  throw new InvalidFormatException("Invalid BYDAY value: " + str);
              }
              byday[index] = wday;
          }
      }
      /** parses BYMONTHDAY=bymodaylist */
      private static class ParseByMonthDay extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              int[] bymonthday = parseNumberList(value, -31, 31, false);
              er.bymonthday = bymonthday;
              er.bymonthdayCount = bymonthday.length;
              return PARSED_BYMONTHDAY;
          }
      }
      /** parses BYYEARDAY=byyrdaylist */
      private static class ParseByYearDay extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              int[] byyearday = parseNumberList(value, -366, 366, false);
              er.byyearday = byyearday;
              er.byyeardayCount = byyearday.length;
              return PARSED_BYYEARDAY;
          }
      }
      /** parses BYWEEKNO=bywknolist */
      private static class ParseByWeekNo extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              int[] byweekno = parseNumberList(value, -53, 53, false);
              er.byweekno = byweekno;
              er.byweeknoCount = byweekno.length;
              return PARSED_BYWEEKNO;
          }
      }
      /** parses BYMONTH=bymolist */
      private static class ParseByMonth extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              int[] bymonth = parseNumberList(value, 1, 12, false);
              er.bymonth = bymonth;
              er.bymonthCount = bymonth.length;
              return PARSED_BYMONTH;
          }
      }
      /** parses BYSETPOS=bysplist */
      private static class ParseBySetPos extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              int[] bysetpos = parseNumberList(value, Integer.MIN_VALUE, Integer.MAX_VALUE, true);
              er.bysetpos = bysetpos;
              er.bysetposCount = bysetpos.length;
              return PARSED_BYSETPOS;
          }
      }
      /** parses WKST={SU,MO,...} */
      private static class ParseWkst extends PartParser {
          @Override public int parsePart(String value, EventRecurrence er) {
              Integer wkst = sParseWeekdayMap.get(value);
              if (wkst == null) {
                  throw new InvalidFormatException("Invalid WKST value: " + value);
              }
              er.wkst = wkst;
              return PARSED_WKST;
          }
      }
  }
  