FileDocCategorySizeDatePackage
EventRecurrence.javaAPI DocAndroid 5.1 API33524Thu Mar 12 22:22:50 GMT 2015com.android.calendarcommon2

EventRecurrence

public class EventRecurrence extends Object
Event recurrence utility functions.

Fields Summary
private static String
TAG
public static final int
SECONDLY
public static final int
MINUTELY
public static final int
HOURLY
public static final int
DAILY
public static final int
WEEKLY
public static final int
MONTHLY
public static final int
YEARLY
public static final int
SU
public static final int
MO
public static final int
TU
public static final int
WE
public static final int
TH
public static final int
FR
public static final int
SA
public android.text.format.Time
startDate
public int
freq
public String
until
public int
count
public int
interval
public int
wkst
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
private static HashMap
sParsePartMap
maps a part string to a parser object
private static final int
PARSED_FREQ
private static final int
PARSED_UNTIL
private static final int
PARSED_COUNT
private static final int
PARSED_INTERVAL
private static final int
PARSED_BYSECOND
private static final int
PARSED_BYMINUTE
private static final int
PARSED_BYHOUR
private static final int
PARSED_BYDAY
private static final int
PARSED_BYMONTHDAY
private static final int
PARSED_BYYEARDAY
private static final int
PARSED_BYWEEKNO
private static final int
PARSED_BYMONTH
private static final int
PARSED_BYSETPOS
private static final int
PARSED_WKST
private static final HashMap
sParseFreqMap
maps a FREQ value to an integer constant
private static final HashMap
sParseWeekdayMap
maps a two-character weekday string to an integer constant
private static final boolean
ALLOW_LOWER_CASE
If set, allow lower-case recurrence rule strings. Minor performance impact.
private static final boolean
VALIDATE_UNTIL
If set, validate the value of UNTIL parts. Minor performance impact.
private static final boolean
ONLY_ONE_UNTIL_COUNT
If set, require that only one of {UNTIL,COUNT} is present. Breaks compat w/ old parser.
Constructors Summary
Methods Summary
private voidappendByDay(java.lang.StringBuilder s, int i)

        int n = this.bydayNum[i];
        if (n != 0) {
            s.append(n);
        }

        String str = day2String(this.byday[i]);
        s.append(str);
    
private static voidappendNumbers(java.lang.StringBuilder s, java.lang.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 static booleanarraysEqual(int[] array1, int count1, int[] array2, int count2)
Determines whether two integer arrays contain identical elements.

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.

TODO: replace this with Arrays.equals() when the old parser goes away.

        if (count1 != count2) {
            return false;
        }

        for (int i = 0; i < count1; i++) {
            if (array1[i] != array2[i])
                return false;
        }

        return true;
    
public static intcalendarDay2Day(int day)
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.

        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 intday2CalendarDay(int 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.

        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);
        }
    
private static java.lang.Stringday2String(int 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.

        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);
        }
    
public static intday2TimeDay(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);
        }
    
public booleanequals(java.lang.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);
    
public inthashCode()

        // We overrode equals, so we must override hashCode().  Nobody seems to need this though.
        throw new UnsupportedOperationException();
    
public voidparse(java.lang.String recur)
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).

        /*
         * 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);
            }
        }
    
public booleanrepeatsMonthlyOnDayCount()
Determines whether this rule specifies a simple monthly rule by weekday, such as "FREQ=MONTHLY;BYDAY=3TU" (the 3rd Tuesday of every month).

Negative days, e.g. "FREQ=MONTHLY;BYDAY=-1TU" (the last Tuesday of every month), will cause "false" to be returned.

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

        if (this.freq != MONTHLY) {
            return false;
        }

        if (bydayCount != 1 || bymonthdayCount != 0) {
            return false;
        }

        if (bydayNum[0] <= 0) {
            return false;
        }

        return true;
    
public booleanrepeatsOnEveryWeekDay()

        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;
    
private voidresetFields()
Resets parser-modified fields to their initial state. Does not alter startDate.

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.

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.

        until = null;
        freq = count = interval = bysecondCount = byminuteCount = byhourCount =
            bydayCount = bymonthdayCount = byyeardayCount = byweeknoCount = bymonthCount =
            bysetposCount = 0;
    
public voidsetStartDate(android.text.format.Time date)

        startDate = date;
    
public static inttimeDay2Day(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 java.lang.StringtoString()

        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();