go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/cron/parse_schedule.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package cron
     9  
    10  import (
    11  	"errors"
    12  	"fmt"
    13  	"sort"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  )
    18  
    19  /*
    20  ParseSchedule parses a cron formatted string into a schedule.
    21  
    22  The string must be at least 5 components, whitespace separated.
    23  If the string has 5 components a 0 will be prepended for the seconds component, and a * appended for the year component.
    24  If the string has 6 components a * appended for the year component.
    25  
    26  The components are (in short form / 5 component):
    27  
    28  	(minutes) (hours) (day of month) (month) (day of week)
    29  
    30  The components are (in medium form / 6 component):
    31  
    32  	(seconds) (hours) (day of month) (month) (day of week)
    33  
    34  The components are (in long form / 7 component):
    35  
    36  	(seconds) (minutes) (hours) (day of month) (month) (day of week) (year)
    37  
    38  The full list of possible field values:
    39  
    40  	Field name     Mandatory?   Allowed values    Allowed special characters
    41  	----------     ----------   --------------    --------------------------
    42  	Seconds        No           0-59              * / , -
    43  	Minutes        Yes          0-59              * / , -
    44  	Hours          Yes          0-23              * / , -
    45  	Day of month   Yes          1-31              * / , - L W
    46  	Month          Yes          1-12 or JAN-DEC   * / , -
    47  	Day of week    Yes          0-6 or SUN-SAT    * / , - L #
    48  	Year           No           1970–2099         * / , -
    49  
    50  You can also use shorthands:
    51  
    52  	"@yearly" is equivalent to "0 0 0 1 1 * *"
    53  	"@monthly" is equivalent to "0 0 0 1 * * *"
    54  	"@weekly" is equivalent to "0 0 0 * * 0 *"
    55  	"@daily" is equivalent to "0 0 0 * * * *"
    56  	"@hourly" is equivalent to "0 0 * * * * *"
    57  	"@every 500ms" is equivalent to "cron.Every(500 * time.Millisecond)""
    58  	"@immediately-then @every 500ms" is equivalent to "cron.Immediately().Then(cron.Every(500*time.Millisecond))"
    59  	"@once-at 2021-06-05 13:04" is "cron.OnceAtUTC(time.Date(...))"
    60  	"@never" is equivalent to an unset schedule (i.e., only on demand) to avoid defaults
    61  */
    62  func ParseSchedule(cronString string) (schedule Schedule, err error) {
    63  	// remove leading and trailing whitespace, including newlines and tabs
    64  	cronString = strings.TrimSpace(cronString)
    65  
    66  	// check for "@never"
    67  	if cronString == StringScheduleNever {
    68  		schedule = Never()
    69  		return
    70  	}
    71  
    72  	// check for "@immediately"
    73  	if cronString == StringScheduleImmediately {
    74  		schedule = Immediately()
    75  		return
    76  	}
    77  
    78  	// check for "@once-at"
    79  	if strings.HasPrefix(cronString, StringScheduleOnceAt) {
    80  		cronString = strings.TrimPrefix(cronString, StringScheduleOnceAt)
    81  		cronString = strings.TrimSpace(cronString)
    82  
    83  		onceAtUTC, errOnceAtUTC := time.Parse(time.RFC3339, cronString)
    84  		if errOnceAtUTC != nil {
    85  			err = fmt.Errorf("%w: %v", ErrStringScheduleInvalid, errOnceAtUTC)
    86  			return
    87  		}
    88  		schedule = OnceAt(onceAtUTC.UTC())
    89  		return
    90  	}
    91  
    92  	// here we assume the rest of the schedule is either
    93  	// 	@immediately-then ...
    94  	//  @delay ...
    95  	// 	@immediately-then @delay ...
    96  	// etc.
    97  
    98  	// pull the @immediately-then off the beginning of
    99  	// the schedule if it's present
   100  	var immediately bool
   101  	if strings.HasPrefix(cronString, StringScheduleImmediatelyThen) {
   102  		immediately = true
   103  		cronString = strings.TrimPrefix(cronString, StringScheduleImmediatelyThen)
   104  		cronString = strings.TrimSpace(cronString)
   105  	}
   106  
   107  	var delay time.Duration
   108  	if strings.HasPrefix(cronString, StringScheduleDelay) {
   109  		cronString = strings.TrimPrefix(cronString, StringScheduleDelay)
   110  		cronString = strings.TrimSpace(cronString)
   111  
   112  		remainingFields := strings.Fields(cronString)
   113  		if len(remainingFields) < 2 {
   114  			err = fmt.Errorf("@delay must be in the form `@delay <DURATION> <THEN>`: %w", ErrStringScheduleInvalid)
   115  			return
   116  		}
   117  		durationPart := remainingFields[0]
   118  		cronString = strings.TrimPrefix(cronString, durationPart)
   119  		cronString = strings.TrimSpace(cronString)
   120  		delay, err = time.ParseDuration(durationPart)
   121  		if err != nil {
   122  			err = fmt.Errorf("%w: %v", ErrStringScheduleInvalid, err)
   123  			return
   124  		}
   125  	}
   126  
   127  	// DEFER / FINALLY BLOCK
   128  	// we add the optional immediately or delay
   129  	// directives into the final schedule.
   130  	defer func() {
   131  		if schedule != nil {
   132  			if delay > 0 {
   133  				schedule = Delay(delay, schedule)
   134  			}
   135  			if immediately {
   136  				schedule = Immediately().Then(schedule)
   137  			}
   138  		}
   139  	}()
   140  
   141  	// at this point, the rest of the cron string is considered
   142  	// a complete, single representation of a schedule
   143  
   144  	// handle the specific @every string representation
   145  	if strings.HasPrefix(cronString, StringScheduleEvery) {
   146  		cronString = strings.TrimPrefix(cronString, StringScheduleEvery)
   147  		cronString = strings.TrimSpace(cronString)
   148  		duration, durationErr := time.ParseDuration(cronString)
   149  		if durationErr != nil {
   150  			err = fmt.Errorf("%w: %v", ErrStringScheduleInvalid, durationErr)
   151  			return
   152  		}
   153  		schedule = Every(duration)
   154  		return
   155  	}
   156  
   157  	// we now assume the string is a 'cron-like' string
   158  	// that is in the form "* * * * *" etc.
   159  
   160  	// check for shorthands, replace the shorthand with a proper cron-like string
   161  	if shorthand, ok := StringScheduleShorthands[cronString]; ok {
   162  		cronString = shorthand
   163  	}
   164  
   165  	parts := strings.Fields(cronString)
   166  	if len(parts) < 5 || len(parts) > 7 {
   167  		return nil, fmt.Errorf("%w: %v; provided string; %s", ErrStringScheduleInvalid, ErrStringScheduleComponents, cronString)
   168  	}
   169  	// fill in optional components
   170  	if len(parts) == 5 {
   171  		parts = append([]string{"0"}, parts...)
   172  		parts = append(parts, "*")
   173  	} else if len(parts) == 6 {
   174  		parts = append(parts, "*")
   175  	}
   176  
   177  	seconds, err := parsePart(parts[0], parseInt, below(60))
   178  	if err != nil {
   179  		return nil, fmt.Errorf("provided string; seconds invalid; %v: %w", ErrStringScheduleInvalid, err)
   180  	}
   181  
   182  	minutes, err := parsePart(parts[1], parseInt, below(60))
   183  	if err != nil {
   184  		return nil, fmt.Errorf("provided string; minutes invalid; %v: %w", ErrStringScheduleInvalid, err)
   185  	}
   186  
   187  	hours, err := parsePart(parts[2], parseInt, below(24))
   188  	if err != nil {
   189  		return nil, fmt.Errorf("provided string; hours invalid; %v: %w", ErrStringScheduleInvalid, err)
   190  	}
   191  
   192  	days, err := parsePart(parts[3], parseInt, between(1, 32))
   193  	if err != nil {
   194  		return nil, fmt.Errorf("provided string; days invalid; %v: %w", ErrStringScheduleInvalid, err)
   195  	}
   196  
   197  	months, err := parsePart(parts[4], parseMonth, between(1, 13))
   198  	if err != nil {
   199  		return nil, fmt.Errorf("provided string; months invalid; %v: %w", ErrStringScheduleInvalid, err)
   200  	}
   201  
   202  	daysOfWeek, err := parsePart(parts[5], parseDayOfWeek, between(0, 7))
   203  	if err != nil {
   204  		return nil, fmt.Errorf("provided string; days of week invalid; %v: %w", ErrStringScheduleInvalid, err)
   205  	}
   206  
   207  	years, err := parsePart(parts[6], parseInt, between(1970, 2100))
   208  	if err != nil {
   209  		return nil, fmt.Errorf("provided string; years invalid; %v: %w", ErrStringScheduleInvalid, err)
   210  	}
   211  
   212  	schedule = &StringSchedule{
   213  		Original:    cronString,
   214  		Seconds:     seconds,
   215  		Minutes:     minutes,
   216  		Hours:       hours,
   217  		DaysOfMonth: days,
   218  		Months:      months,
   219  		DaysOfWeek:  daysOfWeek,
   220  		Years:       years,
   221  	}
   222  	return
   223  }
   224  
   225  // Error Constants
   226  var (
   227  	ErrStringScheduleInvalid         = errors.New("cron: schedule string invalid")
   228  	ErrStringScheduleComponents      = errors.New("cron: must have at least (5) components space delimited; ex: '0 0 * * * * *'")
   229  	ErrStringScheduleValueOutOfRange = errors.New("cron: string schedule part out of range")
   230  	ErrStringScheduleInvalidRange    = errors.New("cron: range (from-to) invalid")
   231  )
   232  
   233  // String schedule constants
   234  const (
   235  	StringScheduleImmediately     = "@immediately"
   236  	StringScheduleDelay           = "@delay"
   237  	StringScheduleImmediatelyThen = "@immediately-then"
   238  	StringScheduleEvery           = "@every"
   239  	StringScheduleOnceAt          = "@once-at"
   240  	StringScheduleNever           = "@never"
   241  )
   242  
   243  // String schedule shorthands labels
   244  const (
   245  	StringScheduleShorthandAnnually = "@annually"
   246  	StringScheduleShorthandYearly   = "@yearly"
   247  	StringScheduleShorthandMonthly  = "@monthly"
   248  	StringScheduleShorthandWeekly   = "@weekly"
   249  	StringScheduleShorthandDaily    = "@daily"
   250  	StringScheduleShorthandHourly   = "@hourly"
   251  )
   252  
   253  // String schedule shorthand values
   254  var (
   255  	StringScheduleShorthands = map[string]string{
   256  		StringScheduleShorthandAnnually: "0 0 0 1 1 * *",
   257  		StringScheduleShorthandYearly:   "0 0 0 1 1 * *",
   258  		StringScheduleShorthandMonthly:  "0 0 0 1 * * *",
   259  		StringScheduleShorthandDaily:    "0 0 0 * * * *",
   260  		StringScheduleShorthandHourly:   "0 0 * * * * *",
   261  	}
   262  )
   263  
   264  // Interface assertions.
   265  var (
   266  	_ Schedule     = (*StringSchedule)(nil)
   267  	_ fmt.Stringer = (*StringSchedule)(nil)
   268  )
   269  
   270  // StringSchedule is a schedule generated from a cron string.
   271  type StringSchedule struct {
   272  	Original string
   273  
   274  	Seconds     []int
   275  	Minutes     []int
   276  	Hours       []int
   277  	DaysOfMonth []int
   278  	Months      []int
   279  	DaysOfWeek  []int
   280  	Years       []int
   281  }
   282  
   283  // String returns the original string schedule.
   284  func (ss *StringSchedule) String() string {
   285  	return ss.Original
   286  }
   287  
   288  // FullString returns a fully formed string representation of the schedule's components.
   289  // It shows fields as expanded.
   290  func (ss *StringSchedule) FullString() string {
   291  	fields := []string{
   292  		csvOfInts(ss.Seconds, "*"),
   293  		csvOfInts(ss.Minutes, "*"),
   294  		csvOfInts(ss.Hours, "*"),
   295  		csvOfInts(ss.DaysOfMonth, "*"),
   296  		csvOfInts(ss.Months, "*"),
   297  		csvOfInts(ss.DaysOfWeek, "*"),
   298  		csvOfInts(ss.Years, "*"),
   299  	}
   300  	return strings.Join(fields, " ")
   301  }
   302  
   303  // Next implements cron.Schedule.
   304  func (ss *StringSchedule) Next(after time.Time) time.Time {
   305  	working := after
   306  	if after.IsZero() {
   307  		working = Now()
   308  	}
   309  	original := working
   310  
   311  	if len(ss.Years) > 0 {
   312  		for _, year := range ss.Years {
   313  			if year >= working.Year() {
   314  				working = advanceYearTo(working, year)
   315  				break
   316  			}
   317  		}
   318  	}
   319  
   320  	if len(ss.Months) > 0 {
   321  		var didSet bool
   322  		for _, month := range ss.Months {
   323  			if time.Month(month) == working.Month() && working.After(original) {
   324  				didSet = true
   325  				break
   326  			}
   327  			if time.Month(month) > working.Month() {
   328  				working = advanceMonthTo(working, time.Month(month))
   329  				didSet = true
   330  				break
   331  			}
   332  		}
   333  		if !didSet {
   334  			working = advanceYear(working)
   335  			for _, month := range ss.Months {
   336  				if time.Month(month) >= working.Month() {
   337  					working = advanceMonthTo(working, time.Month(month))
   338  					break
   339  				}
   340  			}
   341  		}
   342  	}
   343  
   344  	if len(ss.DaysOfMonth) > 0 {
   345  		var didSet bool
   346  		for _, day := range ss.DaysOfMonth {
   347  			if day == working.Day() && working.After(original) {
   348  				didSet = true
   349  				break
   350  			}
   351  			if day > working.Day() {
   352  				working = advanceDayTo(working, day)
   353  				didSet = true
   354  				break
   355  			}
   356  		}
   357  		if !didSet {
   358  			working = advanceMonth(working)
   359  			for _, day := range ss.DaysOfMonth {
   360  				if day >= working.Day() {
   361  					working = advanceDayTo(working, day)
   362  					break
   363  				}
   364  			}
   365  		}
   366  	}
   367  
   368  	if len(ss.DaysOfWeek) > 0 {
   369  		var didSet bool
   370  		for _, dow := range ss.DaysOfWeek {
   371  			if dow == int(working.Weekday()) && working.After(original) {
   372  				didSet = true
   373  				break
   374  			}
   375  			if dow > int(working.Weekday()) {
   376  				working = advanceDayBy(working, (dow - int(working.Weekday())))
   377  				didSet = true
   378  				break
   379  			}
   380  		}
   381  		if !didSet {
   382  			working = advanceToNextSunday(working)
   383  			for _, dow := range ss.DaysOfWeek {
   384  				if dow >= int(working.Weekday()) {
   385  					working = advanceDayBy(working, (dow - int(working.Weekday())))
   386  					break
   387  				}
   388  			}
   389  		}
   390  	}
   391  
   392  	if len(ss.Hours) > 0 {
   393  		var didSet bool
   394  		for _, hour := range ss.Hours {
   395  			if hour == working.Hour() && working.After(original) {
   396  				didSet = true
   397  				break
   398  			}
   399  			if hour > working.Hour() {
   400  				working = advanceHourTo(working, hour)
   401  				didSet = true
   402  				break
   403  			}
   404  		}
   405  		if !didSet {
   406  			working = advanceDay(working)
   407  			for _, hour := range ss.Hours {
   408  				if hour >= working.Hour() {
   409  					working = advanceHourTo(working, hour)
   410  					break
   411  				}
   412  			}
   413  		}
   414  	}
   415  
   416  	if len(ss.Minutes) > 0 {
   417  		var didSet bool
   418  		for _, minute := range ss.Minutes {
   419  			if minute == working.Minute() && working.After(original) {
   420  				didSet = true
   421  				break
   422  			}
   423  			if minute > working.Minute() {
   424  				working = advanceMinuteTo(working, minute)
   425  				didSet = true
   426  				break
   427  			}
   428  		}
   429  		if !didSet {
   430  			working = advanceHour(working)
   431  			for _, minute := range ss.Minutes {
   432  				if minute >= working.Minute() {
   433  					working = advanceMinuteTo(working, minute)
   434  					break
   435  				}
   436  			}
   437  		}
   438  	}
   439  
   440  	if len(ss.Seconds) > 0 {
   441  		var didSet bool
   442  		for _, second := range ss.Seconds {
   443  			if second == working.Second() && working.After(original) {
   444  				didSet = true
   445  				break
   446  			}
   447  			if second > working.Second() {
   448  				working = advanceSecondTo(working, second)
   449  				didSet = true
   450  				break
   451  			}
   452  		}
   453  		if !didSet {
   454  			working = advanceMinute(working)
   455  			for _, second := range ss.Hours {
   456  				if second >= working.Second() {
   457  					working = advanceSecondTo(working, second)
   458  					break
   459  				}
   460  			}
   461  		}
   462  	}
   463  
   464  	return working
   465  }
   466  
   467  func parsePart(values string, parser func(string) (int, error), validator func(int) bool) ([]int, error) {
   468  	if values == string(cronSpecialStar) {
   469  		return nil, nil
   470  	}
   471  
   472  	// check if we need to expand an "every" pattern
   473  	if strings.HasPrefix(values, cronSpecialEvery) {
   474  		return parseEvery(values, parseInt, validator)
   475  	}
   476  
   477  	components := strings.Split(values, string(cronSpecialComma))
   478  
   479  	output := map[int]bool{}
   480  	var component string
   481  	for x := 0; x < len(components); x++ {
   482  		component = components[x]
   483  		if strings.Contains(component, string(cronSpecialDash)) {
   484  			rangeValues, err := parseRange(values, parser, validator)
   485  			if err != nil {
   486  				return nil, err
   487  			}
   488  
   489  			for _, value := range rangeValues {
   490  				output[value] = true
   491  			}
   492  			continue
   493  		}
   494  
   495  		part, err := parser(component)
   496  		if err != nil {
   497  			return nil, err
   498  		}
   499  		if validator != nil && !validator(part) {
   500  			return nil, err
   501  		}
   502  		output[part] = true
   503  	}
   504  	return mapKeysToArray(output), nil
   505  }
   506  
   507  func parseEvery(values string, parser func(string) (int, error), validator func(int) bool) ([]int, error) {
   508  	every, err := parser(strings.TrimPrefix(values, "*/"))
   509  	if err != nil {
   510  		return nil, err
   511  	}
   512  	if validator != nil && !validator(every) {
   513  		return nil, ErrStringScheduleValueOutOfRange
   514  	}
   515  
   516  	var output []int
   517  	for x := 0; x < 60; x += every {
   518  		output = append(output, x)
   519  	}
   520  	return output, nil
   521  }
   522  
   523  func parseRange(values string, parser func(string) (int, error), validator func(int) bool) ([]int, error) {
   524  	parts := strings.Split(values, string(cronSpecialDash))
   525  
   526  	if len(parts) != 2 {
   527  		return nil, fmt.Errorf("invalid range: %s; %w", values, ErrStringScheduleInvalidRange)
   528  	}
   529  
   530  	from, err := parser(parts[0])
   531  	if err != nil {
   532  		return nil, err
   533  	}
   534  	to, err := parser(parts[1])
   535  	if err != nil {
   536  		return nil, err
   537  	}
   538  
   539  	if validator != nil && !validator(from) {
   540  		return nil, fmt.Errorf("invalid range from; %w", ErrStringScheduleValueOutOfRange)
   541  	}
   542  	if validator != nil && !validator(to) {
   543  		return nil, fmt.Errorf("invalid range to; %w", ErrStringScheduleValueOutOfRange)
   544  	}
   545  
   546  	if from >= to {
   547  		return nil, fmt.Errorf("invalid range; from greater than to; %w", ErrStringScheduleValueOutOfRange)
   548  	}
   549  
   550  	var output []int
   551  	for x := from; x <= to; x++ {
   552  		output = append(output, x)
   553  	}
   554  	return output, nil
   555  }
   556  
   557  func parseInt(s string) (int, error) {
   558  	return strconv.Atoi(s)
   559  }
   560  
   561  func parseMonth(s string) (int, error) {
   562  	if value, ok := validMonths[s]; ok {
   563  		return value, nil
   564  	}
   565  	value, err := strconv.Atoi(s)
   566  	if err != nil {
   567  		return 0, fmt.Errorf("%v; month not a valid integer: %w", err, ErrStringScheduleValueOutOfRange)
   568  	}
   569  	if value < 1 || value > 12 {
   570  		return 0, fmt.Errorf("month out of range (1-12): %w", ErrStringScheduleValueOutOfRange)
   571  	}
   572  	return value, nil
   573  }
   574  
   575  func parseDayOfWeek(s string) (int, error) {
   576  	if value, ok := validDaysOfWeek[s]; ok {
   577  		return value, nil
   578  	}
   579  	value, err := strconv.Atoi(s)
   580  	if err != nil {
   581  		return 0, fmt.Errorf("%v; day of week not a valid integer: %w", err, ErrStringScheduleValueOutOfRange)
   582  	}
   583  	if value < 0 || value > 6 {
   584  		return 0, fmt.Errorf("%v; day of week out of range (0-6)", ErrStringScheduleValueOutOfRange)
   585  	}
   586  	return value, nil
   587  }
   588  
   589  func below(max int) func(int) bool {
   590  	return between(0, max)
   591  }
   592  
   593  func between(min, max int) func(int) bool {
   594  	return func(value int) bool {
   595  		return value >= min && value < max
   596  	}
   597  }
   598  
   599  func mapKeysToArray(values map[int]bool) []int {
   600  	output := make([]int, len(values))
   601  	var index int
   602  	for key := range values {
   603  		output[index] = key
   604  		index++
   605  	}
   606  	sort.Ints(output)
   607  	return output
   608  }
   609  
   610  //
   611  // time helpers
   612  //
   613  
   614  func advanceYear(t time.Time) time.Time {
   615  	return time.Date(t.Year(), 1, 1, 0, 0, 0, 0, t.Location()).AddDate(1, 0, 0)
   616  }
   617  
   618  func advanceYearTo(t time.Time, year int) time.Time {
   619  	return time.Date(year, 1, 1, 0, 0, 0, 0, t.Location())
   620  }
   621  
   622  func advanceMonth(t time.Time) time.Time {
   623  	return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location()).AddDate(0, 1, 0)
   624  }
   625  
   626  func advanceMonthTo(t time.Time, month time.Month) time.Time {
   627  	return time.Date(t.Year(), month, 1, 0, 0, 0, 0, t.Location())
   628  }
   629  
   630  func advanceDayTo(t time.Time, day int) time.Time {
   631  	return time.Date(t.Year(), t.Month(), day, 0, 0, 0, 0, t.Location())
   632  }
   633  
   634  func advanceToNextSunday(t time.Time) time.Time {
   635  	daysUntilSunday := 7 - int(t.Weekday())
   636  	return t.AddDate(0, 0, daysUntilSunday)
   637  }
   638  
   639  func advanceDay(t time.Time) time.Time {
   640  	return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()).AddDate(0, 0, 1)
   641  }
   642  
   643  func advanceDayBy(t time.Time, days int) time.Time {
   644  	return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()).AddDate(0, 0, days)
   645  }
   646  
   647  func advanceHour(t time.Time) time.Time {
   648  	return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location()).Add(time.Hour)
   649  }
   650  
   651  func advanceHourTo(t time.Time, hour int) time.Time {
   652  	return time.Date(t.Year(), t.Month(), t.Day(), hour, 0, 0, 0, t.Location())
   653  }
   654  
   655  func advanceMinute(t time.Time) time.Time {
   656  	return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), 0, 0, t.Location()).Add(time.Minute)
   657  }
   658  
   659  func advanceMinuteTo(t time.Time, minute int) time.Time {
   660  	return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), minute, 0, 0, t.Location())
   661  }
   662  
   663  func advanceSecondTo(t time.Time, second int) time.Time {
   664  	return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), second, 0, t.Location())
   665  }
   666  
   667  func csvOfInts(values []int, placeholder string) string {
   668  	if len(values) == 0 {
   669  		return placeholder
   670  	}
   671  	valueStrings := make([]string, len(values))
   672  	for x := 0; x < len(values); x++ {
   673  		valueStrings[x] = strconv.Itoa(values[x])
   674  	}
   675  	return strings.Join(valueStrings, ",")
   676  }
   677  
   678  // these are special characters
   679  const (
   680  	cronSpecialComma = ',' //
   681  	cronSpecialDash  = '-'
   682  	cronSpecialStar  = '*'
   683  
   684  	// these are unused
   685  	// cronSpecialSlash = '/'
   686  	// cronSpecialQuestion = '?' // sometimes used as the startup time, sometimes as a *
   687  
   688  	// cronSpecialLast       = 'L'
   689  	// cronSpecialWeekday    = 'W' // nearest weekday to the given day of the month
   690  	// cronSpecialDayOfMonth = '#' //
   691  
   692  	cronSpecialEvery = "*/"
   693  )
   694  
   695  var (
   696  	validMonths = map[string]int{
   697  		"JAN": 1,
   698  		"FEB": 2,
   699  		"MAR": 3,
   700  		"APR": 4,
   701  		"MAY": 5,
   702  		"JUN": 6,
   703  		"JUL": 7,
   704  		"AUG": 8,
   705  		"SEP": 9,
   706  		"OCT": 10,
   707  		"NOV": 11,
   708  		"DEC": 12,
   709  	}
   710  
   711  	validDaysOfWeek = map[string]int{
   712  		"SUN": 0,
   713  		"MON": 1,
   714  		"TUE": 2,
   715  		"WED": 3,
   716  		"THU": 4,
   717  		"FRI": 5,
   718  		"SAT": 6,
   719  	}
   720  )