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