oficina / calendar Public

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

Plain git — no account needed to clone.

Download

Download this file

1package calendar
2
3import (
4 "testing"
5 "time"
6)
7
8func TestLargeRepresentableWeeklyIntervalIsExact(t *testing.T) {
9 proto := time.Date(2026, 9, 7, 9, 0, 0, 0, time.UTC)
10 r, _ := ParseRule("FREQ=WEEKLY;INTERVAL=400000;BYDAY=MO")
11 next, state := advance(proto, proto, r, time.UTC)
12 want := time.Date(9692, 10, 27, 9, 0, 0, 0, time.UTC)
13 if state != stepCandidate || !next.Equal(want) || next.Weekday() != time.Monday {
14 t.Fatalf("next=%v state=%v want=%v", next, state, want)
15 }
16}
17
18func TestHugeIntervalsRefuseWithoutOverflow(t *testing.T) {
19 proto := time.Date(2026, 9, 7, 9, 0, 0, 0, time.UTC)
20 for _, freq := range []string{FreqDaily, FreqWeekly, FreqMonthly, FreqYearly} {
21 for _, interval := range []int{int(^uint(0) >> 1), int(^uint(0)>>1) / 2} {
22 next, state := advance(proto, proto, Rule{Freq: freq, Interval: interval}, time.UTC)
23 if state != stepIncomplete {
24 t.Errorf("%s interval%d next%v state%v", freq, interval, next, state)
25 }
26 }
27 }
28}
29
30func TestHorizonAllowsItsLastDayAndRefusesTheNext(t *testing.T) {
31 proto := time.Date(9999, 12, 30, 9, 0, 0, 0, time.UTC)
32 r, _ := ParseRule("FREQ=DAILY")
33 next, state := advance(proto, proto, r, time.UTC)
34 if state != stepCandidate || next.Format(LayoutLocal) != "9999-12-31T09:00" {
35 t.Fatalf("last day=%v state=%v", next, state)
36 }
37 if _, state = advance(next, proto, r, time.UTC); state != stepIncomplete {
38 t.Fatal("accepted date beyond four-digit record horizon")
39 }
40 weekly, _ := ParseRule("FREQ=WEEKLY;BYDAY=SA")
41 if _, state = advance(next, next, weekly, time.UTC); state != stepIncomplete {
42 t.Fatal("accepted weekly day beyond horizon")
43 }
44}
45
46func TestCountCompletionDoesNotRequireAnotherRepresentableStep(t *testing.T) {
47 from := time.Date(9999, 12, 31, 0, 0, 0, 0, time.UTC).Unix()
48 to := time.Date(10000, 1, 2, 0, 0, 0, 0, time.UTC).Unix()
49 r, _ := ParseRule("FREQ=YEARLY;INTERVAL=99999999;COUNT=1")
50 got, complete := ExpandComplete("9999-12-31T09:00", "9999-12-31T10:00", "UTC", false, r, "", from, to)
51 if len(got) != 1 || !complete {
52 t.Fatalf("completed COUNT=%v complete=%v", got, complete)
53 }
54 r.Count = 0
55 if _, complete = ExpandComplete("9999-12-31T09:00", "9999-12-31T10:00", "Pacific/Kiritimati", false, r, "", from-86400, to); complete {
56 t.Fatal("horizon is not proof of completion, including zones east of UTC")
57 }
58}
59
60func TestCalendarExhaustionDoesNotUseZoneNormalization(t *testing.T) {
61 proto := time.Date(2026, 2, 1, 9, 0, 0, 0, time.UTC)
62 r, _ := ParseRule("FREQ=MONTHLY;INTERVAL=12;BYMONTHDAY=31")
63 if next, state := advance(proto, proto, r, time.UTC); state != stepExhausted {
64 t.Fatalf("impossible February31=%v state=%v", next, state)
65 }
66 for _, ordinal := range []int{int(^uint(0) >> 1), -int(^uint(0) >> 1)} {
67 r := Rule{Freq: FreqMonthly, Interval: 1, Ordinal: ordinal, OrdinalDay: time.Monday}
68 if _, state := advance(proto, proto, r, time.UTC); state != stepExhausted {
69 t.Fatalf("impossible ordinal%d state%v", ordinal, state)
70 }
71 }
72 leap := time.Date(2000, 2, 29, 9, 0, 0, 0, time.UTC)
73 r, _ = ParseRule("FREQ=YEARLY;INTERVAL=100")
74 if next, state := advance(leap, leap, r, time.UTC); state != stepCandidate || next.Year() != 2400 || next.Day() != 29 {
75 t.Fatalf("century leap skip=%v state=%v", next, state)
76 }
77}
78
79func TestInvalidInputsCannotBecomeCompleteAtCountOrWindow(t *testing.T) {
80 from := time.Date(2026, 9, 7, 0, 0, 0, 0, time.UTC).Unix()
81 for _, rule := range []Rule{{Freq: FreqDaily, Interval: 0}, {Freq: "HOURLY", Interval: 1}, {Freq: FreqWeekly, Interval: 1, ByDay: []time.Weekday{9}}, {Freq: FreqMonthly, Interval: 1, MonthDay: 32}} {
82 if _, complete := ExpandComplete("2026-09-07T09:00", "2026-09-07T10:00", "UTC", false, rule, "", from, from+86400); complete {
83 t.Errorf("invalid direct rule complete: %+v", rule)
84 }
85 }
86 r, _ := ParseRule("FREQ=DAILY;COUNT=1")
87 for _, end := range []string{"invalid", "2026-09-07T08:00"} {
88 if _, complete := ExpandComplete("2026-09-07T09:00", end, "UTC", false, r, "", from, from+86400); complete {
89 t.Errorf("invalid end complete: %s", end)
90 }
91 }
92 if _, complete := ExpandComplete("2026-09-07T09:00", "2026-09-07T10:00", "UTC", false, r, "", from+86400, from); complete {
93 t.Fatal("reversed window complete")
94 }
95}
96
97func TestGapOccurrenceDoesNotTruncateLaterWindows(t *testing.T) {
98 r, _ := ParseRule("FREQ=DAILY")
99 from := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC).Unix()
100 got, complete := ExpandComplete("2026-03-01T01:30", "2026-03-01T02:00", "Europe/Dublin", false, r, "", from, from+86400)
101 if len(got) != 1 || !complete {
102 t.Fatalf("April window after spring gap: got=%v complete=%v", got, complete)
103 }
104 if got[0].Local != "2026-04-01T01:30" || got[0].EndUTC-got[0].StartUTC != 1800 {
105 t.Fatalf("April occurrence=%+v", got[0])
106 }
107}
108