RFC 5545 §3.3.10 describes what a recurrence rule does with a 9×7 table containing two words, Expand and Limit, and the cell you land in is chosen by FREQ rather than by the part you wrote.
This engine implements no such table. It gives every period a window of candidate days whose width is FREQ — a year is 365 candidates, a month 28 to 31, a week 7 aligned to WKST, a day 1 — and runs one filter over it, returning an index rather than a boolean so the same call answers "is this a match" and "which part rejected it".
function rejectAt(dn, win){ // -1 = the day survives
if (byMonth && byMonth.indexOf(c.m) < 0) return 0;
...
if (byDay && !byDayOk(dn, win)) return 4;
return -1;
}
Expand and limit stop being two operators. BYMONTHDAY=13 against a 365-day window keeps 12 days, which is an expansion; against a 1-day window it keeps 0 or 1, which is a limit. The spec's table falls out of the window width — and the page then measures all 58 of its cells instead of trusting them.
Type a rule, get dates, ask why a date is missing: https://dev48.infy.uk/solve/day68-rrule-expander.html
The same nine characters, the opposite mechanism
FREQ=MONTHLY;BYMONTHDAY=13 -> the 13th of every month. BYMONTHDAY GENERATES.
FREQ=DAILY;BYMONTHDAY=13 -> the 13th of every month. BYMONTHDAY FILTERS.
Identical output. And BYDAY flips the same way from a neighbour: FREQ=YEARLY;BYDAY=FR is 52 Fridays, generating. Add BYMONTHDAY=13 and it is Friday the 13th — the part you added was BYMONTHDAY, and the part that changed meaning was BYDAY. An implementation cannot decide what a rule part does by looking at the rule part.
Why the wrong model survives every review
Four wrong engines, each a real mental model, scored against the correct one over 850 generated rules:
| FREQ | "every BYxxx is a filter on the stream" — rules wrong |
|---|---|
| DAILY | 0 of 200 |
| WEEKLY | 52 of 120 |
| MONTHLY | 168 of 230 |
| YEARLY | 276 of 300 |
Not approximately right. Exactly right in the corner where people test, and 92% wrong in the corner nobody writes fixtures for. Two smaller ones from the same corpus: forgetting that naming any of BYWEEKNO, BYYEARDAY, BYMONTHDAY or BYDAY deletes the defaults DTSTART would have supplied costs 178 of 850 rules, and hard-coding the ordinal's scope to the month costs 40.
Four things contradicted the intent; here are two
Verification first: 56 reference cases, most lifted from the RFC's own worked examples so the expected dates are not this engine's opinion; 609 in-page assertions; and 56,528 dates checked differentially against a per-day membership predicate of a completely different shape — walk every date in a window and ask whether it satisfies the rule. 0 disagreements.
Then the corrections. One of the 58 measured cells disagrees with the spec: BYDAY under YEARLY when BYWEEKNO is present, which Note 2 calls a special expand and the measurement calls a limit. The disagreement is structural. In the RFC's staged algorithm the intermediate set after BYWEEKNO is a set of weeks, and BYDAY expands each week into days; in a day-window model the week already exists as seven days, so BYDAY can only cut. Observationally identical — the differential test and the RFC's own week-20 example both pass — but the intermediate cardinalities are not, and reporting it as an expand would have meant a special case whose only purpose was to make a number agree.
And my own reference case was wrong, not the engine. I wrote the WKST=SU variant of the BYWEEKNO example expecting 1999 to shift by a week. The engine said 1998 shifts and 1999 does not. Working the four-day rule by hand for all three years agreed with it: 1 January 1998 is a Thursday, so under WKST=SU only three days of that week fall in 1998 and week 1 starts on 4 January. 1997 and 1999 land the same way under both conventions. The case now asserts the correct dates, and the page makes the more useful claim — the drift is not monotonic, so it is not an offset you can correct for.
Part of a from-scratch series — one tool a day, all client-side, dependency-free engine: https://dev48.infy.uk/solvefromzero.php
Top comments (0)