events: VTIMEZONE iCal export — full DST-correct tz recurrence + 16 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m5s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m5s
A tz event now exports DTSTART;TZID=<name>:<local> (EXDATE/RDATE likewise; UNTIL stays UTC per RFC), and the VCALENDAR emits a VTIMEZONE per distinct zone with DAYLIGHT/STANDARD sub-components generated from the zone's transition rules (offsets + FREQ=YEARLY;BYMONTH;BYDAY) — London/Paris blocks match real-world definitions. Clients recur at fixed wall-clock time, DST-correct (prior caveat gone). Importer tolerates ;TZID= params. 376/376 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -266,47 +266,112 @@
|
||||
(ev-ic-starts (first events))
|
||||
(ev-ic-starts (ev-event (quote a) (ev-dt 2026 6 1 9 0) 30 {:freq :daily :count 3} 1)))))))))
|
||||
|
||||
;; ---- timezone-aware export (local wall-clock -> UTC stamps) ----
|
||||
;; ---- timezone-aware export (TZID + VTIMEZONE) ----
|
||||
(define
|
||||
ev-ic-dtstart
|
||||
(fn (ev) (ev-ic-line (ev/event->ical-lines ev) "DTSTART")))
|
||||
ev-ic-find
|
||||
(fn
|
||||
(lines pfx)
|
||||
(cond
|
||||
((empty? lines) nil)
|
||||
((ev-ic-prefix? (first lines) pfx) (first lines))
|
||||
(else (ev-ic-find (rest lines) pfx)))))
|
||||
|
||||
(define ev-ic-count (fn (lines x) (len (filter (fn (l) (= l x)) lines))))
|
||||
|
||||
(define
|
||||
ev-ic-index
|
||||
(fn
|
||||
(lines x)
|
||||
(cond
|
||||
((empty? lines) -1)
|
||||
((= (first lines) x) 0)
|
||||
(else
|
||||
(let ((r (ev-ic-index (rest lines) x))) (if (< r 0) -1 (+ 1 r)))))))
|
||||
|
||||
(define
|
||||
ev-ic-tz-run-all!
|
||||
(fn
|
||||
()
|
||||
(do
|
||||
;; a tz event's DTSTART is local wall-clock with a TZID parameter
|
||||
(ev-ic-check!
|
||||
"London winter event exports as the same UTC (GMT)"
|
||||
(ev-ic-dtstart (ev-event-tz (quote w) (ev-dt 2026 1 15 18 0) 60 nil 1 ev-tz-london))
|
||||
"DTSTART:20260115T180000Z")
|
||||
"tz event DTSTART uses TZID + local wall-clock (not UTC)"
|
||||
(ev-ic-find (ev/event->ical-lines (ev-event-tz (quote w) (ev-dt 2026 7 15 18 0) 60 nil 1 ev-tz-london)) "DTSTART")
|
||||
"DTSTART;TZID=Europe/London:20260715T180000")
|
||||
(ev-ic-check!
|
||||
"London summer event exports one hour earlier in UTC (BST)"
|
||||
(ev-ic-dtstart (ev-event-tz (quote s) (ev-dt 2026 7 15 18 0) 60 nil 1 ev-tz-london))
|
||||
"DTSTART:20260715T170000Z")
|
||||
(ev-ic-check!
|
||||
"Paris winter (CET +1) exports one hour earlier in UTC"
|
||||
(ev-ic-dtstart (ev-event-tz (quote p) (ev-dt 2026 1 15 18 0) 60 nil 1 ev-tz-paris))
|
||||
"DTSTART:20260115T170000Z")
|
||||
(ev-ic-check!
|
||||
"Paris summer (CEST +2) exports two hours earlier in UTC"
|
||||
(ev-ic-dtstart (ev-event-tz (quote p) (ev-dt 2026 7 15 18 0) 60 nil 1 ev-tz-paris))
|
||||
"DTSTART:20260715T160000Z")
|
||||
(ev-ic-check!
|
||||
"a non-tz event is exported unchanged"
|
||||
(ev-ic-dtstart (ev-event (quote n) (ev-dt 2026 7 15 18 0) 60 nil 1))
|
||||
"a non-tz event still uses a UTC Z stamp"
|
||||
(ev-ic-find (ev/event->ical-lines (ev-event (quote n) (ev-dt 2026 7 15 18 0) 60 nil 1)) "DTSTART")
|
||||
"DTSTART:20260715T180000Z")
|
||||
;; EXDATE on a tz event is also converted to UTC
|
||||
;; UNTIL stays UTC even for a TZID event (RFC 5545)
|
||||
(ev-ic-check!
|
||||
"tz event EXDATE is converted to UTC"
|
||||
(ev-ic-line
|
||||
"tz event RRULE UNTIL is still UTC"
|
||||
(ev-ic-find
|
||||
(ev/event->ical-lines
|
||||
(ev-event-tz (quote s) (ev-dt 2026 6 1 18 0) 60 {:freq :weekly :byday (list 0) :until (ev-dt 2026 6 30 23 0)} 1 ev-tz-london))
|
||||
"RRULE")
|
||||
"RRULE:FREQ=WEEKLY;UNTIL=20260630T220000Z;BYDAY=MO")
|
||||
;; EXDATE matches the DTSTART form (TZID + local)
|
||||
(ev-ic-check!
|
||||
"tz event EXDATE uses TZID + local"
|
||||
(ev-ic-find
|
||||
(ev/event->ical-lines
|
||||
(assoc
|
||||
(ev-event-tz (quote s) (ev-dt 2026 7 1 18 0) 60 {:freq :daily :count 3} 1 ev-tz-london)
|
||||
:exdate
|
||||
(list (ev-dt 2026 7 2 18 0))))
|
||||
"EXDATE")
|
||||
"EXDATE:20260702T170000Z"))))
|
||||
"EXDATE;TZID=Europe/London:20260702T180000")
|
||||
|
||||
;; ---- VTIMEZONE block ----
|
||||
(let
|
||||
((vtz (ev-ical-vtimezone ev-tz-london)))
|
||||
(do
|
||||
(ev-ic-check! "VTIMEZONE names the zone" (ev-ic-find vtz "TZID") "TZID:Europe/London")
|
||||
(ev-ic-check! "DAYLIGHT transitions GMT->BST" (ev-ic-find vtz "TZOFFSETTO:+0100") "TZOFFSETTO:+0100")
|
||||
(ev-ic-check! "DAYLIGHT rule is last Sunday of March" (ev-ic-find vtz "RRULE:FREQ=YEARLY;BYMONTH=3") "RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU")
|
||||
(ev-ic-check! "STANDARD rule is last Sunday of October" (ev-ic-find vtz "RRULE:FREQ=YEARLY;BYMONTH=10") "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU")))
|
||||
(let
|
||||
((vtz (ev-ical-vtimezone ev-tz-paris)))
|
||||
(do
|
||||
(ev-ic-check! "Paris DAYLIGHT goes to +0200 (CEST)" (ev-ic-find vtz "TZOFFSETTO:+0200") "TZOFFSETTO:+0200")
|
||||
(ev-ic-check! "Paris STANDARD goes to +0100 (CET)" (ev-ic-find vtz "TZOFFSETTO:+0100") "TZOFFSETTO:+0100")))
|
||||
|
||||
;; ---- VCALENDAR carries one VTIMEZONE per distinct zone ----
|
||||
(let
|
||||
((cal (ev/events->ical-lines (list (ev-event-tz (quote a) (ev-dt 2026 6 1 9 0) 60 nil 1 ev-tz-london)))))
|
||||
(do
|
||||
(ev-ic-check! "VCALENDAR includes the referenced VTIMEZONE" (ev-ic-count cal "BEGIN:VTIMEZONE") 1)
|
||||
(ev-ic-check! "VTIMEZONE precedes the VEVENT" (< (ev-ic-index cal "BEGIN:VTIMEZONE") (ev-ic-index cal "BEGIN:VEVENT")) true)))
|
||||
(ev-ic-check!
|
||||
"two events in the same zone share one VTIMEZONE"
|
||||
(ev-ic-count
|
||||
(ev/events->ical-lines
|
||||
(list
|
||||
(ev-event-tz (quote a) (ev-dt 2026 6 1 9 0) 60 nil 1 ev-tz-london)
|
||||
(ev-event-tz (quote b) (ev-dt 2026 6 2 9 0) 60 nil 1 ev-tz-london)))
|
||||
"BEGIN:VTIMEZONE")
|
||||
1)
|
||||
(ev-ic-check!
|
||||
"events in two zones get two VTIMEZONEs"
|
||||
(ev-ic-count
|
||||
(ev/events->ical-lines
|
||||
(list
|
||||
(ev-event-tz (quote a) (ev-dt 2026 6 1 9 0) 60 nil 1 ev-tz-london)
|
||||
(ev-event-tz (quote b) (ev-dt 2026 6 2 9 0) 60 nil 1 ev-tz-paris)))
|
||||
"BEGIN:VTIMEZONE")
|
||||
2)
|
||||
(ev-ic-check!
|
||||
"a non-tz-only calendar has no VTIMEZONE"
|
||||
(ev-ic-count (ev/events->ical-lines (list (ev-event (quote a) (ev-dt 2026 6 1 9 0) 60 nil 1))) "BEGIN:VTIMEZONE")
|
||||
0)
|
||||
|
||||
;; ---- import tolerates the TZID parameter ----
|
||||
(ev-ic-check!
|
||||
"import parses DTSTART;TZID local time"
|
||||
(get
|
||||
(ev/ical-lines->event (ev/event->ical-lines (ev-event-tz (quote a) (ev-dt 2026 7 15 18 0) 60 nil 1 ev-tz-london)))
|
||||
:dtstart)
|
||||
(ev-dt 2026 7 15 18 0)))))
|
||||
|
||||
(define
|
||||
ev-ical-tests-run!
|
||||
|
||||
Reference in New Issue
Block a user