diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d4f1e3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +go-quartz diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68406f9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 reugn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..91528e9 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# go-quartz +A go scheduling library + +## About +Inspired by [quartz](https://github.com/quartz-scheduler/quartz) java scheduler. + +### Library building blocks +Job interface. Should be implemented by custom jobs for further scheduling +```go +type Job interface { + Execute() + Description() string +} +``` +Scheduler interface +```go +type Scheduler interface { + Start() + ScheduleJob(job Job, trigger Trigger) error + Stop() +} +``` +Implemented Schedulers +- StdScheduler + +Trigger interface +```go +type Trigger interface { + NextFireTime(prev int64) (int64, error) + Description() string +} +``` +Implemented Triggers +- CronTrigger +- SimpleTrigger +- RunOnceTrigger + +## Examples +Could be found in examples directory. + +## License +Licensed under the MIT License. diff --git a/examples/go-quartz.go b/examples/go-quartz.go new file mode 100644 index 0000000..df99839 --- /dev/null +++ b/examples/go-quartz.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "time" + + "go-quartz/quartz" +) + +//implements quartz.Job interface +type PrintJob struct { + desc string +} + +func (pj PrintJob) Description() string { + return pj.desc +} + +func (pj PrintJob) Execute() { + fmt.Println("Executing " + pj.Description()) +} + +//demo main +func main() { + sched := quartz.NewStdScheduler() + sched.Start() + sched.ScheduleJob(&PrintJob{"Ad hoc Job"}, quartz.NewRunOnceTrigger(time.Second*5)) + sched.ScheduleJob(&PrintJob{"First job"}, quartz.NewSimpleTrigger(time.Second*12)) + sched.ScheduleJob(&PrintJob{"Second job"}, quartz.NewSimpleTrigger(time.Second*6)) + sched.ScheduleJob(&PrintJob{"Third job"}, quartz.NewSimpleTrigger(time.Second*3)) + time.Sleep(time.Second * 15) + sched.Stop() +} diff --git a/quartz/cron.go b/quartz/cron.go new file mode 100644 index 0000000..3c55dff --- /dev/null +++ b/quartz/cron.go @@ -0,0 +1,395 @@ +package quartz + +import ( + "errors" + "fmt" + "sort" + "strconv" + "strings" + "time" +) + +type CronTrigger struct { + fields []*CronField + lastDefined int +} + +func NewCronTrigger(expr string) (*CronTrigger, error) { + fields, err := validateCronExpression(expr) + if err != nil { + return nil, err + } + lastDefined := -1 + for i, field := range fields { + if len(field.values) > 0 { + lastDefined = i + } + } + // full wildcard expression + if lastDefined == -1 { + fields[0].values, _ = fillRange(0, 59) + } + return &CronTrigger{fields, lastDefined}, nil +} + +type CronExpressionParser struct { + minuteBump bool + hourBump bool + dayBump bool + monthBump bool + yearBump bool + done bool + + lastDefined int + maxDays int +} + +func NewCronExpressionParser(lastDefined int) *CronExpressionParser { + return &CronExpressionParser{false, false, false, false, false, false, + lastDefined, 0} +} + +type CronField struct { + values []int +} + +func (cf *CronField) isEmpty() bool { + return len(cf.values) == 0 +} + +func (cf *CronField) toString() string { + return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(cf.values)), ","), "[]") +} + +var ( + months = []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"} + days = []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"} + daysInMonth = []int{31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31} +) + +// +// field is optional +const ( + secondIndex = iota + minuteIndex + hourIndex + dayOfMonthIndex + monthIndex + dayOfWeekIndex + yearIndex +) + +func (ct *CronTrigger) NextFireTime(prev int64) (int64, error) { + parser := NewCronExpressionParser(ct.lastDefined) + return parser.nextTime(prev, ct.fields) +} + +func (parser *CronExpressionParser) nextTime(prev int64, fields []*CronField) (nextTime int64, err error) { + defer func() { + if r := recover(); r != nil { + switch x := r.(type) { + case string: + err = errors.New(x) + case error: + err = x + default: + err = errors.New("Unknown cron expression error") + } + } + }() + // UnixDate = "Mon Jan _2 15:04:05 MST 2006" + tfmt := time.Unix(prev, 0).Format(time.UnixDate) + ttok := strings.Split(strings.Replace(tfmt, " ", " ", 1), " ") + hms := strings.Split(ttok[3], ":") + parser.maxDays = maxDays(intVal(months, ttok[1]), atoi(ttok[5])) + + second := parser.nextSeconds(atoi(hms[2]), fields[0]) + minute := parser.nextMinutes(atoi(hms[1]), fields[1]) + hour := parser.nextHours(atoi(hms[0]), fields[2]) + dayOfWeek, dayOfMonth := parser.nextDay(intVal(days, ttok[0]), fields[5], + atoi(strings.Replace(ttok[2], "_", "", 1)), fields[3]) + month := parser.nextMonth(ttok[1], fields[4]) + year := parser.nextYear(ttok[5], fields[6]) + + nstr := fmt.Sprintf("%s %s %s %s:%s:%s %s %s", dayOfWeek, month, dayOfMonth, + hour, minute, second, ttok[4], year) + ntime, err := time.Parse(time.UnixDate, nstr) + nextTime = ntime.Unix() + return +} + +//the ? wildcard is only used in the day of month and day of week fields +func validateCronExpression(expression string) ([]*CronField, error) { + var tokens []string + tokens = strings.Split(expression, " ") + length := len(tokens) + if length < 6 || length > 7 { + return nil, cronError("length") + } + if length == 6 { + tokens = append(tokens, "*") + } + if (tokens[3] != "?" && tokens[3] != "*") && tokens[5] != "?" { + return nil, cronError("day field set twice") + } + if tokens[6] != "*" { + return nil, cronError("year field not supported, use asterisk") + } + var err error + fields := make([]*CronField, 7) + fields[0], err = parseField(tokens[0], 0, 59) + fields[1], err = parseField(tokens[1], 0, 59) + fields[2], err = parseField(tokens[2], 0, 23) + fields[3], err = parseField(tokens[3], 1, 31) + fields[4], err = parseField(tokens[4], 1, 12, months) + fields[5], err = parseField(tokens[5], 0, 6, days) + fields[6], err = parseField(tokens[6], 1970, 1970*2) + if err != nil { + return nil, err + } + return fields, nil +} + +func parseField(field string, min int, max int, translate ...[]string) (*CronField, error) { + var tr []string + if len(translate) > 0 { + tr = translate[0] + } + //any value + if field == "*" || field == "?" { + return &CronField{[]int{}}, nil + } + //single value + i, err := strconv.Atoi(field) + if err == nil { + if inScope(i, min, max) { + return &CronField{[]int{i}}, nil + } + return nil, cronError("single min/max validation") + } + //list of values + if strings.Contains(field, ",") { + t := strings.Split(field, ",") + si, err := sliceAtoi(t) + if err != nil { + //TODO: translation can fail + si = indexes(t, tr) + } + sort.Ints(si) + return &CronField{si}, nil + } + //range of values + if strings.Contains(field, "-") { + var _range []int + t := strings.Split(field, "-") + if len(t) != 2 { + return nil, cronError("parse range") + } + from := normalize(t[0], tr) + to := normalize(t[1], tr) + if !inScope(from, min, max) || !inScope(to, min, max) { + return nil, cronError("range min/max validation") + } + _range, err = fillRange(from, to) + if err != nil { + return nil, err + } + return &CronField{_range}, nil + } + //step values + if strings.Contains(field, "/") { + var _step []int + t := strings.Split(field, "/") + if len(t) != 2 { + return nil, cronError("parse step") + } + from := normalize(t[0], tr) + step := atoi(t[1]) + if !inScope(from, min, max) { + return nil, cronError("step min/max validation") + } + _step, err = fillStep(from, step, max) + if err != nil { + return nil, err + } + return &CronField{_step}, nil + } + //literal single value + if tr != nil { + i := intVal(tr, field) + if i >= 0 { + if inScope(i, min, max) { + return &CronField{[]int{i}}, nil + } + return nil, cronError("literal min/max validation") + } + } + return nil, cronError("parse") +} + +func (parser *CronExpressionParser) setDone(index int) { + if parser.lastDefined == index { + parser.done = true + } +} + +func (parser *CronExpressionParser) lastSet(index int) bool { + if parser.lastDefined <= index { + return true + } + return false +} + +func (parser *CronExpressionParser) nextSeconds(prev int, field *CronField) string { + var next int + next, parser.minuteBump = parser.findNextValue(prev, field.values) + parser.setDone(secondIndex) + return alignDigit(next, "0") +} + +func (parser *CronExpressionParser) nextMinutes(prev int, field *CronField) string { + var next int + if field.isEmpty() && parser.lastSet(minuteIndex) { + if parser.minuteBump { + next, parser.hourBump = bumpValue(prev, 59, 1) + return alignDigit(next, "0") + } + return alignDigit(prev, "0") + } + next, parser.hourBump = parser.findNextValue(prev, field.values) + parser.setDone(minuteIndex) + return alignDigit(next, "0") +} + +func (parser *CronExpressionParser) nextHours(prev int, field *CronField) string { + var next int + if field.isEmpty() && parser.lastSet(hourIndex) { + if parser.hourBump { + next, parser.dayBump = bumpValue(prev, 23, 1) + return alignDigit(next, "0") + } + return alignDigit(prev, "0") + } + next, parser.dayBump = parser.findNextValue(prev, field.values) + parser.setDone(hourIndex) + return alignDigit(next, "0") +} + +func (parser *CronExpressionParser) nextDay(prevWeek int, weekField *CronField, + prevMonth int, monthField *CronField) (string, string) { + var nextMonth, nextWeek int + if weekField.isEmpty() && monthField.isEmpty() && parser.lastSet(dayOfWeekIndex) { + if parser.dayBump { + nextMonth, parser.monthBump = bumpValue(prevMonth, parser.maxDays, 1) + nextWeek, _ = bumpLiteral(prevWeek, 6, 1) + return days[nextWeek], alignDigit(nextMonth, " ") + } + return days[prevWeek], alignDigit(prevMonth, " ") + } + if len(monthField.values) > 0 { + nextMonth, parser.monthBump = parser.findNextValue(prevMonth, monthField.values) + parser.setDone(dayOfMonthIndex) + nextWeek, _ = bumpLiteral(prevWeek, 6, step(prevMonth, nextMonth, parser.maxDays)) + return days[nextWeek], alignDigit(nextMonth, " ") + } else if len(weekField.values) > 0 { + nextWeek, bumpDayOfMonth := parser.findNextValue(prevWeek, weekField.values) + parser.setDone(dayOfWeekIndex) + var _step int + if bumpDayOfMonth && len(weekField.values) == 1 { + _step = 7 + } else { + _step = step(prevWeek, nextWeek, 7) + } + nextMonth, parser.monthBump = bumpValue(prevMonth, parser.maxDays, _step) + return days[nextWeek], alignDigit(nextMonth, " ") + } + return days[prevWeek], alignDigit(prevMonth, " ") +} + +func (parser *CronExpressionParser) nextMonth(prev string, field *CronField) string { + var next int + if field.isEmpty() && parser.lastSet(dayOfWeekIndex) { + if parser.monthBump { + next, parser.yearBump = bumpLiteral(intVal(months, prev), 11, 1) + return months[next] + } + return prev + } + next, parser.yearBump = parser.findNextValue(intVal(months, prev), field.values) + parser.setDone(monthIndex) + return months[next] +} + +func (parser *CronExpressionParser) nextYear(prev string, field *CronField) string { + var next int + if field.isEmpty() && parser.lastSet(yearIndex) { + if parser.yearBump { + next, _ = bumpValue(prev, int(^uint(0)>>1), 1) + return strconv.Itoa(next) + } + return prev + } + next, halt := parser.findNextValue(prev, field.values) + if halt != false { + panic("out of expression range") + } + return strconv.Itoa(next) +} + +func bumpLiteral(iprev int, max int, step int) (int, bool) { + bumped := iprev + step + if bumped > max { + if bumped%max == 0 { + return iprev, true + } + return (bumped % max) - 1, true + } + return bumped, false +} + +// return: bumped value, bump next +func bumpValue(prev interface{}, max int, step int) (int, bool) { + var iprev, bumped int + switch prev.(type) { + case string: + iprev, _ = strconv.Atoi(prev.(string)) + case int: + iprev = prev.(int) + default: + panic("Unknown type at bumpValue") + } + bumped = iprev + step + if bumped > max { + return bumped % max, true + } + return bumped, false +} + +// return: next value, bump next +func (parser *CronExpressionParser) findNextValue(prev interface{}, values []int) (int, bool) { + var iprev int + switch prev.(type) { + case string: + iprev, _ = strconv.Atoi(prev.(string)) + case int: + iprev = prev.(int) + default: + panic("Unknown type at findNextValue") + } + if len(values) == 0 { + return iprev, false + } + for _, element := range values { + if parser.done { + if element >= iprev { + return element, false + } + } else { + if element > iprev { + parser.done = true + return element, false + } + } + } + return values[0], true +} diff --git a/quartz/cron_test.go b/quartz/cron_test.go new file mode 100644 index 0000000..f44af93 --- /dev/null +++ b/quartz/cron_test.go @@ -0,0 +1,104 @@ +package quartz + +import ( + "testing" + "time" +) + +func TestCronExpression1(t *testing.T) { + prev := int64(1554120000) + result := "" + cronTrigger, err := NewCronTrigger("10/20 15 14 5-10 * ? *") + if err != nil { + t.Fatal(err) + } else { + result, _ = iterate(prev, cronTrigger, 1000) + } + assertEqual(t, result, "Wed Nov 8 14:15:10 IST 2023") +} + +func TestCronExpression2(t *testing.T) { + prev := int64(1554120000) + result := "" + cronTrigger, err := NewCronTrigger("* 5,7,9 14-16 * * ? *") + if err != nil { + t.Fatal(err) + } else { + result, _ = iterate(prev, cronTrigger, 1000) + } + assertEqual(t, result, "Sun Jul 21 15:05:00 IDT 2019") +} + +func TestCronExpression3(t *testing.T) { + prev := int64(1554120000) + result := "" + cronTrigger, err := NewCronTrigger("* 5,7,9 14/2 * * Wed,Sat *") + if err != nil { + t.Fatal(err) + } else { + result, _ = iterate(prev, cronTrigger, 1000) + } + assertEqual(t, result, "Wed Nov 20 22:05:00 IST 2019") +} + +func TestCronExpression4(t *testing.T) { + expression := "0 5,7 14 1 * Sun *" + _, err := NewCronTrigger(expression) + if err == nil { + t.Fatalf("%s should fail", expression) + } +} + +func TestCronExpression5(t *testing.T) { + prev := int64(1554120000) + result := "" + cronTrigger, err := NewCronTrigger("* * * * * ? *") + if err != nil { + t.Fatal(err) + } else { + result, _ = iterate(prev, cronTrigger, 1000) + } + assertEqual(t, result, "Mon Apr 1 15:16:40 IDT 2019") +} + +func TestCronExpression6(t *testing.T) { + prev := int64(1554120000) + result := "" + cronTrigger, err := NewCronTrigger("* * 14/2 * * Mon/3 *") + if err != nil { + t.Fatal(err) + } else { + result, _ = iterate(prev, cronTrigger, 1000) + } + assertEqual(t, result, "Thu Mar 4 20:00:00 IST 2021") +} + +func TestCronExpression7(t *testing.T) { + prev := int64(1554120000) + result := "" + cronTrigger, err := NewCronTrigger("* 5-9 14/2 * * 0-2 *") + if err != nil { + t.Fatal(err) + } else { + result, _ = iterate(prev, cronTrigger, 1000) + } + assertEqual(t, result, "Tue Jul 2 14:09:00 IDT 2019") +} + +func assertEqual(t *testing.T, a interface{}, b interface{}) { + if a != b { + t.Fatalf("%s != %s", a, b) + } +} + +func iterate(prev int64, cronTrigger *CronTrigger, iterations int) (string, error) { + var err error + for i := 0; i < iterations; i++ { + prev, err = cronTrigger.NextFireTime(prev) + if err != nil { + return "", err + } + // fmt.Println(time.Unix(prev, 0).Format(time.UnixDate)) + } + return time.Unix(prev, 0).Format(time.UnixDate), nil +} diff --git a/quartz/queue.go b/quartz/queue.go new file mode 100644 index 0000000..1c758ce --- /dev/null +++ b/quartz/queue.go @@ -0,0 +1,51 @@ +package quartz + +import "container/heap" + +// PriorityQueue Item +type Item struct { + Job Job + Trigger Trigger + priority int64 // item priority by next run time + index int // maintained by the heap.Interface methods +} + +// implements heap.Interface +type PriorityQueue []*Item + +func (pq PriorityQueue) Len() int { return len(pq) } + +func (pq PriorityQueue) Less(i, j int) bool { + return pq[i].priority < pq[j].priority +} + +func (pq PriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] + pq[i].index = i + pq[j].index = j +} + +func (pq *PriorityQueue) Push(x interface{}) { + n := len(*pq) + item := x.(*Item) + item.index = n + *pq = append(*pq, item) +} + +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + item.index = -1 // for safety + *pq = old[0 : n-1] + return item +} + +func (pq *PriorityQueue) Head() *Item { + return (*pq)[0] +} + +func (pq *PriorityQueue) Update(item *Item, sleepTime int64) { + item.priority = sleepTime + heap.Fix(pq, item.index) +} diff --git a/quartz/scheduler.go b/quartz/scheduler.go new file mode 100644 index 0000000..2a38306 --- /dev/null +++ b/quartz/scheduler.go @@ -0,0 +1,140 @@ +package quartz + +import ( + "container/heap" + "fmt" + "sync" + "time" +) + +type Job interface { + Execute() + Description() string +} + +type Scheduler interface { + Start() + ScheduleJob(job Job, trigger Trigger) error + Stop() +} + +//implements quartz.Scheduler interface +type StdScheduler struct { + sync.Mutex + Queue *PriorityQueue + interrupt chan interface{} + exit chan interface{} + feeder chan *Item +} + +func NewStdScheduler() *StdScheduler { + return &StdScheduler{ + Queue: &PriorityQueue{}, + interrupt: make(chan interface{}), + exit: nil, + feeder: make(chan *Item)} +} + +func (sched *StdScheduler) ScheduleJob(job Job, trigger Trigger) error { + nextRunTime, err := trigger.NextFireTime(time.Now().UnixNano()) + if err == nil { + sched.feeder <- &Item{ + job, + trigger, + nextRunTime, + 0} + return nil + } + return err +} + +func (sched *StdScheduler) Start() { + //reset exit channel + sched.exit = make(chan interface{}) + //start feed reader + go sched.startFeedReader() + //start scheduler execution loop + go sched.startExecutionLoop() +} + +func (sched *StdScheduler) Stop() { + fmt.Println("Closing scheduler") + close(sched.exit) +} + +func (sched *StdScheduler) startExecutionLoop() { + for { + if sched.queueLen() == 0 { + select { + case <-sched.interrupt: + case <-sched.exit: + return + } + } else { + tick := sched.calculateNextTick() + select { + case <-tick: + sched.executeAndReschedule() + case <-sched.interrupt: + continue + case <-sched.exit: + fmt.Println("Exit execution loop") + return + } + } + } +} + +func (sched *StdScheduler) queueLen() int { + sched.Lock() + defer sched.Unlock() + return sched.Queue.Len() +} + +func (sched *StdScheduler) calculateNextTick() <-chan time.Time { + sched.Lock() + ts := sched.Queue.Head().priority + sched.Unlock() + return time.After(time.Duration(parkTime(ts))) +} + +func (sched *StdScheduler) executeAndReschedule() { + //fetch item + sched.Lock() + item := heap.Pop(sched.Queue).(*Item) + sched.Unlock() + //execute Job + go item.Job.Execute() + //reschedule Job + nextRunTime, err := item.Trigger.NextFireTime(item.priority) + if err == nil { + item.priority = nextRunTime + sched.feeder <- item + } +} + +func (sched *StdScheduler) startFeedReader() { + for { + select { + case item := <-sched.feeder: + sched.Lock() + heap.Push(sched.Queue, item) + select { + case sched.interrupt <- struct{}{}: + default: + } + sched.Unlock() + case <-sched.exit: + fmt.Println("Exit feed reader") + return + } + } +} + +func parkTime(ts int64) int64 { + now := time.Now().UnixNano() + if ts > now { + return ts - now + } + return 0 +} diff --git a/quartz/trigger.go b/quartz/trigger.go new file mode 100644 index 0000000..62c3f7f --- /dev/null +++ b/quartz/trigger.go @@ -0,0 +1,57 @@ +package quartz + +import ( + "errors" + "fmt" + "time" +) + +type Trigger interface { + NextFireTime(prev int64) (int64, error) + Description() string +} + +type SimpleTrigger struct { + Interval time.Duration +} + +//Simple trigger to reschedule Job by Duration interval +func NewSimpleTrigger(interval time.Duration) *SimpleTrigger { + return &SimpleTrigger{interval} +} + +func (st *SimpleTrigger) NextFireTime(prev int64) (int64, error) { + next := prev + st.Interval.Nanoseconds() + return next, nil +} + +func (st *SimpleTrigger) Description() string { + return fmt.Sprintf("SimpleTrigger with interval %d", st.Interval) +} + +type RunOnceTrigger struct { + Delay time.Duration + expired bool +} + +//New run once trigger with delay +func NewRunOnceTrigger(delay time.Duration) *RunOnceTrigger { + return &RunOnceTrigger{delay, false} +} + +func (st *RunOnceTrigger) NextFireTime(prev int64) (int64, error) { + if !st.expired { + next := prev + st.Delay.Nanoseconds() + st.expired = true + return next, nil + } + return 0, errors.New("RunOnce trigger is expired") +} + +func (st *RunOnceTrigger) Description() string { + status := "valid" + if st.expired { + status = "expired" + } + return fmt.Sprintf("RunOnceTrigger (%s)", status) +} diff --git a/quartz/util.go b/quartz/util.go new file mode 100644 index 0000000..e7018a3 --- /dev/null +++ b/quartz/util.go @@ -0,0 +1,118 @@ +package quartz + +import ( + "fmt" + "strconv" +) + +func indexes(search []string, target []string) []int { + searchIndexes := make([]int, 0, len(search)) + for _, a := range search { + searchIndexes = append(searchIndexes, intVal(target, a)) + } + return searchIndexes +} + +func sliceAtoi(sa []string) ([]int, error) { + si := make([]int, 0, len(sa)) + for _, a := range sa { + i, err := strconv.Atoi(a) + if err != nil { + return si, err + } + si = append(si, i) + } + return si, nil +} + +func fillRange(from int, to int) ([]int, error) { + if to < from { + return nil, cronError("fillRange") + } + len := (to - from) + 1 + arr := make([]int, len) + for i, j := from, 0; i <= to; i, j = i+1, j+1 { + arr[j] = i + } + return arr, nil +} + +func fillStep(from int, step int, max int) ([]int, error) { + if max < from { + return nil, cronError("fillStep") + } + len := ((max - from) / step) + 1 + arr := make([]int, len) + for i, j := from, 0; i <= max; i, j = i+step, j+1 { + arr[j] = i + } + return arr, nil +} + +func normalize(field string, tr []string) int { + i, err := strconv.Atoi(field) + if err == nil { + return i + } + return intVal(tr, field) +} + +func inScope(i int, min int, max int) bool { + if i >= min && i <= max { + return true + } + return false +} + +func cronError(cause string) error { + return fmt.Errorf("Invalid cron expression: %s", cause) +} + +// Align single digit values for time.UnixDate format +func alignDigit(next int, prefix string) string { + if next < 10 { + return prefix + strconv.Itoa(next) + } + return strconv.Itoa(next) +} + +func step(prev int, next int, max int) int { + diff := next - prev + if diff < 0 { + return diff + max + } + return diff +} + +func intVal(target []string, search string) int { + for i, v := range target { + if v == search { + return i + } + } + return -1 //TODO: return error +} + +// Unsafe strconv.Atoi +func atoi(str string) int { + i, _ := strconv.Atoi(str) + return i +} + +func maxDays(month int, year int) int { + if month == 2 && isLeapYear(year) { + return 29 + } + return daysInMonth[month] +} + +func isLeapYear(year int) bool { + if year%4 != 0 { + return false + } else if year%100 != 0 { + return true + } else if year%400 != 0 { + return false + } + return true +}