github.com/gogf/gf/v2@v2.7.4/os/gcron/gcron_schedule.go (about)

     1  // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the MIT License.
     4  // If a copy of the MIT was not distributed with this file,
     5  // You can obtain one at https://github.com/gogf/gf.
     6  
     7  package gcron
     8  
     9  import (
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/gogf/gf/v2/container/gtype"
    15  	"github.com/gogf/gf/v2/errors/gcode"
    16  	"github.com/gogf/gf/v2/errors/gerror"
    17  	"github.com/gogf/gf/v2/os/gtime"
    18  	"github.com/gogf/gf/v2/text/gregex"
    19  )
    20  
    21  // cronSchedule is the schedule for cron job.
    22  type cronSchedule struct {
    23  	createTimestamp int64            // Created timestamp in seconds.
    24  	everySeconds    int64            // Running interval in seconds.
    25  	pattern         string           // The raw cron pattern string that is passed in cron job creation.
    26  	ignoreSeconds   bool             // Mark the pattern is standard 5 parts crontab pattern instead 6 parts pattern.
    27  	secondMap       map[int]struct{} // Job can run in these second numbers.
    28  	minuteMap       map[int]struct{} // Job can run in these minute numbers.
    29  	hourMap         map[int]struct{} // Job can run in these hour numbers.
    30  	dayMap          map[int]struct{} // Job can run in these day numbers.
    31  	weekMap         map[int]struct{} // Job can run in these week numbers.
    32  	monthMap        map[int]struct{} // Job can run in these moth numbers.
    33  
    34  	// This field stores the timestamp that meets schedule latest.
    35  	lastMeetTimestamp *gtype.Int64
    36  
    37  	// Last timestamp number, for timestamp fix in some latency.
    38  	lastCheckTimestamp *gtype.Int64
    39  }
    40  
    41  type patternItemType int
    42  
    43  const (
    44  	patternItemTypeSecond patternItemType = iota
    45  	patternItemTypeMinute
    46  	patternItemTypeHour
    47  	patternItemTypeDay
    48  	patternItemTypeWeek
    49  	patternItemTypeMonth
    50  )
    51  
    52  const (
    53  	// regular expression for cron pattern, which contains 6 parts of time units.
    54  	regexForCron = `^([\-/\d\*,#]+)\s+([\-/\d\*,]+)\s+([\-/\d\*,]+)\s+([\-/\d\*\?,]+)\s+([\-/\d\*,A-Za-z]+)\s+([\-/\d\*\?,A-Za-z]+)$`
    55  )
    56  
    57  var (
    58  	// Predefined pattern map.
    59  	predefinedPatternMap = map[string]string{
    60  		"@yearly":   "# 0 0 1 1 *",
    61  		"@annually": "# 0 0 1 1 *",
    62  		"@monthly":  "# 0 0 1 * *",
    63  		"@weekly":   "# 0 0 * * 0",
    64  		"@daily":    "# 0 0 * * *",
    65  		"@midnight": "# 0 0 * * *",
    66  		"@hourly":   "# 0 * * * *",
    67  	}
    68  	// Short month name to its number.
    69  	monthShortNameMap = map[string]int{
    70  		"jan": 1,
    71  		"feb": 2,
    72  		"mar": 3,
    73  		"apr": 4,
    74  		"may": 5,
    75  		"jun": 6,
    76  		"jul": 7,
    77  		"aug": 8,
    78  		"sep": 9,
    79  		"oct": 10,
    80  		"nov": 11,
    81  		"dec": 12,
    82  	}
    83  	// Full month name to its number.
    84  	monthFullNameMap = map[string]int{
    85  		"january":   1,
    86  		"february":  2,
    87  		"march":     3,
    88  		"april":     4,
    89  		"may":       5,
    90  		"june":      6,
    91  		"july":      7,
    92  		"august":    8,
    93  		"september": 9,
    94  		"october":   10,
    95  		"november":  11,
    96  		"december":  12,
    97  	}
    98  	// Short week name to its number.
    99  	weekShortNameMap = map[string]int{
   100  		"sun": 0,
   101  		"mon": 1,
   102  		"tue": 2,
   103  		"wed": 3,
   104  		"thu": 4,
   105  		"fri": 5,
   106  		"sat": 6,
   107  	}
   108  	// Full week name to its number.
   109  	weekFullNameMap = map[string]int{
   110  		"sunday":    0,
   111  		"monday":    1,
   112  		"tuesday":   2,
   113  		"wednesday": 3,
   114  		"thursday":  4,
   115  		"friday":    5,
   116  		"saturday":  6,
   117  	}
   118  )
   119  
   120  // newSchedule creates and returns a schedule object for given cron pattern.
   121  func newSchedule(pattern string) (*cronSchedule, error) {
   122  	var currentTimestamp = time.Now().Unix()
   123  	// Check given `pattern` if the predefined patterns.
   124  	if match, _ := gregex.MatchString(`(@\w+)\s*(\w*)\s*`, pattern); len(match) > 0 {
   125  		key := strings.ToLower(match[1])
   126  		if v, ok := predefinedPatternMap[key]; ok {
   127  			pattern = v
   128  		} else if strings.Compare(key, "@every") == 0 {
   129  			d, err := gtime.ParseDuration(match[2])
   130  			if err != nil {
   131  				return nil, err
   132  			}
   133  			return &cronSchedule{
   134  				createTimestamp:    currentTimestamp,
   135  				everySeconds:       int64(d.Seconds()),
   136  				pattern:            pattern,
   137  				lastMeetTimestamp:  gtype.NewInt64(currentTimestamp),
   138  				lastCheckTimestamp: gtype.NewInt64(currentTimestamp),
   139  			}, nil
   140  		} else {
   141  			return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern: "%s"`, pattern)
   142  		}
   143  	}
   144  	// Handle given `pattern` as common 6 parts pattern.
   145  	match, _ := gregex.MatchString(regexForCron, pattern)
   146  	if len(match) != 7 {
   147  		return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern: "%s"`, pattern)
   148  	}
   149  	var (
   150  		err error
   151  		cs  = &cronSchedule{
   152  			createTimestamp:    currentTimestamp,
   153  			everySeconds:       0,
   154  			pattern:            pattern,
   155  			lastMeetTimestamp:  gtype.NewInt64(currentTimestamp),
   156  			lastCheckTimestamp: gtype.NewInt64(currentTimestamp),
   157  		}
   158  	)
   159  
   160  	// Second.
   161  	if match[1] == "#" {
   162  		cs.ignoreSeconds = true
   163  	} else {
   164  		cs.secondMap, err = parsePatternItem(match[1], 0, 59, false, patternItemTypeSecond)
   165  		if err != nil {
   166  			return nil, err
   167  		}
   168  	}
   169  	// Minute.
   170  	cs.minuteMap, err = parsePatternItem(match[2], 0, 59, false, patternItemTypeMinute)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  	// Hour.
   175  	cs.hourMap, err = parsePatternItem(match[3], 0, 23, false, patternItemTypeHour)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	// Day.
   180  	cs.dayMap, err = parsePatternItem(match[4], 1, 31, true, patternItemTypeDay)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  	// Month.
   185  	cs.monthMap, err = parsePatternItem(match[5], 1, 12, false, patternItemTypeMonth)
   186  	if err != nil {
   187  		return nil, err
   188  	}
   189  	// Week.
   190  	cs.weekMap, err = parsePatternItem(match[6], 0, 6, true, patternItemTypeWeek)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	return cs, nil
   195  }
   196  
   197  // parsePatternItem parses every item in the pattern and returns the result as map, which is used for indexing.
   198  func parsePatternItem(
   199  	item string, min int, max int,
   200  	allowQuestionMark bool, itemType patternItemType,
   201  ) (itemMap map[int]struct{}, err error) {
   202  	itemMap = make(map[int]struct{}, max-min+1)
   203  	if item == "*" || (allowQuestionMark && item == "?") {
   204  		for i := min; i <= max; i++ {
   205  			itemMap[i] = struct{}{}
   206  		}
   207  		return itemMap, nil
   208  	}
   209  	// Example: 1-10/2,11-30/3
   210  	var number int
   211  	for _, itemElem := range strings.Split(item, ",") {
   212  		var (
   213  			interval      = 1
   214  			intervalArray = strings.Split(itemElem, "/")
   215  		)
   216  		if len(intervalArray) == 2 {
   217  			if number, err = strconv.Atoi(intervalArray[1]); err != nil {
   218  				return nil, gerror.NewCodef(
   219  					gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, itemElem,
   220  				)
   221  			} else {
   222  				interval = number
   223  			}
   224  		}
   225  		var (
   226  			rangeMin   = min
   227  			rangeMax   = max
   228  			rangeArray = strings.Split(intervalArray[0], "-") // Example: 1-30, JAN-DEC
   229  		)
   230  		// Example: 1-30/2
   231  		if rangeArray[0] != "*" {
   232  			if number, err = parseWeekAndMonthNameToInt(rangeArray[0], itemType); err != nil {
   233  				return nil, gerror.NewCodef(
   234  					gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, itemElem,
   235  				)
   236  			} else {
   237  				rangeMin = number
   238  				if len(intervalArray) == 1 {
   239  					rangeMax = number
   240  				}
   241  			}
   242  		}
   243  		// Example: 1-30/2
   244  		if len(rangeArray) == 2 {
   245  			if number, err = parseWeekAndMonthNameToInt(rangeArray[1], itemType); err != nil {
   246  				return nil, gerror.NewCodef(
   247  					gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, itemElem,
   248  				)
   249  			} else {
   250  				rangeMax = number
   251  			}
   252  		}
   253  		for i := rangeMin; i <= rangeMax; i += interval {
   254  			itemMap[i] = struct{}{}
   255  		}
   256  	}
   257  	return
   258  }
   259  
   260  // parseWeekAndMonthNameToInt parses the field value to a number according to its field type.
   261  func parseWeekAndMonthNameToInt(value string, itemType patternItemType) (int, error) {
   262  	if gregex.IsMatchString(`^\d+$`, value) {
   263  		// It is pure number.
   264  		if number, err := strconv.Atoi(value); err == nil {
   265  			return number, nil
   266  		}
   267  	} else {
   268  		// Check if it contains letter,
   269  		// it converts the value to number according to predefined map.
   270  		switch itemType {
   271  		case patternItemTypeWeek:
   272  			if number, ok := weekShortNameMap[strings.ToLower(value)]; ok {
   273  				return number, nil
   274  			}
   275  			if number, ok := weekFullNameMap[strings.ToLower(value)]; ok {
   276  				return number, nil
   277  			}
   278  		case patternItemTypeMonth:
   279  			if number, ok := monthShortNameMap[strings.ToLower(value)]; ok {
   280  				return number, nil
   281  			}
   282  			if number, ok := monthFullNameMap[strings.ToLower(value)]; ok {
   283  				return number, nil
   284  			}
   285  		}
   286  	}
   287  	return 0, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern value: "%s"`, value)
   288  }