130 lines
3.3 KiB
Go
130 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
|
|
type cronSpec struct {
|
|
min, hour, dom, mon, dow map[int]bool
|
|
domAll, dowAll bool
|
|
}
|
|
|
|
func parseCron(spec string) (*cronSpec, error) {
|
|
fields := strings.Fields(spec)
|
|
if len(fields) != 5 {
|
|
return nil, fmt.Errorf("cron 需要 5 个字段(分 时 日 月 周),收到 %d 个", len(fields))
|
|
}
|
|
c := &cronSpec{}
|
|
var err error
|
|
if c.min, err = parseCronField(fields[0], 0, 59); err != nil {
|
|
return nil, fmt.Errorf("分钟字段: %w", err)
|
|
}
|
|
if c.hour, err = parseCronField(fields[1], 0, 23); err != nil {
|
|
return nil, fmt.Errorf("小时字段: %w", err)
|
|
}
|
|
if c.dom, err = parseCronField(fields[2], 1, 31); err != nil {
|
|
return nil, fmt.Errorf("日字段: %w", err)
|
|
}
|
|
if c.mon, err = parseCronField(fields[3], 1, 12); err != nil {
|
|
return nil, fmt.Errorf("月字段: %w", err)
|
|
}
|
|
if c.dow, err = parseCronField(fields[4], 0, 7); err != nil {
|
|
return nil, fmt.Errorf("星期字段: %w", err)
|
|
}
|
|
if c.dow[7] {
|
|
c.dow[0] = true
|
|
}
|
|
c.domAll = len(c.dom) == 31
|
|
c.dowAll = len(c.dow) == 8
|
|
return c, nil
|
|
}
|
|
|
|
func parseCronField(f string, min, max int) (map[int]bool, error) {
|
|
set := map[int]bool{}
|
|
for _, part := range strings.Split(f, ",") {
|
|
part = strings.TrimSpace(part)
|
|
if part == "" {
|
|
return nil, fmt.Errorf("存在空字段")
|
|
}
|
|
step := 1
|
|
if i := strings.Index(part, "/"); i >= 0 {
|
|
n, err := strconv.Atoi(part[i+1:])
|
|
if err != nil || n <= 0 {
|
|
return nil, fmt.Errorf("无效步长 %q", part)
|
|
}
|
|
step = n
|
|
part = part[:i]
|
|
if part == "" {
|
|
part = "*"
|
|
}
|
|
}
|
|
lo, hi := 0, 0
|
|
if part == "*" {
|
|
lo, hi = min, max
|
|
} else if rng := strings.SplitN(part, "-", 2); len(rng) == 2 {
|
|
var err error
|
|
if lo, err = strconv.Atoi(rng[0]); err != nil {
|
|
return nil, fmt.Errorf("无效值 %q", part)
|
|
}
|
|
if hi, err = strconv.Atoi(rng[1]); err != nil {
|
|
return nil, fmt.Errorf("无效值 %q", part)
|
|
}
|
|
} else {
|
|
v, err := strconv.Atoi(part)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("无效值 %q", part)
|
|
}
|
|
lo, hi = v, v
|
|
}
|
|
if lo < min || hi > max || lo > hi {
|
|
return nil, fmt.Errorf("值超出范围 [%d,%d]: %q", min, max, part)
|
|
}
|
|
for v := lo; v <= hi; v += step {
|
|
set[v] = true
|
|
}
|
|
}
|
|
return set, nil
|
|
}
|
|
|
|
func cronNextString(expr string) (string, error) {
|
|
spec, err := parseCron(expr)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return cronNext(spec, time.Now()).Format("2006-01-02 15:04"), nil
|
|
}
|
|
|
|
func cronNext(spec *cronSpec, after time.Time) time.Time {
|
|
t := after.Truncate(time.Minute).Add(time.Minute)
|
|
limit := t.AddDate(1, 0, 0)
|
|
for t.Before(limit) {
|
|
if !spec.mon[int(t.Month())] {
|
|
t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()).AddDate(0, 1, 0)
|
|
continue
|
|
}
|
|
domOK := spec.dom[t.Day()]
|
|
dowOK := spec.dow[int(t.Weekday())]
|
|
dayOK := domOK && dowOK
|
|
if !spec.domAll && !spec.dowAll {
|
|
dayOK = domOK || dowOK
|
|
}
|
|
if !dayOK {
|
|
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()).AddDate(0, 0, 1)
|
|
continue
|
|
}
|
|
if !spec.hour[t.Hour()] {
|
|
t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location()).Add(time.Hour)
|
|
continue
|
|
}
|
|
if spec.min[t.Minute()] {
|
|
return t
|
|
}
|
|
t = t.Add(time.Minute)
|
|
}
|
|
return limit
|
|
}
|