github.com/songzhibin97/gkit@v1.2.13/distributed/schedule/parser.go (about) 1 package schedule 2 3 import ( 4 "fmt" 5 "math" 6 "strconv" 7 "strings" 8 "time" 9 10 "github.com/songzhibin97/gkit/options" 11 ) 12 13 // bounds provides a range of acceptable values (plus a map of name to value). 14 type bounds struct { 15 min, max uint 16 names map[string]uint 17 } 18 19 var ( 20 seconds = bounds{0, 59, nil} 21 minutes = bounds{0, 59, nil} 22 hours = bounds{0, 23, nil} 23 dom = bounds{1, 31, nil} 24 months = bounds{1, 12, map[string]uint{ 25 "jan": 1, 26 "feb": 2, 27 "mar": 3, 28 "apr": 4, 29 "may": 5, 30 "jun": 6, 31 "jul": 7, 32 "aug": 8, 33 "sep": 9, 34 "oct": 10, 35 "nov": 11, 36 "dec": 12, 37 }} 38 dow = bounds{0, 6, map[string]uint{ 39 "sun": 0, 40 "mon": 1, 41 "tue": 2, 42 "wed": 3, 43 "thu": 4, 44 "fri": 5, 45 "sat": 6, 46 }} 47 ) 48 49 type Parser struct { 50 *Config 51 } 52 53 var standardParser = NewParse(WithInterval(Minute | Hour | Dom | Month | Dow | Descriptor)) 54 var secondParser = NewParse(WithInterval(Second | Minute | Hour | Dom | Month | DowOptional | Descriptor)) 55 56 func NewParseWithStandard(standardSpec string) (Schedule, error) { 57 return standardParser.Parse(standardSpec) 58 } 59 func NewParseWithSecondParser(standardSpec string) (Schedule, error) { 60 return secondParser.Parse(standardSpec) 61 } 62 63 func NewParse(ops ...options.Option) Parser { 64 p := Parser{ 65 Config: &Config{}, 66 } 67 for _, op := range ops { 68 op(p.Config) 69 } 70 return p 71 } 72 73 func (p Parser) Parse(spec string) (Schedule, error) { 74 if len(spec) == 0 { 75 return nil, fmt.Errorf("empty spec string") 76 } 77 78 // Extract timezone if present 79 var loc = time.Local 80 if strings.HasPrefix(spec, "TZ=") || strings.HasPrefix(spec, "CRON_TZ=") { 81 var err error 82 i := strings.Index(spec, " ") 83 eq := strings.Index(spec, "=") 84 if loc, err = time.LoadLocation(spec[eq+1 : i]); err != nil { 85 return nil, fmt.Errorf("provided bad location %s: %v", spec[eq+1:i], err) 86 } 87 spec = strings.TrimSpace(spec[i:]) 88 } 89 90 // Handle named schedules (descriptors), if configured 91 if strings.HasPrefix(spec, "@") { 92 if p.interval&Descriptor == 0 { 93 return nil, fmt.Errorf("parser does not accept descriptors: %v", spec) 94 } 95 return parseDescriptor(spec, loc) 96 } 97 98 // Split on whitespace. 99 fields := strings.Fields(spec) 100 101 // Validate & fill in any omitted or optional fields 102 var err error 103 fields, err = normalizeFields(fields, p.interval) 104 if err != nil { 105 return nil, err 106 } 107 108 field := func(field string, r bounds) uint64 { 109 if err != nil { 110 return 0 111 } 112 var bits uint64 113 bits, err = getField(field, r) 114 return bits 115 } 116 117 var ( 118 second = field(fields[0], seconds) 119 minute = field(fields[1], minutes) 120 hour = field(fields[2], hours) 121 dayofmonth = field(fields[3], dom) 122 month = field(fields[4], months) 123 dayofweek = field(fields[5], dow) 124 ) 125 if err != nil { 126 return nil, err 127 } 128 129 return &SpecSchedule{ 130 Second: second, 131 Minute: minute, 132 Hour: hour, 133 Dom: dayofmonth, 134 Month: month, 135 Dow: dayofweek, 136 Location: loc, 137 }, nil 138 } 139 140 // getBits sets all bits in the range [min, max], modulo the given step size. 141 func getBits(min, max, step uint) uint64 { 142 var bits uint64 143 144 // If step is 1, use shifts. 145 if step == 1 { 146 return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min) 147 } 148 149 // Else, use a simple loop. 150 for i := min; i <= max; i += step { 151 bits |= 1 << i 152 } 153 return bits 154 } 155 156 // all returns all bits within the given bounds. (plus the star bit) 157 func all(r bounds) uint64 { 158 return getBits(r.min, r.max, 1) | starBit 159 } 160 161 // parseDescriptor returns a predefined schedule for the expression, or error if none matches. 162 func parseDescriptor(descriptor string, loc *time.Location) (Schedule, error) { 163 switch descriptor { 164 case "@yearly", "@annually": 165 return &SpecSchedule{ 166 Second: 1 << seconds.min, 167 Minute: 1 << minutes.min, 168 Hour: 1 << hours.min, 169 Dom: 1 << dom.min, 170 Month: 1 << months.min, 171 Dow: all(dow), 172 Location: loc, 173 }, nil 174 175 case "@monthly": 176 return &SpecSchedule{ 177 Second: 1 << seconds.min, 178 Minute: 1 << minutes.min, 179 Hour: 1 << hours.min, 180 Dom: 1 << dom.min, 181 Month: all(months), 182 Dow: all(dow), 183 Location: loc, 184 }, nil 185 186 case "@weekly": 187 return &SpecSchedule{ 188 Second: 1 << seconds.min, 189 Minute: 1 << minutes.min, 190 Hour: 1 << hours.min, 191 Dom: all(dom), 192 Month: all(months), 193 Dow: 1 << dow.min, 194 Location: loc, 195 }, nil 196 197 case "@daily", "@midnight": 198 return &SpecSchedule{ 199 Second: 1 << seconds.min, 200 Minute: 1 << minutes.min, 201 Hour: 1 << hours.min, 202 Dom: all(dom), 203 Month: all(months), 204 Dow: all(dow), 205 Location: loc, 206 }, nil 207 208 case "@hourly": 209 return &SpecSchedule{ 210 Second: 1 << seconds.min, 211 Minute: 1 << minutes.min, 212 Hour: all(hours), 213 Dom: all(dom), 214 Month: all(months), 215 Dow: all(dow), 216 Location: loc, 217 }, nil 218 219 } 220 221 const every = "@every " 222 if strings.HasPrefix(descriptor, every) { 223 duration, err := time.ParseDuration(descriptor[len(every):]) 224 if err != nil { 225 return nil, fmt.Errorf("failed to parse duration %s: %s", descriptor, err) 226 } 227 return Every(duration), nil 228 } 229 230 return nil, fmt.Errorf("unrecognized descriptor: %s", descriptor) 231 } 232 233 // normalizeFields takes a subset set of the time fields and returns the full set 234 // with defaults (zeroes) populated for unset fields. 235 // 236 // As part of performing this function, it also validates that the provided 237 // fields are compatible with the configured options. 238 func normalizeFields(fields []string, interval Interval) ([]string, error) { 239 // Validate optionals & add their field to options 240 optionals := 0 241 if interval&SecondOptional > 0 { 242 interval |= Second 243 optionals++ 244 } 245 if interval&DowOptional > 0 { 246 interval |= Dow 247 optionals++ 248 } 249 if optionals > 1 { 250 return nil, fmt.Errorf("multiple optionals may not be configured") 251 } 252 253 // Figure out how many fields we need 254 max := 0 255 for _, place := range places { 256 if interval&place > 0 { 257 max++ 258 } 259 } 260 min := max - optionals 261 262 // Validate number of fields 263 if count := len(fields); count < min || count > max { 264 if min == max { 265 return nil, fmt.Errorf("expected exactly %d fields, found %d: %s", min, count, fields) 266 } 267 return nil, fmt.Errorf("expected %d to %d fields, found %d: %s", min, max, count, fields) 268 } 269 270 // Populate the optional field if not provided 271 if min < max && len(fields) == min { 272 switch { 273 case interval&DowOptional > 0: 274 fields = append(fields, defaults[5]) // TODO: improve access to default 275 case interval&SecondOptional > 0: 276 fields = append([]string{defaults[0]}, fields...) 277 default: 278 return nil, fmt.Errorf("unknown optional field") 279 } 280 } 281 282 // Populate all fields not part of interval with their defaults 283 n := 0 284 expandedFields := make([]string, len(places)) 285 copy(expandedFields, defaults) 286 for i, place := range places { 287 if interval&place > 0 { 288 expandedFields[i] = fields[n] 289 n++ 290 } 291 } 292 return expandedFields, nil 293 } 294 295 // getField returns an Int with the bits set representing all of the times that 296 // the field represents or error parsing field value. A "field" is a comma-separated 297 // list of "ranges". 298 func getField(field string, r bounds) (uint64, error) { 299 var bits uint64 300 ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' }) 301 for _, expr := range ranges { 302 bit, err := getRange(expr, r) 303 if err != nil { 304 return bits, err 305 } 306 bits |= bit 307 } 308 return bits, nil 309 } 310 311 // getRange returns the bits indicated by the given expression: 312 // 313 // number | number "-" number [ "/" number ] 314 // 315 // or error parsing range. 316 func getRange(expr string, r bounds) (uint64, error) { 317 var ( 318 start, end, step uint 319 rangeAndStep = strings.Split(expr, "/") 320 lowAndHigh = strings.Split(rangeAndStep[0], "-") 321 singleDigit = len(lowAndHigh) == 1 322 err error 323 ) 324 325 var extra uint64 326 if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { 327 start = r.min 328 end = r.max 329 extra = starBit 330 } else { 331 start, err = parseIntOrName(lowAndHigh[0], r.names) 332 if err != nil { 333 return 0, err 334 } 335 switch len(lowAndHigh) { 336 case 1: 337 end = start 338 case 2: 339 end, err = parseIntOrName(lowAndHigh[1], r.names) 340 if err != nil { 341 return 0, err 342 } 343 default: 344 return 0, fmt.Errorf("too many hyphens: %s", expr) 345 } 346 } 347 348 switch len(rangeAndStep) { 349 case 1: 350 step = 1 351 case 2: 352 step, err = mustParseInt(rangeAndStep[1]) 353 if err != nil { 354 return 0, err 355 } 356 357 // Special handling: "N/step" means "N-max/step". 358 if singleDigit { 359 end = r.max 360 } 361 if step > 1 { 362 extra = 0 363 } 364 default: 365 return 0, fmt.Errorf("too many slashes: %s", expr) 366 } 367 368 if start < r.min { 369 return 0, fmt.Errorf("beginning of range (%d) below minimum (%d): %s", start, r.min, expr) 370 } 371 if end > r.max { 372 return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr) 373 } 374 if start > end { 375 return 0, fmt.Errorf("beginning of range (%d) beyond end of range (%d): %s", start, end, expr) 376 } 377 if step == 0 { 378 return 0, fmt.Errorf("step of range should be a positive number: %s", expr) 379 } 380 381 return getBits(start, end, step) | extra, nil 382 } 383 384 // mustParseInt parses the given expression as an int or returns an error. 385 func mustParseInt(expr string) (uint, error) { 386 num, err := strconv.Atoi(expr) 387 if err != nil { 388 return 0, fmt.Errorf("failed to parse int from %s: %s", expr, err) 389 } 390 if num < 0 { 391 return 0, fmt.Errorf("negative number (%d) not allowed: %s", num, expr) 392 } 393 394 return uint(num), nil 395 } 396 397 // parseIntOrName returns the (possibly-named) integer contained in expr. 398 func parseIntOrName(expr string, names map[string]uint) (uint, error) { 399 if names != nil { 400 if namedInt, ok := names[strings.ToLower(expr)]; ok { 401 return namedInt, nil 402 } 403 } 404 return mustParseInt(expr) 405 }