events: timezone + DST support + 17 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
timezone.sx: wall-clock LOCAL <-> absolute UTC. :fixed + :dst zones (std/dst offsets + UTC transition rules, EU-style, no IANA DB) computed via calendar helpers. ev-event-tz authors in local time; ev-expand expands tz events in LOCAL time then converts each occurrence to UTC, so a 09:00 weekly meeting stays 09:00 across a DST change (UTC instant shifts). Predefined utc/london/ paris. Plain events unaffected. 295/295 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
173
lib/events/tests/timezone.sx
Normal file
173
lib/events/tests/timezone.sx
Normal file
@@ -0,0 +1,173 @@
|
||||
;; lib/events/tests/timezone.sx — timezones + DST.
|
||||
|
||||
(define ev-tz-pass 0)
|
||||
(define ev-tz-fail 0)
|
||||
(define ev-tz-failures (list))
|
||||
|
||||
(define
|
||||
ev-tz-check!
|
||||
(fn
|
||||
(name got expected)
|
||||
(if
|
||||
(= got expected)
|
||||
(set! ev-tz-pass (+ ev-tz-pass 1))
|
||||
(do
|
||||
(set! ev-tz-fail (+ ev-tz-fail 1))
|
||||
(append!
|
||||
ev-tz-failures
|
||||
(str name "\n expected: " expected "\n got: " got))))))
|
||||
|
||||
;; Wall-clock (civil + minute-of-day) an occurrence's UTC start maps to in a tz.
|
||||
(define
|
||||
ev-tz-local-of
|
||||
(fn
|
||||
(tz utc-dt)
|
||||
(let
|
||||
((l (ev-tz-utc->local tz utc-dt)))
|
||||
(list (ev-dt->civil l) (ev-dt-tod l)))))
|
||||
|
||||
(define
|
||||
ev-tz-run-all!
|
||||
(fn
|
||||
()
|
||||
(do
|
||||
(let
|
||||
((nyc (ev-tz-fixed "EST" -300)))
|
||||
(do
|
||||
(ev-tz-check!
|
||||
"fixed zone: utc -> local subtracts 5h"
|
||||
(ev-tz-utc->local
|
||||
nyc
|
||||
(ev-dt 2026 1 1 17 0))
|
||||
(ev-dt 2026 1 1 12 0))
|
||||
(ev-tz-check!
|
||||
"fixed zone: local -> utc adds 5h back"
|
||||
(ev-tz-local->utc
|
||||
nyc
|
||||
(ev-dt 2026 1 1 12 0))
|
||||
(ev-dt 2026 1 1 17 0))
|
||||
(ev-tz-check!
|
||||
"UTC zone is identity"
|
||||
(ev-tz-local->utc
|
||||
ev-tz-utc
|
||||
(ev-dt 2026 6 1 9 0))
|
||||
(ev-dt 2026 6 1 9 0))))
|
||||
(ev-tz-check!
|
||||
"London winter offset is 0 (GMT)"
|
||||
(ev-tz-offset
|
||||
ev-tz-london
|
||||
(ev-dt 2026 1 15 12 0))
|
||||
0)
|
||||
(ev-tz-check!
|
||||
"London summer offset is 60 (BST)"
|
||||
(ev-tz-offset
|
||||
ev-tz-london
|
||||
(ev-dt 2026 7 15 12 0))
|
||||
60)
|
||||
(ev-tz-check!
|
||||
"Paris winter offset is 60 (CET)"
|
||||
(ev-tz-offset
|
||||
ev-tz-paris
|
||||
(ev-dt 2026 1 15 12 0))
|
||||
60)
|
||||
(ev-tz-check!
|
||||
"Paris summer offset is 120 (CEST)"
|
||||
(ev-tz-offset
|
||||
ev-tz-paris
|
||||
(ev-dt 2026 7 15 12 0))
|
||||
120)
|
||||
(ev-tz-check!
|
||||
"DST starts last Sunday of March"
|
||||
(ev-dt->civil
|
||||
(ev-tz-transition
|
||||
2026
|
||||
(ev-tz-rule 3 -1 6 60)))
|
||||
(list 2026 3 29))
|
||||
(ev-tz-check!
|
||||
"DST ends last Sunday of October"
|
||||
(ev-dt->civil
|
||||
(ev-tz-transition
|
||||
2026
|
||||
(ev-tz-rule 10 -1 6 60)))
|
||||
(list 2026 10 25))
|
||||
(ev-tz-check!
|
||||
"09:00 London in winter is 09:00 UTC"
|
||||
(ev-tz-local->utc
|
||||
ev-tz-london
|
||||
(ev-dt 2026 1 15 9 0))
|
||||
(ev-dt 2026 1 15 9 0))
|
||||
(ev-tz-check!
|
||||
"09:00 London in summer is 08:00 UTC"
|
||||
(ev-tz-local->utc
|
||||
ev-tz-london
|
||||
(ev-dt 2026 7 15 9 0))
|
||||
(ev-dt 2026 7 15 8 0))
|
||||
(ev-tz-check!
|
||||
"round trip utc -> local -> utc"
|
||||
(ev-tz-local->utc
|
||||
ev-tz-london
|
||||
(ev-tz-utc->local
|
||||
ev-tz-london
|
||||
(ev-dt 2026 7 15 8 0)))
|
||||
(ev-dt 2026 7 15 8 0))
|
||||
(let
|
||||
((ev (ev-event-tz (quote standup) (ev-dt 2026 3 27 9 0) 60 {:freq :daily :count 5} 10 ev-tz-london)))
|
||||
(let
|
||||
((occs (ev-expand ev (ev-date 2026 3 1) (ev-date 2026 4 5))))
|
||||
(do
|
||||
(ev-tz-check!
|
||||
"daily occurrences shift in UTC across the DST boundary"
|
||||
(map (fn (o) (ev-dt-tod (get o :start))) occs)
|
||||
(list 540 540 480 480 480))
|
||||
(ev-tz-check!
|
||||
"but every occurrence stays 09:00 local wall-clock"
|
||||
(map
|
||||
(fn
|
||||
(o)
|
||||
(first
|
||||
(rest (ev-tz-local-of ev-tz-london (get o :start)))))
|
||||
occs)
|
||||
(list 540 540 540 540 540))
|
||||
(ev-tz-check!
|
||||
"occurrence dates are stable in local time"
|
||||
(map
|
||||
(fn
|
||||
(o)
|
||||
(ev-civ-d
|
||||
(first (ev-tz-local-of ev-tz-london (get o :start)))))
|
||||
occs)
|
||||
(list 27 28 29 30 31)))))
|
||||
(let
|
||||
((wk (ev-event-tz (quote class) (ev-dt 2026 3 23 18 0) 90 {:freq :weekly :byday (list 0)} 5 ev-tz-london)))
|
||||
(let
|
||||
((occs (ev-expand wk (ev-date 2026 3 1) (ev-date 2026 4 20))))
|
||||
(ev-tz-check!
|
||||
"weekly Monday 18:00 London stays 18:00 local each week"
|
||||
(map
|
||||
(fn
|
||||
(o)
|
||||
(first (rest (ev-tz-local-of ev-tz-london (get o :start)))))
|
||||
occs)
|
||||
(list 1080 1080 1080 1080))))
|
||||
(let
|
||||
((plain (ev-event (quote p) (ev-dt 2026 3 27 9 0) 60 {:freq :daily :count 3} 1)))
|
||||
(ev-tz-check!
|
||||
"plain event expands naively (no UTC shift)"
|
||||
(map
|
||||
(fn (o) (ev-dt-tod (get o :start)))
|
||||
(ev-expand
|
||||
plain
|
||||
(ev-date 2026 3 1)
|
||||
(ev-date 2026 4 5)))
|
||||
(list 540 540 540))))))
|
||||
|
||||
(define
|
||||
ev-timezone-tests-run!
|
||||
(fn
|
||||
()
|
||||
(do
|
||||
(set! ev-tz-pass 0)
|
||||
(set! ev-tz-fail 0)
|
||||
(set! ev-tz-failures (list))
|
||||
(ev-tz-run-all!)
|
||||
{:failures ev-tz-failures :total (+ ev-tz-pass ev-tz-fail) :passed ev-tz-pass :failed ev-tz-fail})))
|
||||
Reference in New Issue
Block a user