/**
   * 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);
      }
    }
  }