Search code examples
icalendarrrule

YEARLY RRULE interpretation when more than one of BYMONTH, BYWEEKNO, BYYEARDAY are present


I am currently struggling with an interpretation detail of RFC 5545. Section 3.3.10 states that essentially, all BYxxx fields show an expanding behavior, except for BYDAY, which has specific rules depending on which BYxxx fields are actually present.

However, it is unclear to me what exactly is supposed to happen for an RRULE that specifies more than one of {BYMONTH, BYWEEKNO, BYYEARDAY}. In my opinion the table in Section 3.3.10 allows for the interpretation that I have to expand every rule part separately, that is, to build the union of the three sets resulting from

  1. Expanding BYMONTH, BYMONTHDAY etc.
  2. Expanding BYWEEKNO
  3. Expanding BYYEARDAY

all the while honoring Note 2 that explains the BYDAY behavior. The result can be somewhat counter-intuitive (and of course it is fairly unlikely that users would specify such rules). But there is at least one implementation out there that claims to do it this way:

For the YEARLY frequency, the BYMONTH, BYWEEKNO, and BYYEARDAY rules are expanded separately from each other if specified.

Other implementations, such as http://recurrence-expansion-service.appspot.com/ or Google Calendar, appear to use a limiting strategy if more than one of the rule parts are present:

  1. Expand BYMONTH/BYMONTHDAY
  2. Then limit using BYWEEKNO and BYYEARDAY

effeectively creating an intersection rather than a union. That approach seems somewhat consistent with the application order for the rule parts stipulated by Section 3.3.10:

If multiple BYxxx rule parts are specified, then after evaluating the specified FREQ and INTERVAL rule parts, the BYxxx rule parts are applied to the current set of evaluated occurrences in the following order: BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY, BYHOUR, BYMINUTE, BYSECOND and BYSETPOS; then COUNT and UNTIL are evaluated.

However, interpreting this to mean that the rule parts are limiting rather than expanding appears to me to be in direct contradiction of the expand/limit table shown on page 44 of the standard.

My question thus is:

Does RFC5545 actually unambiguously specify how the simultaneous presence of more than one of BYMONTH, BYWEEKNO and BYYEARDAY is to be interpreted? If so, where does it state it? And if the standard is actually unclear in this regard, is there a “de-facto standard” preferred way of dealing with this situation?


Solution

  • Does RFC5545 actually unambiguously specify how the simultaneous presence of more than one of BYMONTH, BYWEEKNO and BYYEARDAY is to be interpreted?

    Well yes, in the excerpt you quoted in your question: they are applied to the current set of evaluated occurrences in order.

    The term current set of evaluated occurrences is the key part of the sentence. For example let's take a rule with BYMONTH and BYYEARDAY.

    1. BYMONTH is evaluated first, generating a set of occurrences (expanding on the YEARLY frequency, i.e. more than one occurrence per year can be in the set)
    2. only then BYYEARDAY is applied on those occurrences only, i.e. the occurrences that matches BYMONTH. Every year day that does not match BYMONTH is not considered, because it is not, by definition, part of the current set of occurrences calculated in step 1.

    The expand/limit table is irrelevant in this case. The RFC states very clearly:

    "The table below summarizes the dependency of BYxxx rule part expand or limit behavior on the FREQ rule part value."

    The table does not summarizes the behavior of BYxxx rule part on other BYxxx rule parts.

    Therefore it is unambiguously an intersection. Think about it that way: if the result was a union, there would be no way to write rules such as "every day of the 5th week of the year that are in February" (FREQ=YEARLY;BYWEEKNO=5;BYMONTH=2).

    To achieve a union, the RFC defines a recurrence set, which is simply combining multiple RRULE, RDATE and EXDATE (though for some reason the RFC states that RRULE should not appear more than once).

    And if the standard is actually unclear in this regard, is there a “de-facto standard” preferred way of dealing with this situation?

    I am the maintainer of php-rrule which is a port of Python dateutil and this is how both version handle this situation. As far as I remember, Javascript and Ruby libraries also work this way. I don't know about every other libraries that is out there, but the RFC is quite clear anyway, as I hope I have demonstrated above.

    Edit: answer to your comment about BYDAY

    The Note 2 of the table can be indeed quite confusing, so here is a little clarification. BYDAY is special because it has 2 syntaxes: the simple one with the name of the day (e.g. MO, TU and so on) and the "complicated" one with the name of the day and the position in the set. For example 1MO means "the first Monday" and -1MO "the last Monday". However the question is "the first/last Monday of what?".

    • When using a YEARLY frequency and BYDAY=1MO,2MO, it is "the first and the second Monday of the year", when using MONTHLY it is "the first and the second Monday of the month" and so on, this is pretty straightforward.
    • However when using a YEARLY frequency and BYMONTH=2,3 for example then the rule actually means "every February and March" so the question is: should you get every first and second Monday of February and March (expand), or every first and second Monday of the year that occur in February and March - i.e. empty set, since both are in January (limit). The answer is to do a "special expand" by actually treating it as a "MONTHLY" frequency (even if it is YEARLY) and therefore the meaning of BYDAY=1MO,2MO changes to "every first and second Monday of February AND every first and second Monday of March`.

    Quote relevant part of the RFC

    The numeric value in a BYDAY rule part with the FREQ rule part set to YEARLY corresponds to an offset within the month when the BYMONTH rule part is present, and corresponds to an offset within the year when the BYWEEKNO or BYMONTH rule parts are present.

    To be perfectly honest I'm not sure why there is this special case for BYDAY, but in any case it's quite clear how it should behave.