125 lines
3.1 KiB
Go
125 lines
3.1 KiB
Go
package schedule
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"g.fc.run/ai/byre/internal/api"
|
|
)
|
|
|
|
var ErrInvalid = errors.New("invalid schedule")
|
|
|
|
var Weekdays = []api.Weekday{api.Mon, api.Tue, api.Wed, api.Thu, api.Fri, api.Sat, api.Sun}
|
|
|
|
var goWeekdays = map[api.Weekday]time.Weekday{
|
|
api.Mon: time.Monday,
|
|
api.Tue: time.Tuesday,
|
|
api.Wed: time.Wednesday,
|
|
api.Thu: time.Thursday,
|
|
api.Fri: time.Friday,
|
|
api.Sat: time.Saturday,
|
|
api.Sun: time.Sunday,
|
|
}
|
|
|
|
func invalid(format string, args ...any) error {
|
|
return fmt.Errorf("%w: %s", ErrInvalid, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
func Timing(save api.ScheduleSave) (api.Schedule, error) {
|
|
sc := api.Schedule{
|
|
Title: strings.TrimSpace(save.Title),
|
|
Every: save.Every,
|
|
Weekdays: []api.Weekday{},
|
|
Hour: save.Hour,
|
|
Minute: save.Minute,
|
|
}
|
|
if sc.Title == "" {
|
|
return sc, invalid("title is required")
|
|
}
|
|
if save.Minute < 0 || save.Minute > 59 {
|
|
return sc, invalid("minute must be 0 to 59")
|
|
}
|
|
if save.Hour < 0 || save.Hour > 23 {
|
|
return sc, invalid("hour must be 0 to 23")
|
|
}
|
|
switch save.Every {
|
|
case api.Hour, api.Day, api.Week, api.Month:
|
|
default:
|
|
return sc, invalid("every must be hour, day, week or month")
|
|
}
|
|
if save.Every == api.Hour && save.Hour != 0 {
|
|
return sc, invalid("an hourly schedule runs at a minute past every hour, so hour must be 0")
|
|
}
|
|
if (save.Weekdays != nil && len(*save.Weekdays) > 0) != (save.Every == api.Week) {
|
|
return sc, invalid("weekdays are required for a weekly schedule, and only allowed for one")
|
|
}
|
|
if save.Weekdays != nil {
|
|
for _, d := range Weekdays {
|
|
if slices.Contains(*save.Weekdays, d) {
|
|
sc.Weekdays = append(sc.Weekdays, d)
|
|
}
|
|
}
|
|
for _, d := range *save.Weekdays {
|
|
if _, found := goWeekdays[d]; !found {
|
|
return sc, invalid("unknown weekday %q", d)
|
|
}
|
|
}
|
|
}
|
|
if (save.DayOfMonth != nil) != (save.Every == api.Month) {
|
|
return sc, invalid("day_of_month is required for a monthly schedule, and only allowed for one")
|
|
}
|
|
if save.DayOfMonth != nil {
|
|
if *save.DayOfMonth < 1 || *save.DayOfMonth > 31 {
|
|
return sc, invalid("day_of_month must be 1 to 31")
|
|
}
|
|
sc.DayOfMonth = *save.DayOfMonth
|
|
}
|
|
return sc, nil
|
|
}
|
|
|
|
func Next(sc api.Schedule, after time.Time) time.Time {
|
|
t := after.Local()
|
|
y, m, d := t.Date()
|
|
at := func(y int, m time.Month, d, h int) time.Time {
|
|
return time.Date(y, m, d, h, sc.Minute, 0, 0, time.Local)
|
|
}
|
|
switch sc.Every {
|
|
case api.Hour:
|
|
c := at(y, m, d, t.Hour())
|
|
for !c.After(t) {
|
|
c = c.Add(time.Hour)
|
|
}
|
|
return c
|
|
case api.Day:
|
|
c := at(y, m, d, sc.Hour)
|
|
if !c.After(t) {
|
|
c = at(y, m, d+1, sc.Hour)
|
|
}
|
|
return c
|
|
case api.Week:
|
|
days := map[time.Weekday]bool{}
|
|
for _, w := range sc.Weekdays {
|
|
days[goWeekdays[w]] = true
|
|
}
|
|
for i := 0; i <= 7; i++ {
|
|
c := at(y, m, d+i, sc.Hour)
|
|
if days[c.Weekday()] && c.After(t) {
|
|
return c
|
|
}
|
|
}
|
|
case api.Month:
|
|
for i := 0; i <= 1; i++ {
|
|
first := time.Date(y, m+time.Month(i), 1, 0, 0, 0, 0, time.Local)
|
|
last := first.AddDate(0, 1, -1).Day()
|
|
c := at(first.Year(), first.Month(), min(sc.DayOfMonth, last), sc.Hour)
|
|
if c.After(t) {
|
|
return c
|
|
}
|
|
}
|
|
}
|
|
panic(fmt.Sprintf("schedule %s has no next run after %s", sc.Id, after))
|
|
}
|