All-day events: the end date is exclusive

In the iCalendar format, an all-day event's end date is exclusive: per RFC 5545 §3.6.1, DTEND names the day after the last day of the event. "Homework due Friday" exports as DTSTART Friday, DTEND Saturday. Calendar apps that display the raw DTEND show all-day assignments one day late — and half-fixing it (shifting DTSTART too) is its own bug, because the exclusivity applies to the end only.

What you actually see

You added your school calendar feed to a calendar or to-do app, and the dates are wrong in a very specific way: every all-day item sits one day later than the syllabus says. Timed events — an 11:59 PM deadline with a real clock time — look fine. Only the all-day ones drift, and they all drift by exactly one day, in the same direction, every time.

That consistency is the tell. A timezone problem moves some events and not others, and only by hours. A uniform one-day shift on all-day items is the exclusive end date being displayed as if it were inclusive.

Why the format works this way

An all-day event in iCalendar uses date values rather than date-times:

BEGIN:VEVENT
SUMMARY:Problem Set 4
DTSTART;VALUE=DATE:20260306
DTEND;VALUE=DATE:20260307
END:VEVENT

DTSTART is Friday 6 March. DTEND is Saturday 7 March — and the event does not include Saturday. RFC 5545 §3.6.1 states it directly: "The 'DTEND' property for a 'VEVENT' calendar component specifies the non-inclusive end of the event." It marks the first moment after the event. For a date-valued event that means the event covers DTSTART up to, but not including, DTEND.

The rule is not an accident or a vendor bug. It is what makes the arithmetic clean: a one-day event is DTEND - DTSTART = 1 day, a three-day event is 3, and a run of adjacent events tiles the calendar without overlaps or gaps. It is the same half-open interval convention as a Python slice or a C++ iterator range. The cost of that tidiness is that the number a human reads in the file is one day past the day they mean.

The half-fix that makes it worse

Once you know all-day events are off by one, the tempting patch is to subtract a day. Subtract it from the wrong field and you have moved the bug rather than fixed it.

The exclusivity applies to DTEND only:

This matters most when an event has no DTEND at all. RFC 5545 §3.6.1 allows a VEVENT to carry DTSTART alone; a date-valued event with no end is a one-day event on DTSTART. If your code falls back to DTSTART as the due date and applies the same minus-one shift it applies to DTEND, every such assignment lands a day early — the original bug, mirrored. Getting half the rule right is its own defect, and it is harder to spot because now some items are late and others early.

What to do about it

If you are a student and your subscribed calendar is a day off. You are not doing anything wrong, and there is usually nothing to fix on your end — the feed is most likely correct and the display is reading the end date literally. Options, in order of how little work they are:

  1. Read the start, not the end. Open the event — the start date is the real due date for an all-day item. A multi-day bar's trailing edge is the part that reads one day too long.
  2. Check the source. Canvas, your LMS, or the syllabus is the authority on the date. If the feed shows Friday and your app shows Saturday, Friday is the answer.
  3. Try a different reader. Well-behaved calendar clients apply the rule correctly. If one app shows all-day items a day late and another shows them right, the app is the variable, not your feed.
  4. Report it. If a calendar tool consistently renders all-day feed items one day late, that is a genuine bug in that tool and worth a support ticket — quoting RFC 5545 §3.6.1 gives the report something concrete to point at.

If you are building something that reads .ics files. Three rules cover almost every real feed:

  1. Treat DTEND;VALUE=DATE as exclusive. Subtract one day before showing it to a human or storing it as a due date.
  2. Never apply that subtraction to DTSTART, including when DTSTART is your fallback for a missing DTEND.
  3. Test both shapes. A fixture with DTSTART+DTEND and a fixture with DTSTART alone will catch the mirrored bug immediately; a test suite with only the first shape will not.

A useful sanity check: for a correct implementation, a single all-day event should render the same whether it is written as DTSTART:20260306/DTEND:20260307 or as DTSTART:20260306 with no end. If those two produce different dates, one of the paths has the rule wrong.

Sources

RFC 5545 is the published normative specification for the iCalendar format. It is not vendor-specific: the rule holds for any .ics feed, from any calendar or learning-management system.

Questions people ask

Why does my subscribed assignment calendar show everything one day late? If the items are all-day events, the reading app is almost certainly displaying the raw DTEND, which per RFC 5545 §3.6.1 is the day after the event's last day. The start date is the real date.

Is my school's calendar feed broken? Probably not. Exporting "due Friday" as DTSTART Friday / DTEND Saturday is the specification-correct encoding. The mismatch is in how the date is displayed, not in how it was written.

Why do only some events look wrong? The exclusive-end rule bites on all-day (date-valued) events. Events with a real clock time — an 11:59 PM deadline, say — use date-time values and are not affected by this rule. A shift that hits timed events instead usually points at a timezone problem, which is a separate issue.

Can I fix it in Google Calendar or Apple Calendar? If a client renders all-day feed items a day late, the fix belongs in that client — there is no per-feed toggle that changes how the spec is interpreted. In the meantime, read the start date.

Does this affect multi-day events too? Yes, and the same way. A three-day event running Monday to Wednesday is written DTEND Thursday. Displaying the raw end makes it look like a four-day event. RFC 5545 §3.6.1 shows this in its own worked example: an event held 28 June to 8 July 2007 inclusively is encoded DTSTART;VALUE=DATE:20070628 / DTEND;VALUE=DATE:20070709, "since the 'DTEND' property specifies the non-inclusive end of the event."

I am writing a parser — do I subtract a day from every date? No. Subtract from DTEND only. DTSTART is already the first included day, and shifting it — including when you use it as a fallback due date for events with no DTEND — creates the same off-by-one in the other direction.