Skip to content

fix: preserve calendar date for whole-day events with non-UTC offsets - #91

Draft
dominik1001 wants to merge 2 commits into
mainfrom
claude/serene-brown-gdwj73
Draft

fix: preserve calendar date for whole-day events with non-UTC offsets#91
dominik1001 wants to merge 2 commits into
mainfrom
claude/serene-brown-gdwj73

Conversation

@dominik1001

@dominik1001 dominik1001 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

Whole-day events created (or updated) with a local-midnight ISO datetime at a positive UTC offset (e.g. 2026-09-30T00:00:00+09:00) were stored one calendar day early on the server.

Root cause

new Date(iso) preserves the input's UTC instant, not its local calendar date. ts-caldav then derives the stored VALUE=DATE (for DTSTART/DTEND/RRULE UNTIL on whole-day events) via date.toISOString().split("T")[0]. For any offset east of UTC, that truncation silently rolls the calendar date back by one day — e.g. 2026-09-30T00:00:00+09:00 becomes the UTC instant 2026-09-29T15:00:00.000Z, which truncates to 2026-09-29.

Fix

Added toWholeDayDate() (src/tools/whole-day-date.ts), which takes the calendar date directly from the ISO input string and anchors it at UTC midnight, so ts-caldav's later toISOString() truncation is a no-op. create-event and update-event now use it for start/end/recurrenceRule.until whenever the event is (or is being updated to be, or already is) whole-day. update-event falls back to the existing event's wholeDay flag when the caller doesn't pass one explicitly, so partial updates (e.g. just changing start) on an existing whole-day event stay date-correct too.

Test plan

  • npm run validate (check + test + knip + docs:check + build) passes locally
  • New unit tests in whole-day-date.test.ts cover UTC, +09:00, -07:00, month-end (Jan 31, Sep 30), and leap day (Feb 29) inputs
  • New regression tests in create-event.test.ts / update-event.test.ts reproduce the exact scenario from the issue (Asia/Seoul +09:00, including recurrenceRule.until) and assert the stored date is unchanged
  • Added a whole-day event case to scripts/smoke.ts (create → list → delete with a +09:00 input) so the fix is also verified end-to-end against a real CalDAV server, since the unit tests mock the client and don't exercise ts-caldav's actual serialization or a real server's storage/parsing. Not run here (no live CalDAV server in this environment) — needs npm run smoke against a real server to confirm.

Fixes #89

claude added 2 commits July 29, 2026 19:19
new Date(iso) preserves the input's UTC instant, not its local calendar
date. ts-caldav then derives the stored VALUE=DATE via
date.toISOString().split("T")[0], which truncates any offset east of
UTC back by one day. Anchor whole-day start/end/until at UTC midnight
of the input's calendar date instead, so the truncation is a no-op.

Fixes #89
Unit tests mock the CalDAV client, so they stop short of the boundary
where the #89 bug actually lived: ts-caldav's serialization and a real
server's storage/parsing of VALUE=DATE. Extend scripts/smoke.ts with a
create -> list -> delete round trip using a non-UTC-offset (+09:00)
whole-day event, asserting the listed date matches the input calendar
date rather than shifting a day earlier.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] wholeDay events are stored one day early for timezones ahead of UTC (UTC date truncation after Date conversion)

2 participants