events: southern-hemisphere DST (+8) — 384/384
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 37s

The :dst zone model assumed northern ordering (dst-start < dst-end, DST =
[start, end)). Southern zones — DST begins ~Oct and ends ~Apr — have
dst-start > dst-end, so the old (>= start AND < end) test was never true and
ev-tz-offset returned the standard offset year-round.

Fix: detect the ordering. start < end → DST is [start, end); start > end →
DST wraps the calendar-year boundary, active when (utc >= start OR utc < end).

Add predefined ev-tz-sydney (AEST +600 / AEDT +660; transitions 02:00 AEST
first-Sun-Oct and 03:00 AEDT first-Sun-Apr, both 16:00 UTC the preceding
Saturday → rule time -480). VTIMEZONE export is already rule-agnostic, so
southern zones round-trip through iCal unchanged (the -480 folds the
from-offset back to the correct local 02:00/03:00 DTSTART).

+8 timezone tests (now 25): summer/winter offsets, both transition dates,
local->utc in both seasons, and a daily expansion crossing the autumn DST-end
that shifts in UTC (1320,1320,1380,1380,1380) while staying 09:00 local.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 20:59:59 +00:00
parent 826d926740
commit 78b45a331e
3 changed files with 97 additions and 6 deletions

View File

@@ -76,6 +76,58 @@
ev-tz-paris
(ev-dt 2026 7 15 12 0))
120)
;; ---- southern hemisphere (reversed seasons) ----
(ev-tz-check!
"Sydney January offset is 660 (AEDT, summer DST)"
(ev-tz-offset
ev-tz-sydney
(ev-dt 2026 1 15 12 0))
660)
(ev-tz-check!
"Sydney July offset is 600 (AEST, winter std)"
(ev-tz-offset
ev-tz-sydney
(ev-dt 2026 7 15 12 0))
600)
(ev-tz-check!
"Sydney DST starts first Sunday of October"
(ev-dt->civil
(+ (ev-tz-transition 2026 (get ev-tz-sydney :dst-start)) 480))
(list 2026 10 4))
(ev-tz-check!
"Sydney DST ends first Sunday of April"
(ev-dt->civil
(+ (ev-tz-transition 2026 (get ev-tz-sydney :dst-end)) 480))
(list 2026 4 5))
(ev-tz-check!
"09:00 Sydney in summer (AEDT) is previous-day 22:00 UTC"
(ev-tz-local->utc
ev-tz-sydney
(ev-dt 2026 1 15 9 0))
(ev-dt 2026 1 14 22 0))
(ev-tz-check!
"09:00 Sydney in winter (AEST) is previous-day 23:00 UTC"
(ev-tz-local->utc
ev-tz-sydney
(ev-dt 2026 7 15 9 0))
(ev-dt 2026 7 14 23 0))
(let
((au (ev-event-tz (quote au) (ev-dt 2026 4 3 9 0) 60 {:freq :daily :count 5} 8 ev-tz-sydney)))
(let
((occs (ev-expand au (ev-date 2026 3 25) (ev-date 2026 4 12))))
(do
(ev-tz-check!
"Sydney daily occurrences shift in UTC across the autumn DST end"
(map (fn (o) (ev-dt-tod (get o :start))) occs)
(list 1320 1320 1380 1380 1380))
(ev-tz-check!
"but every Sydney occurrence stays 09:00 local wall-clock"
(map
(fn
(o)
(first (rest (ev-tz-local-of ev-tz-sydney (get o :start)))))
occs)
(list 540 540 540 540 540)))))
(ev-tz-check!
"DST starts last Sunday of March"
(ev-dt->civil

View File

@@ -13,8 +13,11 @@
;; :fixed — a constant offset.
;; :dst — std/dst offsets + two transition rules. Transitions are given in
;; UTC (EU zones all switch at 01:00 UTC), so the offset at any UTC
;; instant is a direct range check; no recursion. Northern-hemisphere
;; ordering (dst-start < dst-end within a year) is assumed.
;; instant is a direct range check; no recursion. Both hemispheres
;; are supported: northern zones have dst-start < dst-end (DST is the
;; interval [start, end)); southern zones have dst-start > dst-end
;; (DST wraps the year boundary), detected by comparing the two
;; transitions — see ev-tz-offset.
;;
;; Requires calendar.sx (ev-dt, ev-days-from-civil, ev-civil-from-days,
;; ev-civ-y, ev-floor-div, ev-resolve-nth-weekday).
@@ -58,10 +61,20 @@
(let
((start (ev-tz-transition year (get tz :dst-start)))
(end (ev-tz-transition year (get tz :dst-end))))
;; Northern hemisphere: dst-start < dst-end, DST is the closed-open
;; interval [start, end). Southern hemisphere: dst-start > dst-end
;; (DST begins in spring ~Oct and ends ~Apr), so within a calendar
;; year DST wraps the boundary — active OUTSIDE [end, start).
(if
(and (>= utc-dt start) (< utc-dt end))
(get tz :dst-offset)
(get tz :std-offset)))))
(< start end)
(if
(and (>= utc-dt start) (< utc-dt end))
(get tz :dst-offset)
(get tz :std-offset))
(if
(or (>= utc-dt start) (< utc-dt end))
(get tz :dst-offset)
(get tz :std-offset))))))
(else 0))))
;; UTC instant -> local wall-clock.
@@ -98,6 +111,19 @@
120
(ev-tz-rule 3 -1 6 60)
(ev-tz-rule 10 -1 6 60)))
;; Southern hemisphere: AEST +600 (std, winter), AEDT +660 (dst, summer). DST
;; begins 02:00 AEST first Sunday October and ends 03:00 AEDT first Sunday April
;; — both 16:00 UTC the preceding Saturday, i.e. -480 minutes from the Sunday in
;; the rule (the model adds rule :time to the resolved weekday's UTC midnight).
;; dst-start (Oct) > dst-end (Apr), so ev-tz-offset takes the wrap-the-year path.
(define
ev-tz-sydney
(ev-tz-dst
"Australia/Sydney"
600
660
(ev-tz-rule 10 1 6 -480)
(ev-tz-rule 4 1 6 -480)))
;; ---- tz-aware event expansion ----