github.com/gogf/gf@v1.16.9/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  	"github.com/gogf/gf/errors/gcode"
    11  	"github.com/gogf/gf/errors/gerror"
    12  	"github.com/gogf/gf/os/gtime"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/gogf/gf/text/gregex"
    18  )
    19  
    20  // cronSchedule is the schedule for cron job.
    21  type cronSchedule struct {
    22  	create  int64            // Created timestamp.
    23  	every   int64            // Running interval in seconds.
    24  	pattern string           // The raw cron pattern string.
    25  	second  map[int]struct{} // Job can run in these second numbers.
    26  	minute  map[int]struct{} // Job can run in these minute numbers.
    27  	hour    map[int]struct{} // Job can run in these hour numbers.
    28  	day     map[int]struct{} // Job can run in these day numbers.
    29  	week    map[int]struct{} // Job can run in these week numbers.
    30  	month   map[int]struct{} // Job can run in these moth numbers.
    31  }
    32  
    33  const (
    34  	// regular expression for cron pattern, which contains 6 parts of time units.
    35  	regexForCron = `^([\-/\d\*\?,]+)\s+([\-/\d\*\?,]+)\s+([\-/\d\*\?,]+)\s+([\-/\d\*\?,]+)\s+([\-/\d\*\?,A-Za-z]+)\s+([\-/\d\*\?,A-Za-z]+)$`
    36  
    37  	patternItemTypeUnknown = iota
    38  	patternItemTypeWeek
    39  	patternItemTypeMonth
    40  )
    41  
    42  var (
    43  	// Predefined pattern map.
    44  	predefinedPatternMap = map[string]string{
    45  		"@yearly":   "0 0 0 1 1 *",
    46  		"@annually": "0 0 0 1 1 *",
    47  		"@monthly":  "0 0 0 1 * *",
    48  		"@weekly":   "0 0 0 * * 0",
    49  		"@daily":    "0 0 0 * * *",
    50  		"@midnight": "0 0 0 * * *",
    51  		"@hourly":   "0 0 * * * *",
    52  	}
    53  	// Short month name to its number.
    54  	monthMap = map[string]int{
    55  		"jan": 1,
    56  		"feb": 2,
    57  		"mar": 3,
    58  		"apr": 4,
    59  		"may": 5,
    60  		"jun": 6,
    61  		"jul": 7,
    62  		"aug": 8,
    63  		"sep": 9,
    64  		"oct": 10,
    65  		"nov": 11,
    66  		"dec": 12,
    67  	}
    68  	// Short week name to its number.
    69  	weekMap = map[string]int{
    70  		"sun": 0,
    71  		"mon": 1,
    72  		"tue": 2,
    73  		"wed": 3,
    74  		"thu": 4,
    75  		"fri": 5,
    76  		"sat": 6,
    77  	}
    78  )
    79  
    80  // newSchedule creates and returns a schedule object for given cron pattern.
    81  func newSchedule(pattern string) (*cronSchedule, error) {
    82  	// Check if the predefined patterns.
    83  	if match, _ := gregex.MatchString(`(@\w+)\s*(\w*)\s*`, pattern); len(match) > 0 {
    84  		key := strings.ToLower(match[1])
    85  		if v, ok := predefinedPatternMap[key]; ok {
    86  			pattern = v
    87  		} else if strings.Compare(key, "@every") == 0 {
    88  			if d, err := gtime.ParseDuration(match[2]); err != nil {
    89  				return nil, err
    90  			} else {
    91  				return &cronSchedule{
    92  					create:  time.Now().Unix(),
    93  					every:   int64(d.Seconds()),
    94  					pattern: pattern,
    95  				}, nil
    96  			}
    97  		} else {
    98  			return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern: "%s"`, pattern)
    99  		}
   100  	}
   101  	// Handle the common cron pattern, like:
   102  	// 0 0 0 1 1 2
   103  	if match, _ := gregex.MatchString(regexForCron, pattern); len(match) == 7 {
   104  		schedule := &cronSchedule{
   105  			create:  time.Now().Unix(),
   106  			every:   0,
   107  			pattern: pattern,
   108  		}
   109  		// Second.
   110  		if m, err := parsePatternItem(match[1], 0, 59, false); err != nil {
   111  			return nil, err
   112  		} else {
   113  			schedule.second = m
   114  		}
   115  		// Minute.
   116  		if m, err := parsePatternItem(match[2], 0, 59, false); err != nil {
   117  			return nil, err
   118  		} else {
   119  			schedule.minute = m
   120  		}
   121  		// Hour.
   122  		if m, err := parsePatternItem(match[3], 0, 23, false); err != nil {
   123  			return nil, err
   124  		} else {
   125  			schedule.hour = m
   126  		}
   127  		// Day.
   128  		if m, err := parsePatternItem(match[4], 1, 31, true); err != nil {
   129  			return nil, err
   130  		} else {
   131  			schedule.day = m
   132  		}
   133  		// Month.
   134  		if m, err := parsePatternItem(match[5], 1, 12, false); err != nil {
   135  			return nil, err
   136  		} else {
   137  			schedule.month = m
   138  		}
   139  		// Week.
   140  		if m, err := parsePatternItem(match[6], 0, 6, true); err != nil {
   141  			return nil, err
   142  		} else {
   143  			schedule.week = m
   144  		}
   145  		return schedule, nil
   146  	} else {
   147  		return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern: "%s"`, pattern)
   148  	}
   149  }
   150  
   151  // parsePatternItem parses every item in the pattern and returns the result as map, which is used for indexing.
   152  func parsePatternItem(item string, min int, max int, allowQuestionMark bool) (map[int]struct{}, error) {
   153  	m := make(map[int]struct{}, max-min+1)
   154  	if item == "*" || (allowQuestionMark && item == "?") {
   155  		for i := min; i <= max; i++ {
   156  			m[i] = struct{}{}
   157  		}
   158  	} else {
   159  		// Like: MON,FRI
   160  		for _, item := range strings.Split(item, ",") {
   161  			var (
   162  				interval      = 1
   163  				intervalArray = strings.Split(item, "/")
   164  			)
   165  			if len(intervalArray) == 2 {
   166  				if number, err := strconv.Atoi(intervalArray[1]); err != nil {
   167  					return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, item)
   168  				} else {
   169  					interval = number
   170  				}
   171  			}
   172  			var (
   173  				rangeMin   = min
   174  				rangeMax   = max
   175  				itemType   = patternItemTypeUnknown
   176  				rangeArray = strings.Split(intervalArray[0], "-") // Like: 1-30, JAN-DEC
   177  			)
   178  			switch max {
   179  			case 6:
   180  				// It's checking week field.
   181  				itemType = patternItemTypeWeek
   182  			case 12:
   183  				// It's checking month field.
   184  				itemType = patternItemTypeMonth
   185  			}
   186  			// Eg: */5
   187  			if rangeArray[0] != "*" {
   188  				if number, err := parsePatternItemValue(rangeArray[0], itemType); err != nil {
   189  					return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, item)
   190  				} else {
   191  					rangeMin = number
   192  					rangeMax = number
   193  				}
   194  			}
   195  			if len(rangeArray) == 2 {
   196  				if number, err := parsePatternItemValue(rangeArray[1], itemType); err != nil {
   197  					return nil, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern item: "%s"`, item)
   198  				} else {
   199  					rangeMax = number
   200  				}
   201  			}
   202  			for i := rangeMin; i <= rangeMax; i += interval {
   203  				m[i] = struct{}{}
   204  			}
   205  		}
   206  	}
   207  	return m, nil
   208  }
   209  
   210  // parsePatternItemValue parses the field value to a number according to its field type.
   211  func parsePatternItemValue(value string, itemType int) (int, error) {
   212  	if gregex.IsMatchString(`^\d+$`, value) {
   213  		// It is pure number.
   214  		if number, err := strconv.Atoi(value); err == nil {
   215  			return number, nil
   216  		}
   217  	} else {
   218  		// Check if it contains letter,
   219  		// it converts the value to number according to predefined map.
   220  		switch itemType {
   221  		case patternItemTypeWeek:
   222  			if number, ok := monthMap[strings.ToLower(value)]; ok {
   223  				return number, nil
   224  			}
   225  		case patternItemTypeMonth:
   226  			if number, ok := weekMap[strings.ToLower(value)]; ok {
   227  				return number, nil
   228  			}
   229  		}
   230  	}
   231  	return 0, gerror.NewCodef(gcode.CodeInvalidParameter, `invalid pattern value: "%s"`, value)
   232  }
   233  
   234  // meet checks if the given time `t` meets the runnable point for the job.
   235  func (s *cronSchedule) meet(t time.Time) bool {
   236  	if s.every != 0 {
   237  		// It checks using interval.
   238  		diff := t.Unix() - s.create
   239  		if diff > 0 {
   240  			return diff%s.every == 0
   241  		}
   242  		return false
   243  	} else {
   244  		// It checks using normal cron pattern.
   245  		if _, ok := s.second[t.Second()]; !ok {
   246  			return false
   247  		}
   248  		if _, ok := s.minute[t.Minute()]; !ok {
   249  			return false
   250  		}
   251  		if _, ok := s.hour[t.Hour()]; !ok {
   252  			return false
   253  		}
   254  		if _, ok := s.day[t.Day()]; !ok {
   255  			return false
   256  		}
   257  		if _, ok := s.month[int(t.Month())]; !ok {
   258  			return false
   259  		}
   260  		if _, ok := s.week[int(t.Weekday())]; !ok {
   261  			return false
   262  		}
   263  		return true
   264  	}
   265  }