From 8c70be8fd0e7478418bc2ddd9f4eaf664bb6c014 Mon Sep 17 00:00:00 2001 From: huangsimin Date: Thu, 6 Dec 2018 11:07:16 +0800 Subject: [PATCH] the best crontabex v0.0.1 --- crontab.go | 412 ++++++++++++++++++++++++++++++++++++++++++++++++ crontab_test.go | 82 ++++++++++ trie_year.go | 269 +++++++++++++++++++++++++++++++ 3 files changed, 763 insertions(+) create mode 100644 crontab.go create mode 100644 crontab_test.go create mode 100644 trie_year.go diff --git a/crontab.go b/crontab.go new file mode 100644 index 0000000..257e562 --- /dev/null +++ b/crontab.go @@ -0,0 +1,412 @@ +package crontab + +import ( + "errors" + "fmt" + "log" + "regexp" + "strconv" + "strings" + "time" + + clinked "474420502.top/eson/structure/circular_linked" + "github.com/Pallinder/go-randomdata" + + "github.com/davecgh/go-spew/spew" +) + +type randLR struct { + left, right int +} + +type hInterval struct { + PlanFail []randLR + PlanNormal []randLR + + Count int + ConstCount int +} + +func (interval *hInterval) reset() { + interval.Count = interval.ConstCount +} + +type timePointer struct { + left, right int + leftlimit, rightlimit int + per int + isAll bool +} + +func (tp *timePointer) String() string { + return fmt.Sprintf("left: %d, right: %d, leftlimit: %d, rightlimit: %d, per: %d", tp.left, tp.right, tp.leftlimit, tp.rightlimit, tp.per) +} + +// Crontab 的string解析 +type Crontab struct { + min []timePointer + hour []timePointer + day []timePointer + month []timePointer + week []timePointer + + WillPlans []time.Time + SkipPlans []time.Time + + YearPlan *trieYear + + interval *clinked.CircularLinked + lastStatus bool + nextTime time.Time +} + +// NewCrontab create 一个crontab +func NewCrontab(crontab string) *Crontab { + cron := &Crontab{} + cron.FromString(crontab) + return cron +} + +// SetStatus 设置状态 接口定义 +func (cron *Crontab) SetStatus(status interface{}) { + + if cron.interval != nil { + cron.lastStatus = status.(bool) + } +} + +// GetStatus 设置状态 接口定义 +func (cron *Crontab) GetStatus() (status interface{}) { + if cron.interval != nil { + return cron.lastStatus + } + return nil +} + +// TimeUp 是否时间快到 +func (cron *Crontab) TimeUp() bool { + + if cron.interval != nil { + return cron.intervalTimeUp() + } + + return cron.linuxTimeUp() +} + +// NextTime 返回下次任务的时间 +func (cron *Crontab) NextTime() *time.Time { + if cron.interval != nil { + return &cron.nextTime + } + + if len(cron.WillPlans) > 0 { + return &cron.WillPlans[0] + } + + return nil +} + +func (cron *Crontab) String() string { + return fmt.Sprintf("min:%s\nhour:%s\nday:%s\nmonth:%s\nweek:%s\n", spew.Sdump(cron.min), spew.Sdump(cron.hour), spew.Sdump(cron.day), spew.Sdump(cron.month), spew.Sdump(cron.week)) +} + +// FromString 解析crontab 的 表达式 +func (cron *Crontab) FromString(crontab string) error { + crontab = strings.TrimSpace(crontab) + + matches := regexp.MustCompile("[^ ]+").FindAllString(crontab, -1) + mlen := len(matches) + switch mlen { + case 1: + // "f1-2|5-10x5,f1|10m,10-15,f1" + cron.nextTime = time.Now() + cron.lastStatus = true + cron.interval = clinked.NewCircularLinked() + var intervalList []interface{} + intervalList = parseIntervalString(matches[0]) + cron.interval.Append(intervalList...) + + case 5: + cron.min = createTimePointer(matches[0], 0, 59, true) + cron.hour = createTimePointer(matches[1], 0, 23, true) + cron.day = createTimePointer(matches[2], 1, 31, false) + cron.month = createTimePointer(matches[3], 1, 12, true) + cron.week = createTimePointer(matches[4], 0, 6, true) + + cron.createYearPlan() + default: + return errors.New("mathches len != want, check crontab string") + } + + return nil +} + +// createYearPlan 创建年度计划 +func (cron *Crontab) createYearPlan() { + cron.YearPlan = newTrieYear() + cron.YearPlan.FromCrontab(cron) +} + +func (cron *Crontab) linuxTimeUp() bool { + now := time.Now() + maxlen := 1000 + createlen := 500 + + plen := len(cron.WillPlans) + if plen <= createlen { + var lastplan time.Time + if plen == 0 { + lastplan = now + } else { + lastplan = cron.WillPlans[plen-1].Add(time.Minute) + } + if !cron.YearPlan.CheckYear() { + cron.createYearPlan() + } + + timeplans := cron.YearPlan.GetPlanTime(cron, lastplan, uint(maxlen-plen)) + cron.WillPlans = append(cron.WillPlans, timeplans...) + } + + if len(cron.WillPlans) > 0 { + istimeup := false + for i := 0; i < maxlen; i++ { + if now.Unix() >= cron.WillPlans[i].Unix() { + istimeup = true + } else { + if istimeup { + if i-1 > 0 { + cron.SkipPlans = append(cron.SkipPlans, cron.WillPlans[0:i-1]...) + if len(cron.SkipPlans) >= maxlen+200 { + cron.SkipPlans = cron.SkipPlans[200:] + } + } + cron.WillPlans = cron.WillPlans[i:] + return istimeup + } + + return istimeup + } + } + + cron.SkipPlans = append(cron.SkipPlans, cron.WillPlans...) + cron.WillPlans = nil + return istimeup + } + + log.Panicln("error willplans range") + return false +} + +func (cron *Crontab) intervalTimeUp() bool { + + now := time.Now() + if now.Unix() >= cron.nextTime.Unix() { + + iv := cron.interval.Cursor().GetValue().(hInterval) + isecond := 0 + if cron.lastStatus == false && len(iv.PlanFail) > 0 { + idx := randomdata.Number(len(iv.PlanFail)) + lr := iv.PlanFail[idx] + isecond = randomdata.Number(lr.left, lr.right+1) + } else { + idx := randomdata.Number(len(iv.PlanNormal)) + lr := iv.PlanNormal[idx] + isecond = randomdata.Number(lr.left, lr.right+1) + } + + iv.Count-- + if iv.Count <= 0 { + iv.reset() + cron.interval.MoveNext() + } + + cron.nextTime = now.Add(time.Duration(isecond) * time.Second) + return true + } + + return false +} + +func createTimePointer(min string, llimit, rlimit int, fixedLeftRight bool) []timePointer { + + var result []timePointer + + exelist := strings.Split(min, ",") + for _, exe := range exelist { + tp := timePointer{} + + takeper := strings.Split(exe, "/") // per + var rangevalue, per string + if len(takeper) == 1 { + rangevalue = exe + per = "1" + } else { + rangevalue = takeper[0] + per = takeper[1] + } + // takeRange + be := strings.Split(rangevalue, "-") + var left, rigth string + switch len(be) { + case 1: + left = be[0] + rigth = be[0] + case 2: + left = be[0] + rigth = be[1] + default: + panic(errors.New("range value is > 2")) + } + + if left == "*" { + tp.left = llimit + } else { + ileft, err := strconv.Atoi(strings.Replace(left, "^", "-", -1)) + if err != nil { + panic(err) + } + tp.left = ileft + } + + if rigth == "*" { + tp.right = rlimit + } else { + iright, err := strconv.Atoi(strings.Replace(rigth, "^", "-", -1)) + if err != nil { + panic(err) + } + tp.right = iright + } + + iper, err := strconv.Atoi(per) + if err != nil { + panic(err) + } + + tp.per = iper + tp.leftlimit = llimit + tp.rightlimit = rlimit + + // 修正左值 + leftfixed := tp.left + if leftfixed < 0 { + leftfixed += tp.rightlimit + 1 + if fixedLeftRight { + tp.left = leftfixed + } + } + + rightfixed := tp.right + if rightfixed < 0 { + rightfixed += tp.rightlimit + 1 + if fixedLeftRight { + tp.right = rightfixed + } + } + + // 全部符合 当左等于左 且 右等于右最大 并且 per == 1 TODO: 如果加入时间间隔 就需要 加多一个 附加值 + if leftfixed == tp.leftlimit && rightfixed == tp.rightlimit && tp.per == 1 { + tp.isAll = true + } + + result = append(result, tp) + } + + return result +} + +func parseIntervalString(crontab string) []interface{} { + var result []interface{} + + values := strings.Split(crontab, ",") + for _, value := range values { + interval := hInterval{} + // 次数 + valuesCounts := strings.Split(value, "x") + switch len(valuesCounts) { + case 1: + interval.ConstCount = 1 + + case 2: + count, err := strconv.Atoi(valuesCounts[1]) + if err != nil { + panic(err) + } + interval.ConstCount = count + default: + panic("valuesCounts error, the len is not in range") + } + + // 统计失败与普通间隔值的数组 + failAndNormal := valuesCounts[0] + + valuesFN := strings.Split(failAndNormal, "|") + + for _, FN := range valuesFN { + if FN == "" { + continue + } + + switch FN[0] { + case 'f', 'F': + fvalue := FN[1:] + interval.PlanFail = append(interval.PlanFail, parseRandLR(fvalue)) + case 'n': + value := FN[1:] + interval.PlanNormal = append(interval.PlanNormal, parseRandLR(value)) + default: + interval.PlanNormal = append(interval.PlanNormal, parseRandLR(FN)) + } + } + + interval.reset() + result = append(result, interval) + } + + return result +} + +func parseRandLR(value string) randLR { + vlen := len(value) + lastchar := value[vlen-1] + + lr := strings.Split(value, "-") + switch len(lr) { + case 1: + lr := randLR{parseTimeValue(lr[0], lastchar), parseTimeValue(lr[0], lastchar)} + return lr + case 2: + + lr := randLR{parseTimeValue(lr[0], lastchar), parseTimeValue(lr[1], lastchar)} + return lr + default: + panic("lr is error") + } +} + +func getInt(v string) int { + vint, err := strconv.Atoi(v) + if err != nil { + panic(err) + } + return vint +} + +func parseTimeValue(v string, lastchar byte) int { + + vlen := len(v) + + switch lastchar { + case 's': + return getInt(v[:vlen-1]) + case 'm': + return getInt(v[:vlen-1]) * 60 + case 'h': + return getInt(v[:vlen-1]) * 3600 + case 'd': + return getInt(v[:vlen-1]) * 3600 * 24 + default: + return getInt(v) + } +} diff --git a/crontab_test.go b/crontab_test.go new file mode 100644 index 0000000..77663ab --- /dev/null +++ b/crontab_test.go @@ -0,0 +1,82 @@ +package crontab + +import ( + "fmt" + "runtime" + "testing" + "time" +) + +// type LRValue struct { +// left, right int +// } + +func TestParseCrontab(t *testing.T) { + // crontab := "0-5/2,7-30/3,30,35,40-^1 * * * *" //(秒) 分 时 号(每月的多少号, 要注意月可可能性) 星期几(每个星期的) /每 ,列表 -范围 + crontab := "* * * * *" + + PrintMemUsage() + + ty := newTrieYear() + cron := NewCrontab(crontab) + ty.FromCrontab(cron) + + if len(ty.GetPlanTime(cron, time.Now(), 10)) != 10 { + t.Error("GetPlanTime error len != 10") + } + + cron.createYearPlan() + if !cron.TimeUp() { + t.Error("timeup error") + } + + PrintMemUsage() + +} + +func TestParseInterval(t *testing.T) { + //crontab := "0-5/2,7-30/3,30,35,40-^1 * * * *" //(秒) 分 时 号(每月的多少号, 要注意月可可能性) 星期几(每个星期的) /每 ,列表 -范围 + //crontab := "f1-2|1-3|5-8x5,f1|10m,10-15,f1" + crontab := "2" + cron := NewCrontab(crontab) + now := time.Now() + for i := 0; i <= 6; i++ { + if cron.TimeUp() { + + sec := time.Since(now).Seconds() + if i != 0 { + if 2.0 <= sec && sec <= 2.1 { + t.Log(sec) + } else { + t.Error("interval time is ", sec) + } + } + + now = time.Now() + } + time.Sleep(time.Second) + } +} + +func TestParseIntervalPlus(t *testing.T) { + // crontab := "0-5/2,7-30/3,30,35,40-^1 * * * *" //(秒) 分 时 号(每月的多少号, 要注意月可可能性) 星期几(每个星期的) /每 ,列表 -范围 + // crondata := NewCrontab("*22 * * * *") + + //crontab := "0-5/2,7-30/3,30,35,40-^1 * * * *" //(秒) 分 时 号(每月的多少号, 要注意月可可能性) 星期几(每个星期的) /每 ,列表 -范围 + //crontab := "f1-2|1-3|5-8x5,f1|10m,10-15,f1" + +} + +func PrintMemUsage() { + var m runtime.MemStats + runtime.ReadMemStats(&m) + // For info on each, see: https://golang.org/pkg/runtime/#MemStats + fmt.Printf("Alloc = %v MiB", bToMb(m.Alloc)) + fmt.Printf("\tTotalAlloc = %v MiB", bToMb(m.TotalAlloc)) + fmt.Printf("\tSys = %v MiB", bToMb(m.Sys)) + fmt.Printf("\tNumGC = %v\n", m.NumGC) +} + +func bToMb(b uint64) uint64 { + return b / 1024 / 1024 +} diff --git a/trie_year.go b/trie_year.go new file mode 100644 index 0000000..55f65c5 --- /dev/null +++ b/trie_year.go @@ -0,0 +1,269 @@ +package crontab + +import ( + "time" +) + +type minuteNode struct { +} + +func newMinuteNode() *minuteNode { + return &minuteNode{} +} + +type hourNode struct { + Minute [60]*minuteNode +} + +func (hour *hourNode) CreateMinute(nminute int) { + min := &minuteNode{} + hour.Minute[nminute] = min +} + +func newHourNode() *hourNode { + return &hourNode{} +} + +type dayNode struct { + IsClear bool + Week time.Weekday + Hour [24]*hourNode +} + +func (day *dayNode) CreateHour(nhour int) { + hour := &hourNode{} + day.Hour[nhour] = hour +} + +func newDayNode(curday *time.Time) *dayNode { + day := &dayNode{} + + week := curday.Weekday() + day.Week = week + day.IsClear = true + + return day +} + +type monthNode struct { + MaxDay int + First *time.Time + Day [32]*dayNode +} + +func (month *monthNode) CreateDay(nday int) { + day := month.First.AddDate(0, 0, nday-1) + month.Day[nday] = newDayNode(&day) +} + +func newMonthNode(year, month int) *monthNode { + Month := &monthNode{} + First := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.Local) + Month.First = &First + Month.MaxDay = Month.First.AddDate(0, 1, -1).Day() + return Month +} + +type trieYear struct { + Year int + Month [13]*monthNode +} + +// CheckYear 跨年判断 +func (ty *trieYear) CheckYear() bool { + year := time.Now().Year() + return ty.Year == year +} + +func (ty *trieYear) clearHour() { + + for _, month := range ty.Month { + if month != nil { + for _, day := range month.Day { + if day != nil { + for i := 0; i <= 23; i++ { + day.Hour[i] = nil + day.IsClear = true + } + } + + } + } + + } + +} + +func newTrieYear() *trieYear { + ty := trieYear{} + ty.Year = time.Now().Year() + return &ty +} + +// GetPlanTime 获取计划表 +func (ty *trieYear) GetPlanTime(cron *Crontab, aftertime time.Time, count uint) []time.Time { + + now := aftertime + nmonth := int(now.Month()) + nday := now.Day() + nhour := now.Hour() + nminute := now.Minute() + + var result []time.Time + + for i := 1; i <= 12; i++ { + + if i < nmonth { + continue + } + + month := ty.Month[i] + if month != nil { + + for j := 1; j <= 31; j++ { + + if nmonth == i { + if j < nday { + continue + } + } + + day := month.Day[j] + if day != nil { + + if day.IsClear { + insertHour(cron, day) + } + + for k := 0; k <= 23; k++ { + + if nmonth == i && nday == j { + if k < nhour { + continue + } + } + + hour := day.Hour[k] + + if hour != nil { + + for n := 0; n <= 59; n++ { + if nmonth == i && nday == j && nhour == k { + if n < nminute { + continue + } + } + + min := hour.Minute[n] + if min != nil { + + curTime := time.Date(ty.Year, time.Month(i), j, k, n, 0, 0, time.Local) + result = append(result, curTime) + count-- + if count <= 0 { + ty.clearHour() + return result + } + } + } + + } + } + + } + } + } + } + + ty.clearHour() + return result +} + +// FromCrontab 从Crontab生成树 +func (ty *trieYear) FromCrontab(cron *Crontab) { + // 月的填充 + for _, month := range cron.month { + + left := month.left + right := month.right + + for i := left; i <= right; i += month.per { + curMonth := newMonthNode(ty.Year, i) + ty.Month[i] = curMonth + // 天的填充 + insertDay(cron, curMonth) + } + } +} + +func filterDay(cron *Crontab, curday *dayNode) bool { + + for _, w := range cron.week { + if w.isAll { + return false + } + + for n := w.left; n <= w.right; n += w.per { + if n == int(curday.Week) { + return false + } + } + } + return true +} + +func insertDay(cron *Crontab, curMonth *monthNode) { + for _, day := range cron.day { + + left := day.left + if left < 0 { + left += curMonth.MaxDay + 1 + } + + right := day.right + if right < 0 { + right += curMonth.MaxDay + 1 + } + + for j := left; j <= right; j += day.per { + curMonth.CreateDay(j) + curDay := curMonth.Day[j] + + if filterDay(cron, curDay) { + curMonth.Day[j] = nil + } else { + // insertHour(cron, curDay) + } + } + + } +} + +func insertHour(cron *Crontab, curDay *dayNode) { + curDay.IsClear = false + // 时的填充 + for _, hour := range cron.hour { + + left := hour.left + right := hour.right + + for k := left; k <= right; k += hour.per { + curDay.CreateHour(k) + curHour := curDay.Hour[k] + insertMinute(cron, curHour) + } + } +} + +func insertMinute(cron *Crontab, curHour *hourNode) { + for _, min := range cron.min { + + left := min.left + right := min.right + + for l := left; l <= right; l += min.per { + curHour.CreateMinute(l) + } + + } +}