events: fix timezone-aware iCal export (local->UTC stamps) + 6 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 41s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 41s
Bug: tz events store wall-clock LOCAL times but export stamped them with a Z (UTC) suffix, so a London 18:00 event falsely read as 18:00 UTC. ev-ical-conv now converts a tz event's DTSTART/UNTIL/EXDATE/RDATE local->UTC before formatting (London summer 18:00 -> 170000Z; Paris -> 160000Z); non-tz events unchanged. Caveat: UTC RRULE drifts from wall-clock-stable tz recurrence across a DST boundary (VTIMEZONE deferred). 366/366 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,11 +94,27 @@
|
||||
(str (get e :ord) (ev-ical-wd (get e :wd)))
|
||||
(ev-ical-wd e))))
|
||||
|
||||
;; A datetime converter for an event: tz-aware events store wall-clock LOCAL
|
||||
;; times, so export converts them to UTC (the `Z` stamps are absolute);
|
||||
;; non-tz events pass through unchanged.
|
||||
;; CAVEAT: a UTC RRULE recurs at a fixed UTC offset, whereas a tz event's
|
||||
;; expansion stays wall-clock-stable across DST — so for a tz recurrence that
|
||||
;; crosses a DST boundary the exported series drifts by the offset change
|
||||
;; after the boundary. DTSTART and each individual stamp are correct; full
|
||||
;; fidelity would need a VTIMEZONE block (deferred).
|
||||
(define
|
||||
ev-ical-conv
|
||||
(fn
|
||||
(event)
|
||||
(let
|
||||
((tz (get event :tz)))
|
||||
(if (nil? tz) (fn (t) t) (fn (t) (ev-tz-local->utc tz t))))))
|
||||
|
||||
;; ---- RRULE ----
|
||||
(define
|
||||
ev-ical-rrule
|
||||
(fn
|
||||
(rrule)
|
||||
(rrule conv)
|
||||
(let
|
||||
((parts (list (str "FREQ=" (ev-ical-freq (get rrule :freq))))))
|
||||
(begin
|
||||
@@ -112,7 +128,7 @@
|
||||
(append! parts (str "COUNT=" (get rrule :count))))
|
||||
(when
|
||||
(not (nil? (get rrule :until)))
|
||||
(append! parts (str "UNTIL=" (ev-ical-dt (get rrule :until)))))
|
||||
(append! parts (str "UNTIL=" (ev-ical-dt (conv (get rrule :until))))))
|
||||
(when
|
||||
(not (nil? (get rrule :byday)))
|
||||
(append!
|
||||
@@ -139,17 +155,17 @@
|
||||
(fn
|
||||
(event)
|
||||
(let
|
||||
((lines (list "BEGIN:VEVENT")))
|
||||
((lines (list "BEGIN:VEVENT")) (conv (ev-ical-conv event)))
|
||||
(begin
|
||||
(append! lines (str "UID:" (get event :id)))
|
||||
(append! lines (str "SUMMARY:" (get event :id)))
|
||||
(append! lines (str "DTSTART:" (ev-ical-dt (get event :dtstart))))
|
||||
(append! lines (str "DTSTART:" (ev-ical-dt (conv (get event :dtstart)))))
|
||||
(append!
|
||||
lines
|
||||
(str "DURATION:" (ev-ical-duration (get event :duration))))
|
||||
(when
|
||||
(not (nil? (get event :rrule)))
|
||||
(append! lines (ev-ical-rrule (get event :rrule))))
|
||||
(append! lines (ev-ical-rrule (get event :rrule) conv)))
|
||||
(when
|
||||
(and
|
||||
(not (nil? (get event :exdate)))
|
||||
@@ -158,7 +174,7 @@
|
||||
lines
|
||||
(str
|
||||
"EXDATE:"
|
||||
(ev-ical-join (map ev-ical-dt (get event :exdate)) ","))))
|
||||
(ev-ical-join (map (fn (d) (ev-ical-dt (conv d))) (get event :exdate)) ","))))
|
||||
(when
|
||||
(and
|
||||
(not (nil? (get event :rdate)))
|
||||
@@ -167,7 +183,7 @@
|
||||
lines
|
||||
(str
|
||||
"RDATE:"
|
||||
(ev-ical-join (map ev-ical-dt (get event :rdate)) ","))))
|
||||
(ev-ical-join (map (fn (d) (ev-ical-dt (conv d))) (get event :rdate)) ","))))
|
||||
(append! lines "END:VEVENT")
|
||||
lines))))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user