| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913 | 
							- /*
 
-  * Public Domain Software
 
-  *
 
-  * I (Matthias Ladkau) am the author of the source code in this file.
 
-  * I have placed the source code in this file in the public domain.
 
-  *
 
-  * For further information see: http://creativecommons.org/publicdomain/zero/1.0/
 
-  */
 
- /*
 
- Package timeutil contains common function for time related operations.
 
- */
 
- package timeutil
 
- import (
 
- 	"bytes"
 
- 	"fmt"
 
- 	"sort"
 
- 	"strconv"
 
- 	"strings"
 
- 	"sync"
 
- 	"time"
 
- 	"devt.de/krotik/common/errorutil"
 
- )
 
- /*
 
- Cron is an object which implements cron-like functionality. It can be
 
- used to schedule jobs at certain time intervals based on local time.
 
- Each Cron object runs a single scheduling thread. Very time consuming
 
- tasks which are triggered by this object are expected to run in a
 
- separate thread otherwise other registered tasks might not be
 
- triggered on time.
 
- Time can be speed up for testing purposes by changing the NowFunc and Tick
 
- properties. See NewTestingCron for details.
 
- Example code:
 
- 	c := NewCron()
 
- 	c.Register("0 0 12 1 * *", func() {
 
- 		// Do something at the beginning of hour 12:00 on 1st of every month
 
- 	})
 
- 	c.Start() // Start cron thread
 
- 	...
 
- 	c.Stop() // Shutdown cron thread
 
- */
 
- type Cron struct {
 
- 	newNowFunc func() time.Time     // Function to get the current local time
 
- 	NowFunc    func() time.Time     // Function to get the current local time
 
- 	Tick       time.Duration        // Cron check interval
 
- 	cronLock   *sync.Mutex          // Lock for data operations
 
- 	handlerMap map[string][]func()  // Map of spec to handler functions
 
- 	specMap    map[string]*CronSpec // Map of spec to spec object
 
- 	stopChan   chan bool            // Channel for the kill command
 
- }
 
- /*
 
- NewCron creates a new Cron object.
 
- */
 
- func NewCron() *Cron {
 
- 	return &Cron{
 
- 		time.Now,
 
- 		time.Now,
 
- 		time.Second * 1, // Cron check interval is a second by default
 
- 		&sync.Mutex{},
 
- 		make(map[string][]func()),
 
- 		make(map[string]*CronSpec),
 
- 		nil,
 
- 	}
 
- }
 
- /*
 
- run is the actual cron thread.
 
- */
 
- func (c *Cron) run() {
 
- 	t := c.NowFunc()
 
- Mainloop:
 
- 	for {
 
- 		select {
 
- 		case <-c.stopChan:
 
- 			break Mainloop
 
- 		case <-time.After(c.Tick):
 
- 			break
 
- 		}
 
- 		c.cronLock.Lock()
 
- 		for specString, cs := range c.specMap {
 
- 			if cs.MatchesTime(t) {
 
- 				// Execute all handlers if the current time matches a cron spec
 
- 				for _, handler := range c.handlerMap[specString] {
 
- 					handler()
 
- 				}
 
- 			}
 
- 		}
 
- 		c.cronLock.Unlock()
 
- 		if t = c.newNowFunc(); t == TestingEndOfTime {
 
- 			// We are a testing cron and stop after the given testing time range
 
- 			// has passed
 
- 			c.cronLock.Lock()
 
- 			close(c.stopChan)
 
- 			c.stopChan = nil
 
- 			c.cronLock.Unlock()
 
- 			return
 
- 		}
 
- 	}
 
- 	c.stopChan <- true
 
- }
 
- /*
 
- Start starts the cron thread. Start is a NOP if cron is already running.
 
- */
 
- func (c *Cron) Start() {
 
- 	c.cronLock.Lock()
 
- 	defer c.cronLock.Unlock()
 
- 	if c.stopChan == nil {
 
- 		c.stopChan = make(chan bool)
 
- 		go c.run()
 
- 	}
 
- }
 
- /*
 
- Stop stops the cron thread. Stop is a NOP if cron is not running.
 
- */
 
- func (c *Cron) Stop() {
 
- 	c.cronLock.Lock()
 
- 	defer c.cronLock.Unlock()
 
- 	if c.stopChan != nil {
 
- 		// Do the closing handshake
 
- 		c.stopChan <- true
 
- 		<-c.stopChan
 
- 		// Dispose of the stop channel
 
- 		close(c.stopChan)
 
- 		c.stopChan = nil
 
- 	}
 
- }
 
- /*
 
- Register registers a new handler to be called every interval, defined
 
- by the given spec.
 
- */
 
- func (c *Cron) Register(specString string, handler func()) error {
 
- 	spec, err := NewCronSpec(specString)
 
- 	if err == nil {
 
- 		c.RegisterSpec(spec, handler)
 
- 	}
 
- 	return err
 
- }
 
- /*
 
- RegisterSpec registers a new handler to be called every interval, defined
 
- by the given spec.
 
- */
 
- func (c *Cron) RegisterSpec(spec *CronSpec, handler func()) {
 
- 	specString := spec.SpecString()
 
- 	c.cronLock.Lock()
 
- 	defer c.cronLock.Unlock()
 
- 	if _, ok := c.specMap[spec.SpecString()]; !ok {
 
- 		c.specMap[specString] = spec
 
- 	}
 
- 	handlers, _ := c.handlerMap[specString]
 
- 	c.handlerMap[specString] = append(handlers, handler)
 
- }
 
- /*
 
- CronSpec is a data structure which is used to specify time schedules.
 
- A CronSpec can be stated as a single text string which must have the
 
- following 6 entries separated by whitespace:
 
- 	Field	        Valid values
 
- 	-----	        ------------
 
- 	second         * or 0-59 or *%1-59
 
- 	minute         * or 0-59 or *%1-59
 
- 	hour           * or 0-23 or *%1-23
 
- 	day of month   * or 1-31 or *%1-31
 
- 	month          * or 1-12 or *%1-12
 
- 	day of week    * or 0-6 (0 is Sunday) or *%1-7
 
- Multiple values for an entry can be separated by commas e.g. 1,3,5,7.
 
- A * in any field matches all values i.e. execute every minute, every
 
- day, etc. A *%<number> in any field entry matches when the time is a
 
- multiple of <number>.
 
- Example code:
 
- 	ss, _ := NewCronSpec("0 0 12 1 * *")
 
- 	fmt.Println(ss.String())
 
- Output:
 
- 	at the beginning of hour 12:00 on 1st of every month
 
- */
 
- type CronSpec struct {
 
- 	Second     []string
 
- 	Minute     []string
 
- 	Hour       []string
 
- 	DayOfMonth []string
 
- 	Month      []string
 
- 	DayOfWeek  []string
 
- }
 
- /*
 
- NewCronSpec creates a new CronSpec from a given spec string.
 
- */
 
- func NewCronSpec(spec string) (*CronSpec, error) {
 
- 	sspec := strings.Split(spec, " ")
 
- 	if len(sspec) != 6 {
 
- 		return nil, fmt.Errorf("Cron spec must have 6 entries separated by space")
 
- 	}
 
- 	checkNumberRange := func(num, min, max int) string {
 
- 		if num < min {
 
- 			return fmt.Sprintf("must be greater or equal than %v", min)
 
- 		} else if num > max {
 
- 			return fmt.Sprintf("must be smaller or equal than %v", max)
 
- 		}
 
- 		return ""
 
- 	}
 
- 	entries := make([][]string, 6)
 
- 	for i, entry := range sspec {
 
- 		field := fields[i]
 
- 		vals := strings.Split(entry, ",")
 
- 	valsLoop:
 
- 		for _, val := range vals {
 
- 			// Auto convert pointless things like *%1 -> *
 
- 			if val == "*%1" || (val == "*" && len(vals) > 1) {
 
- 				val = "*"
 
- 				vals = []string{val}
 
- 				break valsLoop
 
- 			}
 
- 			if strings.HasPrefix(val, "*%") {
 
- 				var res string
 
- 				// Deal with multiple-of entries
 
- 				num, err := strconv.Atoi(val[2:])
 
- 				if err != nil {
 
- 					return nil, fmt.Errorf("Cron %v entry needs a number after '*%%'", field)
 
- 				}
 
- 				// Check number range
 
- 				switch i {
 
- 				case 0: // Second
 
- 					res = checkNumberRange(num, 1, 59)
 
- 				case 1: // Minute
 
- 					res = checkNumberRange(num, 1, 59)
 
- 				case 2: // Hour
 
- 					res = checkNumberRange(num, 1, 23)
 
- 				case 3: // Day of month
 
- 					res = checkNumberRange(num, 1, 31)
 
- 				case 4: // Month
 
- 					res = checkNumberRange(num, 1, 12)
 
- 				case 5: // Day of week
 
- 					res = checkNumberRange(num, 1, 7)
 
- 				}
 
- 				if res != "" {
 
- 					return nil, fmt.Errorf("Cron %v entry %v", field, res)
 
- 				}
 
- 			} else if val != "*" {
 
- 				var res string
 
- 				num, err := strconv.Atoi(val)
 
- 				if err != nil {
 
- 					return nil, fmt.Errorf("Cron entries must be a number, '*' or *%% and a number")
 
- 				}
 
- 				// Check number range
 
- 				switch i {
 
- 				case 0: // Second
 
- 					res = checkNumberRange(num, 0, 59)
 
- 				case 1: // Minute
 
- 					res = checkNumberRange(num, 0, 59)
 
- 				case 2: // Hour
 
- 					res = checkNumberRange(num, 0, 23)
 
- 				case 3: // Day of month
 
- 					res = checkNumberRange(num, 1, 31)
 
- 				case 4: // Month
 
- 					res = checkNumberRange(num, 1, 12)
 
- 				case 5: // Day of week
 
- 					res = checkNumberRange(num, 0, 6)
 
- 				}
 
- 				if res != "" {
 
- 					return nil, fmt.Errorf("Cron %v entry %v", field, res)
 
- 				}
 
- 			}
 
- 		}
 
- 		entries[i] = vals
 
- 	}
 
- 	ret := &CronSpec{entries[0], entries[1], entries[2], entries[3],
 
- 		entries[4], entries[5]}
 
- 	// Sort all entries
 
- 	sort.Sort(numberStringSlice(ret.Second))
 
- 	sort.Sort(numberStringSlice(ret.Minute))
 
- 	sort.Sort(numberStringSlice(ret.Hour))
 
- 	sort.Sort(numberStringSlice(ret.DayOfMonth))
 
- 	sort.Sort(numberStringSlice(ret.Month))
 
- 	sort.Sort(numberStringSlice(ret.DayOfWeek))
 
- 	return ret, nil
 
- }
 
- /*
 
- MatchesTime checks if a given time object matches this CronSpec.
 
- */
 
- func (cs *CronSpec) MatchesTime(t time.Time) bool {
 
- 	matchItem := func(timeItem int, specItems []string) bool {
 
- 		for _, specItem := range specItems {
 
- 			if specItem == "*" {
 
- 				return true
 
- 			}
 
- 			if strings.HasPrefix(specItem, "*%") {
 
- 				interval, err := strconv.Atoi(specItem[2:])
 
- 				if err == nil && interval != 0 && timeItem%interval == 0 {
 
- 					return true
 
- 				}
 
- 			} else {
 
- 				item, err := strconv.Atoi(specItem)
 
- 				if err == nil && item == timeItem {
 
- 					return true
 
- 				}
 
- 			}
 
- 		}
 
- 		return false
 
- 	}
 
- 	return matchItem(t.Second(), cs.Second) &&
 
- 		matchItem(t.Minute(), cs.Minute) &&
 
- 		matchItem(t.Hour(), cs.Hour) &&
 
- 		matchItem(t.Day(), cs.DayOfMonth) &&
 
- 		matchItem(int(t.Month()), cs.Month) &&
 
- 		matchItem(int(t.Weekday()), cs.DayOfWeek)
 
- }
 
- /*
 
- Generate2000Examples generates matching time examples from the year 2000
 
- for this CronSpec. This function returns the first n examples starting
 
- from 01. January 2000 00:00:00.
 
- */
 
- func (cs *CronSpec) Generate2000Examples(n int) []string {
 
- 	var t time.Time
 
- 	res := make([]string, 0, n)
 
- 	// Create reference time boundaries for the year 2000
 
- 	startTime, err := time.Parse(time.RFC3339, "2000-01-01T00:00:00Z")
 
- 	errorutil.AssertOk(err)
 
- 	startNano := startTime.UnixNano()
 
- 	// Loop over all seconds of the year (2000 had 366 days)
 
- 	timerange := int64(60 * 60 * 24 * 366)
 
- 	for i := int64(0); i < timerange; i++ {
 
- 		if t = time.Unix(i, startNano).In(time.UTC); cs.MatchesTime(t) {
 
- 			res = append(res, t.String())
 
- 		}
 
- 		if len(res) > n-1 {
 
- 			break
 
- 		}
 
- 	}
 
- 	return res
 
- }
 
- /*
 
- SpecString returns the spec object as a spec string. This string can
 
- be used to construct the object again using NewCronSpec.
 
- */
 
- func (cs *CronSpec) SpecString() string {
 
- 	var res bytes.Buffer
 
- 	res.WriteString(strings.Join(cs.Second, ","))
 
- 	res.WriteString(" ")
 
- 	res.WriteString(strings.Join(cs.Minute, ","))
 
- 	res.WriteString(" ")
 
- 	res.WriteString(strings.Join(cs.Hour, ","))
 
- 	res.WriteString(" ")
 
- 	res.WriteString(strings.Join(cs.DayOfMonth, ","))
 
- 	res.WriteString(" ")
 
- 	res.WriteString(strings.Join(cs.Month, ","))
 
- 	res.WriteString(" ")
 
- 	res.WriteString(strings.Join(cs.DayOfWeek, ","))
 
- 	return res.String()
 
- }
 
- /*
 
- Constants for pretty printing
 
- */
 
- var (
 
- 	fields = []string{"second", "minute", "hour", "day of month",
 
- 		"month", "day of week"}
 
- 	days = []string{"Sunday", "Monday", "Tuesday",
 
- 		"Wednesday", "Thursday", "Friday", "Saturday"}
 
- 	months = []string{"January", "February", "March", "April",
 
- 		"May", "June", "July", "August", "September", "October",
 
- 		"November", "December"}
 
- )
 
- /*
 
- TimeString returns on which time during the day this spec will trigger.
 
- */
 
- func (cs *CronSpec) TimeString() string {
 
- 	secondString := func() []string {
 
- 		ret := make([]string, len(cs.Second))
 
- 		for i, s := range cs.Second {
 
- 			if strings.HasPrefix(s, "*%") {
 
- 				ret[i] = fmt.Sprintf("every %v second",
 
- 					ordinalNumberString(s[2:]))
 
- 			} else {
 
- 				if s == "0" {
 
- 					ret[i] = "the beginning"
 
- 				} else if s == "59" {
 
- 					ret[i] = "the end"
 
- 				} else {
 
- 					ret[i] = fmt.Sprintf("second %v", s)
 
- 				}
 
- 			}
 
- 		}
 
- 		return ret
 
- 	}
 
- 	minuteString := func() []string {
 
- 		ret := make([]string, len(cs.Minute))
 
- 		for i, m := range cs.Minute {
 
- 			if strings.HasPrefix(m, "*%") {
 
- 				ret[i] = fmt.Sprintf("every %v minute",
 
- 					ordinalNumberString(m[2:]))
 
- 			} else {
 
- 				if m == "0" {
 
- 					ret[i] = "the beginning"
 
- 				} else if m == "59" {
 
- 					ret[i] = "the end"
 
- 				} else {
 
- 					ret[i] = fmt.Sprintf("minute %v", m)
 
- 				}
 
- 			}
 
- 		}
 
- 		return ret
 
- 	}
 
- 	hourString := func() []string {
 
- 		ret := make([]string, len(cs.Hour))
 
- 		for i, h := range cs.Hour {
 
- 			if strings.HasPrefix(h, "*%") {
 
- 				ret[i] = fmt.Sprintf("every %v hour",
 
- 					ordinalNumberString(h[2:]))
 
- 			} else {
 
- 				hour, _ := strconv.Atoi(h)
 
- 				ret[i] = fmt.Sprintf("hour %02d:00", hour)
 
- 			}
 
- 		}
 
- 		return ret
 
- 	}
 
- 	if len(cs.Hour) == 1 && cs.Hour[0] == "*" {
 
- 		if len(cs.Minute) == 1 && cs.Minute[0] == "*" {
 
- 			if len(cs.Second) == 1 && cs.Second[0] == "*" {
 
- 				return "every second"
 
- 			}
 
- 			// Specs of the format [Seconds] * * ? ? ?
 
- 			return fmt.Sprintf("at %v of every minute", andJoin(secondString()))
 
- 		}
 
- 		if len(cs.Second) == 1 {
 
- 			if cs.Second[0] == "*" {
 
- 				// Specs of the format * [Minutes] * ? ? ?
 
- 				return fmt.Sprintf("every second of %v of every hour", andJoin(minuteString()))
 
- 			} else if cs.Second[0] == "0" {
 
- 				// Specs of the format 0 [Minutes] * ? ? ?
 
- 				return fmt.Sprintf("at %v of every hour", andJoin(minuteString()))
 
- 			}
 
- 		}
 
- 		// Specs of the format [Seconds] [Minutes] * ? ? ?
 
- 		return fmt.Sprintf("at %v of %v of every hour",
 
- 			andJoin(secondString()), andJoin(minuteString()))
 
- 	}
 
- 	if len(cs.Minute) == 1 && cs.Minute[0] == "*" {
 
- 		if len(cs.Second) == 1 {
 
- 			if cs.Second[0] == "*" {
 
- 				// Specs of the format * * [Hours] ? ? ?
 
- 				return fmt.Sprintf("every second of %v", andJoin(hourString()))
 
- 			} else if cs.Second[0] == "0" {
 
- 				// Specs of the format 0 * [Hours] ? ? ?
 
- 				return fmt.Sprintf("every minute of %v", andJoin(hourString()))
 
- 			}
 
- 		}
 
- 		// Specs of the format [Seconds] * [Hours] ? ? ?
 
- 		return fmt.Sprintf("at %v of every minute of %v",
 
- 			andJoin(secondString()), andJoin(hourString()))
 
- 	}
 
- 	if len(cs.Second) == 1 {
 
- 		if cs.Second[0] == "*" {
 
- 			// Specs of the format * [Minutes] [Hours] ? ? ?
 
- 			return fmt.Sprintf("every second of %v of %v", andJoin(minuteString()),
 
- 				andJoin(hourString()))
 
- 		} else if cs.Second[0] == "0" {
 
- 			// Specs of the format 0 [Minutes] [Hours] ? ? ?
 
- 			return fmt.Sprintf("at %v of %v", andJoin(minuteString()),
 
- 				andJoin(hourString()))
 
- 		}
 
- 	}
 
- 	// Specs of the format [Seconds] [Minutes] [Hours] ? ? ?
 
- 	return fmt.Sprintf("at %v of %v of %v",
 
- 		andJoin(secondString()), andJoin(minuteString()),
 
- 		andJoin(hourString()))
 
- }
 
- /*
 
- DaysString returns on which days this spec will trigger.
 
- */
 
- func (cs *CronSpec) DaysString() string {
 
- 	dayOfWeekToString := func() []string {
 
- 		ret := make([]string, len(cs.DayOfWeek))
 
- 		for i, dow := range cs.DayOfWeek {
 
- 			if strings.HasPrefix(dow, "*%") {
 
- 				ret[i] = fmt.Sprintf("every %v day of the week",
 
- 					ordinalNumberString(dow[2:]))
 
- 			} else {
 
- 				idx, _ := strconv.Atoi(dow)
 
- 				ret[i] = days[idx]
 
- 			}
 
- 		}
 
- 		return ret
 
- 	}
 
- 	dayOfMonthString := func() []string {
 
- 		ret := make([]string, len(cs.DayOfMonth))
 
- 		for i, dom := range cs.DayOfMonth {
 
- 			if strings.HasPrefix(dom, "*%") {
 
- 				// No need to write here "of the month" since a cron spec
 
- 				// addresses months specifically i.e. the month(s) will always
 
- 				// be included in every DaysString.
 
- 				ret[i] = fmt.Sprintf("every %v day",
 
- 					ordinalNumberString(dom[2:]))
 
- 			} else {
 
- 				ret[i] = ordinalNumberString(dom)
 
- 			}
 
- 		}
 
- 		return ret
 
- 	}
 
- 	monthString := func() []string {
 
- 		ret := make([]string, len(cs.Month))
 
- 		for i, m := range cs.Month {
 
- 			if strings.HasPrefix(m, "*%") {
 
- 				ret[i] = fmt.Sprintf("every %v month",
 
- 					ordinalNumberString(m[2:]))
 
- 			} else {
 
- 				idx, _ := strconv.Atoi(m)
 
- 				ret[i] = months[idx-1]
 
- 			}
 
- 		}
 
- 		return ret
 
- 	}
 
- 	if len(cs.Month) == 1 && cs.Month[0] == "*" {
 
- 		if len(cs.DayOfMonth) == 1 && cs.DayOfMonth[0] == "*" {
 
- 			if len(cs.DayOfWeek) == 1 && cs.DayOfWeek[0] == "*" {
 
- 				return "every day"
 
- 			}
 
- 			// Specs of the format ? ? ? * * [Days of week]
 
- 			return fmt.Sprintf("on %v", andJoin(dayOfWeekToString()))
 
- 		}
 
- 		if len(cs.DayOfWeek) == 1 && cs.DayOfWeek[0] == "*" {
 
- 			// Specs of the format ? ? ? [Days of month] * *
 
- 			return fmt.Sprintf("on %v of every month", andJoin(dayOfMonthString()))
 
- 		}
 
- 		// Specs of the format ? ? ? [Days of month] * [Days of week]
 
- 		return fmt.Sprintf("on %v and %v of every month",
 
- 			andJoin(dayOfWeekToString()), andJoin(dayOfMonthString()))
 
- 	}
 
- 	if len(cs.DayOfMonth) == 1 && cs.DayOfMonth[0] == "*" {
 
- 		if len(cs.DayOfWeek) == 1 && cs.DayOfWeek[0] == "*" {
 
- 			return fmt.Sprintf("in %v", andJoin(monthString()))
 
- 		}
 
- 		// Specs of the format ? ? ? * [Months] [Days of week]
 
- 		return fmt.Sprintf("on %v in %v", andJoin(dayOfWeekToString()),
 
- 			andJoin(monthString()))
 
- 	}
 
- 	if len(cs.DayOfWeek) == 1 && cs.DayOfWeek[0] == "*" {
 
- 		// Specs of the format ? ? ? [Days of month] [Months] *
 
- 		return fmt.Sprintf("on %v of %v", andJoin(dayOfMonthString()),
 
- 			andJoin(monthString()))
 
- 	}
 
- 	// Specs of the format ? ? ? [Days of month] [Months] [Days of week]
 
- 	return fmt.Sprintf("on %v and %v of %v",
 
- 		andJoin(dayOfWeekToString()), andJoin(dayOfMonthString()),
 
- 		andJoin(monthString()))
 
- }
 
- /*
 
- String returns a human readable string representing the spec.
 
- */
 
- func (cs *CronSpec) String() string {
 
- 	return fmt.Sprintf("%v %v", cs.TimeString(), cs.DaysString())
 
- }
 
- // Testing functions
 
- // =================
 
- /*
 
- NewTestingCronMonth creates a new test Cron object which goes through a full month.
 
- */
 
- func NewTestingCronMonth() *Cron {
 
- 	startTime, _ := time.Parse(time.RFC3339, "2000-01-01T00:00:00Z")
 
- 	endTime, _ := time.Parse(time.RFC3339, "2000-01-31T23:59:59Z")
 
- 	return NewTestingCron(startTime, endTime)
 
- }
 
- /*
 
- NewTestingCronWeek creates a new test Cron object which goes through a full week.
 
- */
 
- func NewTestingCronWeek() *Cron {
 
- 	startTime, _ := time.Parse(time.RFC3339, "2000-01-01T00:00:00Z")
 
- 	endTime, _ := time.Parse(time.RFC3339, "2000-01-07T23:59:59Z")
 
- 	return NewTestingCron(startTime, endTime)
 
- }
 
- /*
 
- NewTestingCronDay creates a new test Cron object which goes through a full day.
 
- */
 
- func NewTestingCronDay() *Cron {
 
- 	startTime, _ := time.Parse(time.RFC3339, "2000-01-01T00:00:00Z")
 
- 	endTime, _ := time.Parse(time.RFC3339, "2000-01-01T23:59:59Z")
 
- 	return NewTestingCron(startTime, endTime)
 
- }
 
- /*
 
- NewTestingCron creates a new Cron object which can be used for testing.
 
- When started it goes through all seconds of the the given time range in
 
- a fraction of the time (seconds are instantly increased). All handler functions
 
- are still called the exact number of times as if they would be running
 
- during the given time range in normal time. Use the NowFunc of the cron
 
- object to get the current testing time - the testing time will always
 
- return the same time unless advanced by the cron thread.
 
- Example code:
 
- 	c = NewTestingCronDay()
 
- 	c.Register("0 12 12,8 * * *", func() {
 
- 		// Do something at minute 12 of hour 08:00 and hour 12:00 every day
 
- 	})
 
- 	c.Start()
 
- 	WaitTestingCron(c)
 
- */
 
- func NewTestingCron(startTime, endTime time.Time) *Cron {
 
- 	tn, _ := NewTestingNow(startTime, endTime)
 
- 	ret := NewCron()
 
- 	ret.newNowFunc = tn.NewNow
 
- 	ret.NowFunc = tn.Now
 
- 	ret.Tick = 0 // A normal second is instantly increased
 
- 	return ret
 
- }
 
- /*
 
- WaitTestingCron waits for a testing cron object to end. No need to call
 
- stop afterwards.
 
- */
 
- func WaitTestingCron(c *Cron) {
 
- 	<-c.stopChan
 
- }
 
- /*
 
- TestingEndOfTime is a special time returned by TestingNow objects to indicate
 
- that the end time has been reached.
 
- */
 
- var TestingEndOfTime = time.Time{}
 
- /*
 
- TestingNow is a testing object which can provide a specialized Now function
 
- which runs from a given start to a given end time.
 
- */
 
- type TestingNow struct {
 
- 	start time.Time
 
- 	end   time.Time
 
- 	tick  int64
 
- }
 
- /*
 
- NewTestingNow creates a new TestingNow object with a given start and end time.
 
- */
 
- func NewTestingNow(start, end time.Time) (*TestingNow, error) {
 
- 	if !end.After(start) {
 
- 		return nil, fmt.Errorf("End time %v is not after start time %v", end, start)
 
- 	}
 
- 	return &TestingNow{start, end, 0}, nil
 
- }
 
- /*
 
- Now returns the current testing time.
 
- */
 
- func (tn *TestingNow) Now() time.Time {
 
- 	return tn.testingTime(false)
 
- }
 
- /*
 
- NewNow returns the current testing time and advances the clock.
 
- */
 
- func (tn *TestingNow) NewNow() time.Time {
 
- 	return tn.testingTime(true)
 
- }
 
- /*
 
- testingTime returns the current testing time and optionally advances the clock.
 
- */
 
- func (tn *TestingNow) testingTime(advance bool) time.Time {
 
- 	if advance {
 
- 		tn.tick++
 
- 	}
 
- 	if newTime := time.Unix(tn.tick, tn.start.UnixNano()); newTime.Before(tn.end) {
 
- 		return newTime.UTC()
 
- 	}
 
- 	return TestingEndOfTime
 
- }
 
- // Helper functions
 
- // ================
 
- func andJoin(ss []string) string {
 
- 	var buf bytes.Buffer
 
- 	sslen := len(ss) - 1
 
- 	for i, s := range ss {
 
- 		buf.WriteString(s)
 
- 		if i == sslen-1 {
 
- 			buf.WriteString(" and ")
 
- 		} else if i < sslen {
 
- 			buf.WriteString(", ")
 
- 		}
 
- 	}
 
- 	return buf.String()
 
- }
 
- /*
 
- ordinalNumber produces an ordinal number string from a given number
 
- string (e.g. 1 -> 1st, 2 -> 2nd).
 
- */
 
- func ordinalNumberString(number string) string {
 
- 	var suffix = "th"
 
- 	if strings.HasSuffix(number, "1") && !strings.HasSuffix(number, "11") {
 
- 		suffix = "st"
 
- 	} else if strings.HasSuffix(number, "2") && !strings.HasSuffix(number, "12") {
 
- 		suffix = "nd"
 
- 	} else if strings.HasSuffix(number, "3") && !strings.HasSuffix(number, "13") {
 
- 		suffix = "rd"
 
- 	}
 
- 	return number + suffix
 
- }
 
- // Comparator for number strings
 
- type numberStringSlice []string
 
- func (p numberStringSlice) Len() int { return len(p) }
 
- func (p numberStringSlice) Less(i, j int) bool {
 
- 	val1 := p[i]
 
- 	val2 := p[j]
 
- 	convert := func(val string) string {
 
- 		if strings.HasPrefix(val, "*%") {
 
- 			if num, err := strconv.Atoi(val[2:]); err == nil && num < 10 {
 
- 				return fmt.Sprintf("*%%0%v", num)
 
- 			}
 
- 		} else if val != "*" {
 
- 			if num, err := strconv.Atoi(val); err == nil && num < 10 {
 
- 				return fmt.Sprintf("0%v", num)
 
- 			}
 
- 		}
 
- 		return val
 
- 	}
 
- 	return convert(val1) < convert(val2)
 
- }
 
- func (p numberStringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
 
 
  |