Compare commits

...

4 Commits

Author SHA1 Message Date
7d1d732623 events: sync scoreboard.md to 391/391
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 36s
Companion to b2578139 — the markdown scoreboard render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 23:05:53 +00:00
b257813926 events: sync scoreboard to 391/391
Some checks are pending
Test, Build, and Deploy / test-build-deploy (push) Has started running
Scoreboard artifact for the southern-hemisphere DST commits (78b45a33,
6716af69) — timezone 17->25, ical 56->63, total 376->391. Was left out of
those commits; committing now to keep the tracked scoreboard in sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 23:05:35 +00:00
6716af69dc events: iCal coverage for southern-hemisphere VTIMEZONE (+7) — 391/391
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 25s
The previous commit asserted southern zones round-trip through iCal unchanged
but verified it only by reasoning. Close that gap with explicit tests:

- A Sydney VTIMEZONE export block: TZID:Australia/Sydney, DAYLIGHT->+1100
  (AEDT) / STANDARD->+1000 (AEST), first-Sunday rules (BYMONTH=10/4 BYDAY=1SU),
  and DAYLIGHT DTSTART:19701004T020000 — confirming the -480 rule time folds
  the from-offset back to the correct local 02:00 AEST transition.
- A southern-zone DTSTART;TZID export -> import round-trip preserving :dtstart.

+7 ical tests (now 63).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 22:04:04 +00:00
78b45a331e 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>
2026-06-10 20:59:59 +00:00
6 changed files with 130 additions and 15 deletions

View File

@@ -1,12 +1,12 @@
{ {
"lang": "events", "lang": "events",
"total_passed": 376, "total_passed": 391,
"total_failed": 0, "total_failed": 0,
"total": 376, "total": 391,
"suites": [ "suites": [
{"name":"calendar","passed":51,"failed":0,"total":51}, {"name":"calendar","passed":51,"failed":0,"total":51},
{"name":"timezone","passed":17,"failed":0,"total":17}, {"name":"timezone","passed":25,"failed":0,"total":25},
{"name":"ical","passed":56,"failed":0,"total":56}, {"name":"ical","passed":63,"failed":0,"total":63},
{"name":"availability","passed":22,"failed":0,"total":22}, {"name":"availability","passed":22,"failed":0,"total":22},
{"name":"api","passed":41,"failed":0,"total":41}, {"name":"api","passed":41,"failed":0,"total":41},
{"name":"booking","passed":82,"failed":0,"total":82}, {"name":"booking","passed":82,"failed":0,"total":82},
@@ -17,5 +17,5 @@
{"name":"federation","passed":29,"failed":0,"total":29}, {"name":"federation","passed":29,"failed":0,"total":29},
{"name":"integration","passed":8,"failed":0,"total":8} {"name":"integration","passed":8,"failed":0,"total":8}
], ],
"generated": "2026-06-07T20:02:48+00:00" "generated": "2026-06-10T22:03:34+00:00"
} }

View File

@@ -1,12 +1,12 @@
# events scoreboard # events scoreboard
**376 / 376 passing** (0 failure(s)). **391 / 391 passing** (0 failure(s)).
| Suite | Passed | Total | Status | | Suite | Passed | Total | Status |
|-------|--------|-------|--------| |-------|--------|-------|--------|
| calendar | 51 | 51 | ok | | calendar | 51 | 51 | ok |
| timezone | 17 | 17 | ok | | timezone | 25 | 25 | ok |
| ical | 56 | 56 | ok | | ical | 63 | 63 | ok |
| availability | 22 | 22 | ok | | availability | 22 | 22 | ok |
| api | 41 | 41 | ok | | api | 41 | 41 | ok |
| booking | 82 | 82 | ok | | booking | 82 | 82 | ok |

View File

@@ -335,6 +335,17 @@
(do (do
(ev-ic-check! "Paris DAYLIGHT goes to +0200 (CEST)" (ev-ic-find vtz "TZOFFSETTO:+0200") "TZOFFSETTO:+0200") (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"))) (ev-ic-check! "Paris STANDARD goes to +0100 (CET)" (ev-ic-find vtz "TZOFFSETTO:+0100") "TZOFFSETTO:+0100")))
;; southern hemisphere exports a valid VTIMEZONE too: reversed offsets,
;; first-Sunday rules, and the -480 rule time folds back to local 02:00/03:00
(let
((vtz (ev-ical-vtimezone ev-tz-sydney)))
(do
(ev-ic-check! "Sydney VTIMEZONE names the zone" (ev-ic-find vtz "TZID") "TZID:Australia/Sydney")
(ev-ic-check! "Sydney DAYLIGHT goes to +1100 (AEDT)" (ev-ic-find vtz "TZOFFSETTO:+1100") "TZOFFSETTO:+1100")
(ev-ic-check! "Sydney STANDARD goes to +1000 (AEST)" (ev-ic-find vtz "TZOFFSETTO:+1000") "TZOFFSETTO:+1000")
(ev-ic-check! "Sydney DAYLIGHT rule is first Sunday of October" (ev-ic-find vtz "RRULE:FREQ=YEARLY;BYMONTH=10") "RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=1SU")
(ev-ic-check! "Sydney STANDARD rule is first Sunday of April" (ev-ic-find vtz "RRULE:FREQ=YEARLY;BYMONTH=4") "RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU")
(ev-ic-check! "Sydney DAYLIGHT begins 02:00 local (AEST std, -480 folded)" (ev-ic-find vtz "DTSTART") "DTSTART:19701004T020000")))
;; ---- VCALENDAR carries one VTIMEZONE per distinct zone ---- ;; ---- VCALENDAR carries one VTIMEZONE per distinct zone ----
(let (let
@@ -371,7 +382,13 @@
(get (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))) (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) :dtstart)
(ev-dt 2026 7 15 18 0))))) (ev-dt 2026 7 15 18 0))
(ev-ic-check!
"import parses a southern-zone DTSTART;TZID local time"
(get
(ev/ical-lines->event (ev/event->ical-lines (ev-event-tz (quote a) (ev-dt 2026 1 15 18 0) 60 nil 1 ev-tz-sydney)))
:dtstart)
(ev-dt 2026 1 15 18 0)))))
(define (define
ev-ical-tests-run! ev-ical-tests-run!

View File

@@ -76,6 +76,58 @@
ev-tz-paris ev-tz-paris
(ev-dt 2026 7 15 12 0)) (ev-dt 2026 7 15 12 0))
120) 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! (ev-tz-check!
"DST starts last Sunday of March" "DST starts last Sunday of March"
(ev-dt->civil (ev-dt->civil

View File

@@ -13,8 +13,11 @@
;; :fixed — a constant offset. ;; :fixed — a constant offset.
;; :dst — std/dst offsets + two transition rules. Transitions are given in ;; :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 ;; 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 ;; instant is a direct range check; no recursion. Both hemispheres
;; ordering (dst-start < dst-end within a year) is assumed. ;; 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, ;; Requires calendar.sx (ev-dt, ev-days-from-civil, ev-civil-from-days,
;; ev-civ-y, ev-floor-div, ev-resolve-nth-weekday). ;; ev-civ-y, ev-floor-div, ev-resolve-nth-weekday).
@@ -58,10 +61,20 @@
(let (let
((start (ev-tz-transition year (get tz :dst-start))) ((start (ev-tz-transition year (get tz :dst-start)))
(end (ev-tz-transition year (get tz :dst-end)))) (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 (if
(and (>= utc-dt start) (< utc-dt end)) (< start end)
(get tz :dst-offset) (if
(get tz :std-offset))))) (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)))) (else 0))))
;; UTC instant -> local wall-clock. ;; UTC instant -> local wall-clock.
@@ -98,6 +111,19 @@
120 120
(ev-tz-rule 3 -1 6 60) (ev-tz-rule 3 -1 6 60)
(ev-tz-rule 10 -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 ---- ;; ---- tz-aware event expansion ----

View File

@@ -18,7 +18,7 @@ capacity rules, transactional booking, and a flow-driven notification dispatcher
## Status (rolling) ## Status (rolling)
`bash lib/events/conformance.sh`**376/376** (Phases 1-4 + 13 ext + tz iCal export via TZID + VTIMEZONE) `bash lib/events/conformance.sh`**391/391** (Phases 1-4 + 14 ext + tz iCal export via TZID + VTIMEZONE + southern-hemisphere DST incl. iCal round-trip)
## Ground rules ## Ground rules
@@ -88,6 +88,26 @@ lib/events/api.sx ── (events/schedule) (events/book) (events/agenda) ──
## Progress log ## Progress log
- 2026-06-10 — Harden southern-hemisphere DST: explicit iCal coverage for the
previous commit's unverified claim that "southern zones round-trip through
iCal unchanged". Added a Sydney VTIMEZONE export block (TZID:Australia/Sydney,
DAYLIGHT→+1100/STANDARD→+1000, first-Sunday rules BYMONTH=10/4 BYDAY=1SU, and
DAYLIGHT DTSTART:19701004T020000 — proving the 480 rule time folds back to
local 02:00 AEST) and a southern-zone DTSTART;TZID export→import round-trip.
+7 tests (ical 63). 391/391 green.
- 2026-06-10 — Southern-hemisphere DST. The `:dst` zone model assumed northern
ordering (dst-start < dst-end, DST = [start, end)); southern zones (DST begins
~Oct, ends ~Apr) have dst-start > dst-end and so silently never entered DST —
`ev-tz-offset` returned std year-round. Fixed by detecting the ordering: when
start < end DST is the interval [start, end); when start > end DST wraps the
year boundary (active when `utc ≥ start OR utc < end`). Added 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 prior Saturday → rule time
480). VTIMEZONE export already rule-agnostic, so southern zones round-trip
too (the 480 folds the from-offset back to the correct local 02:00/03:00).
+8 tests (timezone 25): summer/winter offsets, both transition dates,
local→utc both seasons, and a daily expansion crossing the autumn DST-end that
shifts in UTC (1320·1320·1380·1380·1380) yet stays 09:00 local. 384/384 green.
- 2026-06-07 — VTIMEZONE iCal export (supersedes the UTC-Z tz fix — full DST - 2026-06-07 — VTIMEZONE iCal export (supersedes the UTC-Z tz fix — full DST
fidelity). A tz event now exports DTSTART;TZID=<name>:<local> (+ EXDATE/RDATE fidelity). A tz event now exports DTSTART;TZID=<name>:<local> (+ EXDATE/RDATE
in the same TZID-local form; UNTIL stays UTC per RFC), and the VCALENDAR emits in the same TZID-local form; UNTIL stays UTC per RFC), and the VCALENDAR emits