github.com/wangyougui/gf/v2@v2.6.5/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/wangyougui/gf. 6 7 package gcron 8 9 import ( 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/wangyougui/gf/v2/container/gtype" 15 "github.com/wangyougui/gf/v2/errors/gcode" 16 "github.com/wangyougui/gf/v2/errors/gerror" 17 "github.com/wangyougui/gf/v2/os/gtime" 18 "github.com/wangyougui/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 }