Split up spec parsing and usage
This commit is contained in:
parent
74b6d40774
commit
bd61ef5f68
172
core/cron.go
172
core/cron.go
@ -8,57 +8,95 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type cronSpec interface {
|
||||
// Get the next time matching this spec that is after the base time.
|
||||
GetNextTime(base time.Time) (nextUpdate time.Time)
|
||||
}
|
||||
|
||||
func GetNextSpecTime(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:
|
||||
// "HH:MM" is implicitly an at-spec "at HH:MM", so see if it's that
|
||||
nextUpdates, err = parseAtSpec(lastUpdated, spec)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("unknown spec format: %v", spec)
|
||||
}
|
||||
}
|
||||
var cron cronSpec
|
||||
cron, err = ParseCronSpec(spec)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return cron.GetNextTime(lastUpdated), nil
|
||||
}
|
||||
|
||||
func ParseCronSpec(spec string) (cron cronSpec, err error) {
|
||||
switch {
|
||||
case strings.HasPrefix(spec, "every "):
|
||||
cron, err = parseEverySpec(spec[len("every "):])
|
||||
case strings.HasPrefix(spec, "at "):
|
||||
cron, err = parseAtSpec(spec[len("at "):])
|
||||
case strings.HasPrefix(spec, "on "):
|
||||
cron, err = parseOnSpec(spec[len("on "):])
|
||||
default:
|
||||
// Interpret "HH:MM" as "at HH:MM"
|
||||
cron, err = parseAtSpec(spec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unknown spec format: %v", spec)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Return the time that follows the base time most closely.
|
||||
func nextAfter(base time.Time, nextUpdates []time.Time) (nextUpdate time.Time) {
|
||||
for _, next := range nextUpdates {
|
||||
if next.After(lastUpdated) && (nextUpdate.IsZero() || next.Before(nextUpdate)) {
|
||||
if next.After(base) && (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
|
||||
// An every-spec is a Go duration string, e.g. "every 5m"
|
||||
type everySpec struct {
|
||||
duration time.Duration
|
||||
}
|
||||
|
||||
func parseEverySpec(spec string) (*everySpec, error) {
|
||||
duration, err := time.ParseDuration(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &everySpec{duration}, nil
|
||||
}
|
||||
|
||||
// Get the next time matching the every-spec that is after the base time.
|
||||
func (spec *everySpec) GetNextTime(base time.Time) (nextUpdate time.Time) {
|
||||
next := base.Round(spec.duration)
|
||||
if !next.After(base) {
|
||||
next = next.Add(spec.duration)
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
// 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, ",") {
|
||||
type atSpec struct {
|
||||
hours []int
|
||||
minutes []int
|
||||
}
|
||||
|
||||
func parseAtSpec(spec string) (*atSpec, error) {
|
||||
var hours, minutes []int
|
||||
for _, timeSpec := range strings.Split(spec, ",") {
|
||||
var hour, minute int
|
||||
_, err = fmt.Sscanf(timeSpec, "%d:%d", &hour, &minute)
|
||||
_, err := fmt.Sscanf(timeSpec, "%d:%d", &hour, &minute)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse %s: %v", timeSpec, err)
|
||||
return nil, fmt.Errorf("could not parse at-spec %s: %w", timeSpec, err)
|
||||
}
|
||||
hours = append(hours, hour)
|
||||
minutes = append(minutes, minute)
|
||||
}
|
||||
return &atSpec{hours, minutes}, nil
|
||||
}
|
||||
|
||||
// Get the next time matching the at-spec that is after the base time.
|
||||
func (spec *atSpec) GetNextTime(base time.Time) (nextUpdate time.Time) {
|
||||
nextUpdates := []time.Time{}
|
||||
for i, hour := range spec.hours {
|
||||
minute := spec.minutes[i]
|
||||
// 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
|
||||
@ -67,7 +105,7 @@ func parseAtSpec(base time.Time, atSpec string) (nextUpdates []time.Time, err er
|
||||
}
|
||||
nextUpdates = append(nextUpdates, specOfDay)
|
||||
}
|
||||
return
|
||||
return nextAfter(base, nextUpdates)
|
||||
}
|
||||
|
||||
var weekdays = map[string]time.Weekday{
|
||||
@ -131,62 +169,66 @@ func (date *onSpecMonthDay) getDates(base time.Time) (dates []ymd) {
|
||||
return
|
||||
}
|
||||
|
||||
// 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 Time struct {
|
||||
Hour int
|
||||
Minute int
|
||||
type onSpec struct {
|
||||
atSpec atSpec
|
||||
dates []onSpecDate
|
||||
}
|
||||
|
||||
func parseOnSpec(spec string) (*onSpec, error) {
|
||||
atPart := "00:00"
|
||||
if at := strings.Index(spec, " at "); at > -1 {
|
||||
atPart = spec[at+len(" at "):]
|
||||
spec = spec[:at]
|
||||
}
|
||||
atSpec, err := parseAtSpec(atPart)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not parse at-spec %v: %w", atSpec, err)
|
||||
}
|
||||
|
||||
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 []ymd
|
||||
for _, daySpec := range strings.Split(onSpec, ",") {
|
||||
var date onSpecDate
|
||||
var dates []onSpecDate
|
||||
for _, daySpec := range strings.Split(spec, ",") {
|
||||
if weekday, ok := weekdays[daySpec]; ok {
|
||||
date = &onSpecWeekday{weekday}
|
||||
dates = append(dates, &onSpecWeekday{weekday})
|
||||
} else if strings.HasPrefix(daySpec, "*/") {
|
||||
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)
|
||||
}
|
||||
date = &onSpecEveryMonth{day}
|
||||
dates = append(dates, &onSpecEveryMonth{day})
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
date = &onSpecMonthDay{month, day}
|
||||
dates = append(dates, &onSpecMonthDay{month, day})
|
||||
}
|
||||
}
|
||||
return &onSpec{*atSpec, dates}, nil
|
||||
}
|
||||
|
||||
// Get the next time matching the on-spec that is after the base time.
|
||||
func (spec *onSpec) GetNextTime(base time.Time) (nextUpdate time.Time) {
|
||||
// Convert each type of on-spec date spec to a concrete YMD date
|
||||
var dates []ymd
|
||||
for _, date := range spec.dates {
|
||||
dates = append(dates, date.getDates(base)...)
|
||||
}
|
||||
|
||||
// Now, for each date, create a datetime based on the at-spec.
|
||||
// Then, for each date, create a datetime for each at-spec.
|
||||
nextUpdates := []time.Time{}
|
||||
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()))
|
||||
for i, atHour := range spec.atSpec.hours {
|
||||
atMinute := spec.atSpec.minutes[i]
|
||||
nextUpdates = append(nextUpdates, time.Date(date.Year, date.Month, date.Day, atHour, atMinute, 0, 0, base.Location()))
|
||||
}
|
||||
}
|
||||
return
|
||||
return nextAfter(base, nextUpdates)
|
||||
}
|
||||
|
||||
type Schedule struct {
|
||||
|
Loading…
Reference in New Issue
Block a user