git-sync-web 自动提交 2026-09-08 17:12:27
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 5 段 cron:分 时 日 月 周。支持 * 、数字、范围 a-b、步长 */n 与 a-b/n、逗号列表。
|
||||
// 日(day-of-month) 与 周(day-of-week) 均受限时采用标准 cron 的「或」语义。
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// cronNext 返回 spec 在 after 之后的下一次触发时间(分钟精度,最多向前搜索一年)
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user