150 lines
5.0 KiB
Go
150 lines
5.0 KiB
Go
package core
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func GetNextUpdate(lastUpdated time.Time, spec string) (nextUpdate time.Time, err error) {
|
|
var nextUpdates []time.Time
|
|
switch {
|
|
case strings.HasPrefix(spec, "every "):
|
|
nextUpdates, err = parseEverySpec(lastUpdated, spec[len("every "):])
|
|
case strings.HasPrefix(spec, "at "):
|
|
nextUpdates, err = parseAtSpec(lastUpdated, spec[len("at "):])
|
|
case strings.HasPrefix(spec, "on "):
|
|
nextUpdates, err = parseOnSpec(lastUpdated, spec[len("on "):])
|
|
default:
|
|
return time.Time{}, fmt.Errorf("unknown spec format: %v", spec)
|
|
}
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
for _, next := range nextUpdates {
|
|
if next.After(lastUpdated) && (nextUpdate.IsZero() || next.Before(nextUpdate)) {
|
|
nextUpdate = next
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get the next instance of the every-spec after the base time.
|
|
// An every-spec is a Go duration string.
|
|
func parseEverySpec(base time.Time, everySpec string) (nextUpdates []time.Time, err error) {
|
|
var duration time.Duration
|
|
duration, err = time.ParseDuration(everySpec)
|
|
if err == nil {
|
|
next := base.Round(duration)
|
|
if !next.After(base) {
|
|
next = next.Add(duration)
|
|
}
|
|
nextUpdates = []time.Time{next}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Get the next instances of the at-spec times after the base time.
|
|
// An at-spec is in the patterm HH:MM[,HH:MM,[...]].
|
|
func parseAtSpec(base time.Time, atSpec string) (nextUpdates []time.Time, err error) {
|
|
for _, timeSpec := range strings.Split(atSpec, ",") {
|
|
var hour, minute int
|
|
_, err = fmt.Sscanf(timeSpec, "%d:%d", &hour, &minute)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse %s: %v", timeSpec, err)
|
|
}
|
|
// The time instance on the same day as the base time
|
|
specOfDay := time.Date(base.Year(), base.Month(), base.Day(), hour, minute, 0, 0, base.Location())
|
|
// Bump it forward one day if it's before the base time
|
|
if !specOfDay.After(base) {
|
|
specOfDay = specOfDay.Add(24 * time.Hour)
|
|
}
|
|
nextUpdates = append(nextUpdates, specOfDay)
|
|
}
|
|
return
|
|
}
|
|
|
|
var weekdays = map[string]time.Weekday{
|
|
"Sun": time.Sunday,
|
|
"Mon": time.Monday,
|
|
"Tue": time.Tuesday,
|
|
"Wed": time.Wednesday,
|
|
"Thu": time.Thursday,
|
|
"Fri": time.Friday,
|
|
"Sat": time.Saturday,
|
|
}
|
|
|
|
// Get the next instances of the on-spec times after the base time.
|
|
// An on-spec is in the pattern DOW[,DOW[...]] where DOW is an abbreviated weekday
|
|
// or M/D[,M/D[...]] where M/D is a month and day.
|
|
// As a special case, "*/N" matches the Nth day of every month.
|
|
// An on-spec may be followed by an at-spec; otherwise, "at 00:00" is implied.
|
|
func parseOnSpec(base time.Time, onSpec string) (nextUpdates []time.Time, err error) {
|
|
type Date struct {
|
|
Year int
|
|
Month time.Month
|
|
Day int
|
|
}
|
|
type Time struct {
|
|
Hour int
|
|
Minute int
|
|
}
|
|
|
|
atSpec := "00:00"
|
|
if on := strings.Index(onSpec, " at "); on > -1 {
|
|
atSpec = onSpec[on+len(" at "):]
|
|
onSpec = onSpec[:on]
|
|
}
|
|
var atTimes []Time
|
|
for _, timeSpec := range strings.Split(atSpec, ",") {
|
|
var hour, minute int
|
|
_, err := fmt.Sscanf(timeSpec, "%d:%d", &hour, &minute)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse at-spec %s: %v", timeSpec, err)
|
|
}
|
|
atTimes = append(atTimes, Time{hour, minute})
|
|
}
|
|
|
|
var dates []Date
|
|
for _, daySpec := range strings.Split(onSpec, ",") {
|
|
if weekday, ok := weekdays[daySpec]; ok {
|
|
// For a weekday, add the next of that weekday 0-6 days ahead and 7-13 days ahead.
|
|
// The first date ensures that we don't miss multiple updates on the same day of the week
|
|
// (e.g. "on Sun at 06:00,18:00") and the second date ensures that we don't get stuck with
|
|
// only a date in the past (e.g. "on Sun at 02:00" when base is Sun 03:00).
|
|
daysForward := (int(weekday) - int(base.Weekday()) + 7) % 7 // value in [0,6]
|
|
day0_6 := base.AddDate(0, 0, daysForward)
|
|
dates = append(dates, Date{day0_6.Year(), day0_6.Month(), day0_6.Day()})
|
|
day7_13 := base.AddDate(0, 0, daysForward+7)
|
|
dates = append(dates, Date{day7_13.Year(), day7_13.Month(), day7_13.Day()})
|
|
} else if strings.HasPrefix(daySpec, "*/") {
|
|
// For every-month, add the date for the current month and the date for the next month.
|
|
var day int
|
|
_, err := fmt.Sscanf(daySpec, "*/%d", &day)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse month/day %s: %v", daySpec, err)
|
|
}
|
|
dates = append(dates, Date{base.Year(), base.Month(), day})
|
|
nextMonth := base.AddDate(0, 1, 0)
|
|
dates = append(dates, Date{nextMonth.Year(), nextMonth.Month(), day})
|
|
} else {
|
|
// For month/day, add the date for the base year and the next year.
|
|
var month, day int
|
|
_, err := fmt.Sscanf(daySpec, "%d/%d", &month, &day)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse month/day %s: %v", daySpec, err)
|
|
}
|
|
dates = append(dates, Date{base.Year(), time.Month(month), day})
|
|
dates = append(dates, Date{base.Year() + 1, time.Month(month), day})
|
|
}
|
|
}
|
|
|
|
// Now, for each date, create a datetime based on the at-spec.
|
|
for _, date := range dates {
|
|
for _, atTime := range atTimes {
|
|
nextUpdates = append(nextUpdates, time.Date(date.Year, date.Month, date.Day, atTime.Hour, atTime.Minute, 0, 0, base.Location()))
|
|
}
|
|
}
|
|
return
|
|
}
|