aboutsummaryrefslogtreecommitdiff
path: root/internal/calendar/datespec.go
blob: c04bc14e96cdab2aca08e09c7697410f4cd9d34f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package calendar

import (
	"strconv"
	"strings"
	"time"
)

// ValidDate reports whether spec resolves to a date (validated against a sample
// year). Used by the `--cal-check` layer linter.
func ValidDate(spec DateSpec) bool {
	_, ok := resolveDate(spec, 2025, Easter(2025))
	return ok
}

// resolveDate returns the date (UTC midnight) that spec names in year, or
// ok=false if the spec is unparseable.
func resolveDate(spec DateSpec, year int, easter time.Time) (time.Time, bool) {
	s := strings.TrimSpace(string(spec))
	switch {
	case len(s) == 5 && s[2] == '-': // MM-DD
		mo, err1 := strconv.Atoi(s[0:2])
		da, err2 := strconv.Atoi(s[3:5])
		if err1 != nil || err2 != nil || mo < 1 || mo > 12 || da < 1 || da > 31 {
			return time.Time{}, false
		}
		return time.Date(year, time.Month(mo), da, 0, 0, 0, 0, time.UTC), true
	case strings.HasPrefix(s, "easter"):
		return offset(easter, s[len("easter"):])
	case strings.HasPrefix(s, "christmas"):
		xmas := time.Date(year, 12, 25, 0, 0, 0, 0, time.UTC)
		return offset(xmas, s[len("christmas"):])
	case strings.HasPrefix(s, "sunday-after "):
		base, ok := resolveDate(DateSpec(strings.TrimPrefix(s, "sunday-after ")), year, easter)
		if !ok {
			return time.Time{}, false
		}
		return nextWeekday(base.AddDate(0, 0, 1), time.Sunday), true
	}
	return time.Time{}, false
}

// offset parses "±N" and adds N days to base.
func offset(base time.Time, pm string) (time.Time, bool) {
	if pm == "" {
		return base, true
	}
	n, err := strconv.Atoi(pm) // strconv.Atoi handles a leading '+' and '-'
	if err != nil {
		return time.Time{}, false
	}
	return base.AddDate(0, 0, n), true
}

// nextWeekday returns the first day >= from whose weekday is wd.
func nextWeekday(from time.Time, wd time.Weekday) time.Time {
	delta := (int(wd) - int(from.Weekday()) + 7) % 7
	return from.AddDate(0, 0, delta)
}