A 90-day recurrence horizon silently truncates your semester
A semester runs about 15 to 16 weeks. A calendar tool that expands repeating events over a fixed 90-day horizon covers only 12.9 of those weeks — the last three weeks of term silently never render. No error, no warning; your calendar stops in November. In building Margin we hit this arithmetic and now expand recurrences to term length instead of a flat 90 days — the 90-day horizon survives only as a fallback for a series that declares no end at all.
The arithmetic, in one line
90 days ÷ 7 = 12.857 weeks, so a 90-day window covers about 12.9 weeks of a term. A weekly series that runs the full term has 15 or 16 occurrences, one per week starting in week one. Everything past the thirteenth falls outside the window.
| Term length | Weekly occurrences | Covered by a 90-day window | Missing |
|---|---|---|---|
| 15 weeks | 15 | 13 | 2 |
| 16 weeks | 16 | 13 | 3 |
The horizon is measured in days; the semester is measured in weeks. Nothing reconciles the two, so the shortfall is arithmetic rather than a bug in any one piece of data.
Why it happens at all
A calendar feed does not list your weekly homework twelve or sixteen times. It lists it once, as a single event with a recurrence rule attached:
BEGIN:VEVENT
SUMMARY:CS 225 Homework
DTSTART:20260119T235900
RRULE:FREQ=WEEKLY;COUNT=16
END:VEVENT
Anything reading that feed has to expand the rule into individual dates. An unbounded rule — FREQ=WEEKLY with no COUNT and no UNTIL — would expand forever, so a reader needs some stopping point. Ninety days is one such stopping point, and for a rule that genuinely never ends, any fixed horizon is a defensible choice.
The failure is applying that same stop to a rule that already told you where it ends. COUNT=16 and UNTIL=20260508T235900Z are both explicit bounds. When a reader clips them at 90 days anyway, it discards dates the feed handed it outright. There is no error condition to report — the expansion succeeded but stopped early — which is why the symptom is silence rather than a warning.
What it looks like from the student side
- Weekly homework, labs, or readings appear on schedule for the first two or three months, then stop.
- The gap starts at a round-ish distance from whenever you subscribed, not at a date that means anything academically.
- One-off items — a midterm, a paper, a final project — still show up in the same window that lost the weekly series, because they were never recurrences to begin with.
- Refreshing or re-subscribing may appear to fix it briefly, if your app anchors its window at the moment you subscribe: the visible end then moves forward by however long it has been since you last subscribed.
That last symptom is worth testing. Re-subscribe and watch the cliff. If it moves later rather than filling in, you are looking at an expansion window, not a missing assignment.
How to check
- Scroll your calendar to the last two weeks of the term and look for the weekly items you know are there — the recurring homework, the lab, the discussion post.
- Compare against the syllabus, which states the true end date.
- Note the date of the last visible occurrence. If that date sits roughly three months after you subscribed — and moves when you re-subscribe — the series was truncated, not canceled.
What to do about it
Non-product fixes first, because several of them are enough on their own.
- Re-subscribe or force a refresh near the end of term. Crude, and it only helps if your app anchors its window at the moment you subscribe — which is what the re-subscribe test above tells you. If it does, re-anchoring in, say, week 10 extends coverage past the end of a 15-week term. You have to remember to do it, which is the weakness.
- Check for an expansion or sync range setting. Some calendar apps expose an expansion or sync range under their sync or subscription settings; if yours does, widen it past your term length rather than leaving the default.
- Enter the tail weeks by hand from the syllabus. For one or two recurring items across three weeks this is ten minutes, once, and it is the only fix that does not depend on how a tool expands rules.
- Treat the syllabus as authoritative for the end of term. Whatever reads your feed, the last weeks of a semester are the part most worth verifying against the source document.
If you build something that reads calendar feeds: the rule is narrower than "raise the horizon". Let an explicit COUNT or UNTIL expand fully, and keep a horizon only for the unbounded case. Runaway protection belongs on a maximum occurrence count, which bounds the loop without discarding dates the feed declared.
A note on what a feed can tell you
Margin shows what Canvas exposes — check your syllabus too. A calendar feed carries what the course put on the calendar, and a recurrence rule carries what the instructor set when the series was created. If the tail of your term was never entered into Canvas in the first place, no expansion window, however generous, will produce it.
Evidence
RFC 5545 §3.8.5.3 (Recurrence Rule). COUNT and UNTIL are the two ways a recurrence rule states its own end; a rule carrying neither is unbounded. Both bounds are part of the rule the feed hands its reader.
Margin repo (first-person), extension/ical-parser.js:479-499 — re-grepped 2026-07-30. The comment above our expansion loop records the rule we settled on:
The 90-day horizon is ONLY a fallback for an UNBOUNDED series. An explicit COUNT or UNTIL must be allowed to expand fully even past the horizon -- otherwise a term-length weekly feed (FREQ=WEEKLY;COUNT=16, or UNTIL at term end ~15-16 weeks out) silently loses its tail beyond ~12.8 weeks […] Runaway protection still comes from maxCount (RRULE_MAX_OCCURRENCES) and countLimit (rule.count), enforced in the loop.
The same file anchors the unbounded fallback at Math.max(baseDue, nowMs) + horizonMs — our parser's own choice, recorded there as a fix for an earlier version that anchored at the series start and starved long-lived rules.
Margin repo (first-person), extension/test-ical-parser.js:449-459 — re-grepped 2026-07-30. The arithmetic is pinned by a test:
test('FREQ=WEEKLY;COUNT=16 expands fully even past the 90-day horizon', …)— "90 days is only ~12.86 weeks, so the old unconditional horizon clipped this to 13 and silently dropped the last 3 weeks of graded work." The test asserts all sixteen occurrences are emitted, the sixteenth landing 2026-04-16, about 105 days out.