oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1package calendartest
2
3import (
4 "testing"
5 "time"
6
7 calendar "amadan.net/oficina/calendar/internal/calendar"
8)
9
10// The two Sundays a year that break slot pages, with a 00:00–04:00
11// window laid straight across the transition. Europe/Dublin 2026: DST
12// starts 29 March and ends 25 October, both at 01:00 UTC.
13//
14// As in clock_test.go, this does not assert correctness — there is no
15// correct answer for an hour that does not exist or happens twice, and
16// Go promises none. It asserts B17's four-part check holds of every
17// slot that IS offered, and then pins THE LIST, so a Go or tzdata
18// change that moves it fails here rather than double-booking somebody.
19
20// dstSunday computes one Dublin Sunday's 00:00–04:00 hourly slots, from
21// a clock at the Saturday before.
22func dstSunday(t *testing.T, y int, mo time.Month, d int) []calendar.Slot {
23 t.Helper()
24 day := time.Date(y, mo, d, 0, 0, 0, 0, zoneOf(t, dublin))
25 rule := calendar.SlotRule{DurationMin: 60, HorizonDays: 7, Zone: dublin,
26 Windows: []calendar.Window{{Weekday: time.Sunday, StartMin: 0, EndMin: 4 * 60}}}
27 return compute(t, rule, nil, day.AddDate(0, 0, -1), day, day.AddDate(0, 0, 1))
28}
29
30// honest asserts B17 of every slot: nothing straddles the transition,
31// both ends round-trip through the stored form, and elapsed length is
32// exactly the duration.
33func honest(t *testing.T, slots []calendar.Slot, transitionUTC int64) {
34 t.Helper()
35 loc := zoneOf(t, dublin)
36 for _, s := range slots {
37 if s.StartUTC < transitionUTC && s.EndUTC > transitionUTC {
38 t.Errorf("slot %v straddles the transition at %d", s.Local, transitionUTC)
39 }
40 if s.EndUTC-s.StartUTC != 3600 {
41 t.Errorf("slot %v is %d elapsed seconds, want 3600", s.Local, s.EndUTC-s.StartUTC)
42 }
43 if s.Local.Unix() != s.StartUTC {
44 t.Errorf("slot Local %v is not its own StartUTC %d", s.Local, s.StartUTC)
45 }
46 for _, u := range []int64{s.StartUTC, s.EndUTC} {
47 back, err := calendar.ToUTC(calendar.FormatLocal(time.Unix(u, 0).In(loc), false), dublin, false)
48 if err != nil || back != u {
49 t.Errorf("slot %v: %d does not round-trip through its wall clock (got %d, %v)", s.Local, u, back, err)
50 }
51 }
52 }
53}
54
55func pinned(t *testing.T, slots []calendar.Slot, want ...int64) {
56 t.Helper()
57 got := make([]int64, 0, len(slots))
58 for _, s := range slots {
59 got = append(got, s.StartUTC)
60 }
61 if len(got) != len(want) {
62 t.Fatalf("starts = %v, want %v", got, want)
63 }
64 for i := range got {
65 if got[i] != want[i] {
66 t.Fatalf("starts = %v, want %v", got, want)
67 }
68 }
69}
70
71// Spring forward: 01:00 GMT jumps to 02:00 IST at 1774746000.
72//
73// What Go does, observed: time.Date resolves a wall time inside the gap
74// with the offset in force BEFORE the transition, so "01:00" lands at
75// 01:00 UTC and reads back as 02:00. Hence:
76//
77// - 00:00 (m=0) exists, but its END is the gap's "01:00", which reads
78// back as 02:00 — wall minute 120, not 60 — so it is refused.
79// - 01:00 (m=60) is itself in the gap and reads back as 02:00: refused.
80// - 02:00 IST = 1774746000 and 03:00 IST = 1774749600 are ordinary
81// hours on both clocks: offered.
82func TestSpringForwardSundayOffersOnlyHonestHours(t *testing.T) {
83 slots := dstSunday(t, 2026, time.March, 29)
84 honest(t, slots, 1774746000)
85 pinned(t, slots, 1774746000, 1774749600)
86}
87
88// Fall back: 02:00 IST returns to 01:00 GMT at 1792890000, so the wall
89// clock reads 01:xx twice.
90//
91// What Go does, observed: an ambiguous wall time resolves to the SECOND
92// occurrence (clock.go, TestDSTGapAndFold). Hence:
93//
94// - 00:00 IST (m=0, 1792882800) exists, and its end 1792886400 is the
95// FIRST 01:00 — but "01:00" round-trips to the second one, an hour
96// later, so the stored form would name a different instant: refused.
97// This is B17's "two-hour hour".
98// - 01:00 (m=60) resolves to the second 01:00, 01:00 GMT = 1792890000,
99// a plain hour to 02:00 GMT: offered.
100// - 02:00 GMT = 1792893600 and 03:00 GMT = 1792897200: offered.
101func TestFallBackSundayOffersOnlyHonestHours(t *testing.T) {
102 slots := dstSunday(t, 2026, time.October, 25)
103 honest(t, slots, 1792890000)
104 pinned(t, slots, 1792890000, 1792893600, 1792897200)
105}
106