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) }