github.com/isyscore/isc-gobase@v1.5.3-0.20231218061332-cbc7451899e9/cron/parser.go (about) 1 package cron 2 3 import ( 4 "fmt" 5 "math" 6 "strconv" 7 "strings" 8 "time" 9 ) 10 11 //ParseOption Configuration options for creating a parser. Most options specify which 12 // fields should be included, while others enable features. If a field is not 13 // included the parser will assume a default value. These options do not change 14 // the order fields are parse in. 15 type ParseOption int 16 17 const ( 18 Second ParseOption = 1 << iota // Seconds field, default 0 19 Minute // Minutes field, default 0 20 Hour // Hours field, default 0 21 Dom // Day of month field, default * 22 Month // Month field, default * 23 Dow // Day of week field, default * 24 DowOptional // Optional day of week field, default * 25 Descriptor // Allow descriptors such as @monthly, @weekly, etc. 26 ) 27 28 var places = []ParseOption{ 29 Second, 30 Minute, 31 Hour, 32 Dom, 33 Month, 34 Dow, 35 } 36 37 var defaults = []string{ 38 "0", 39 "0", 40 "0", 41 "*", 42 "*", 43 "*", 44 } 45 46 //Parser A custom Parser that can be configured. 47 type Parser struct { 48 options ParseOption 49 optionals int 50 } 51 52 //NewParser Creates a custom Parser with custom options. 53 // 54 // // Standard parser without descriptors 55 // specParser := NewParser(Minute | Hour | Dom | Month | Dow) 56 // sched, err := specParser.Parse("0 0 15 */3 *") 57 // 58 // // Same as above, just excludes time fields 59 // subsParser := NewParser(Dom | Month | Dow) 60 // sched, err := specParser.Parse("15 */3 *") 61 // 62 // // Same as above, just makes Dow optional 63 // subsParser := NewParser(Dom | Month | DowOptional) 64 // sched, err := specParser.Parse("15 */3") 65 // 66 func NewParser(options ParseOption) Parser { 67 optionals := 0 68 if options&DowOptional > 0 { 69 options |= Dow 70 optionals++ 71 } 72 return Parser{options, optionals} 73 } 74 75 //Parse returns a new crontab schedule representing the given spec. 76 // It returns a descriptive error if the spec is not valid. 77 // It accepts crontab specs and features configured by NewParser. 78 func (p Parser) Parse(spec string) (Schedule, error) { 79 if len(spec) == 0 { 80 return nil, fmt.Errorf("empty spec string") 81 } 82 if spec[0] == '@' && p.options&Descriptor > 0 { 83 return parseDescriptor(spec) 84 } 85 86 // Figure out how many fields we need 87 max := 0 88 for _, place := range places { 89 if p.options&place > 0 { 90 max++ 91 } 92 } 93 min := max - p.optionals 94 95 // Split fields on whitespace 96 fields := strings.Fields(spec) 97 98 // Validate number of fields 99 if count := len(fields); count < min || count > max { 100 if min == max { 101 return nil, fmt.Errorf("expected exactly %d fields, found %d: %s", min, count, spec) 102 } 103 return nil, fmt.Errorf("expected %d to %d fields, found %d: %s", min, max, count, spec) 104 } 105 106 // Fill in missing fields 107 fields = expandFields(fields, p.options) 108 109 var err error 110 field := func(field string, r bounds) uint64 { 111 if err != nil { 112 return 0 113 } 114 var bits uint64 115 bits, err = getField(field, r) 116 return bits 117 } 118 119 var ( 120 second = field(fields[0], seconds) 121 minute = field(fields[1], minutes) 122 hour = field(fields[2], hours) 123 dayofmonth = field(fields[3], dom) 124 month = field(fields[4], months) 125 dayofweek = field(fields[5], dow) 126 ) 127 if err != nil { 128 return nil, err 129 } 130 131 return &SpecSchedule{ 132 Second: second, 133 Minute: minute, 134 Hour: hour, 135 Dom: dayofmonth, 136 Month: month, 137 Dow: dayofweek, 138 }, nil 139 } 140 141 func expandFields(fields []string, options ParseOption) []string { 142 n := 0 143 count := len(fields) 144 expFields := make([]string, len(places)) 145 copy(expFields, defaults) 146 for i, place := range places { 147 if options&place > 0 { 148 expFields[i] = fields[n] 149 n++ 150 } 151 if n == count { 152 break 153 } 154 } 155 return expFields 156 } 157 158 var standardParser = NewParser( 159 Minute | Hour | Dom | Month | Dow | Descriptor, 160 ) 161 162 // ParseStandard returns a new crontab schedule representing the given standardSpec 163 // (https://en.wikipedia.org/wiki/Cron). It differs from Parse requiring to always 164 // pass 5 entries representing: minute, hour, day of month, month and day of week, 165 // in that order. It returns a descriptive error if the spec is not valid. 166 // 167 // It accepts 168 // - Standard crontab specs, e.g. "* * * * ?" 169 // - Descriptors, e.g. "@midnight", "@every 1h30m" 170 func ParseStandard(standardSpec string) (Schedule, error) { 171 return standardParser.Parse(standardSpec) 172 } 173 174 var defaultParser = NewParser( 175 Second | Minute | Hour | Dom | Month | DowOptional | Descriptor, 176 ) 177 178 // Parse returns a new crontab schedule representing the given spec. 179 // It returns a descriptive error if the spec is not valid. 180 // 181 // It accepts 182 // - Full crontab specs, e.g. "* * * * * ?" 183 // - Descriptors, e.g. "@midnight", "@every 1h30m" 184 func Parse(spec string) (Schedule, error) { 185 return defaultParser.Parse(spec) 186 } 187 188 // getField returns an Int with the bits set representing all of the times that 189 // the field represents or error parsing field value. A "field" is a comma-separated 190 // list of "ranges". 191 func getField(field string, r bounds) (uint64, error) { 192 var bits uint64 193 ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' }) 194 for _, expr := range ranges { 195 bit, err := getRange(expr, r) 196 if err != nil { 197 return bits, err 198 } 199 bits |= bit 200 } 201 return bits, nil 202 } 203 204 // getRange returns the bits indicated by the given expression: 205 // number | number "-" number [ "/" number ] 206 // or error parsing range. 207 func getRange(expr string, r bounds) (uint64, error) { 208 var ( 209 start, end, step uint 210 rangeAndStep = strings.Split(expr, "/") 211 lowAndHigh = strings.Split(rangeAndStep[0], "-") 212 singleDigit = len(lowAndHigh) == 1 213 err error 214 ) 215 216 var extra uint64 217 if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { 218 start = r.min 219 end = r.max 220 extra = starBit 221 } else { 222 start, err = parseIntOrName(lowAndHigh[0], r.names) 223 if err != nil { 224 return 0, err 225 } 226 switch len(lowAndHigh) { 227 case 1: 228 end = start 229 case 2: 230 end, err = parseIntOrName(lowAndHigh[1], r.names) 231 if err != nil { 232 return 0, err 233 } 234 default: 235 return 0, fmt.Errorf("too many hyphens: %s", expr) 236 } 237 } 238 239 switch len(rangeAndStep) { 240 case 1: 241 step = 1 242 case 2: 243 step, err = mustParseInt(rangeAndStep[1]) 244 if err != nil { 245 return 0, err 246 } 247 248 // Special handling: "N/step" means "N-max/step". 249 if singleDigit { 250 end = r.max 251 } 252 default: 253 return 0, fmt.Errorf("too many slashes: %s", expr) 254 } 255 256 if start < r.min { 257 return 0, fmt.Errorf("beginning of range (%d) below minimum (%d): %s", start, r.min, expr) 258 } 259 if end > r.max { 260 return 0, fmt.Errorf("end of range (%d) above maximum (%d): %s", end, r.max, expr) 261 } 262 if start > end { 263 return 0, fmt.Errorf("beginning of range (%d) beyond end of range (%d): %s", start, end, expr) 264 } 265 if step == 0 { 266 return 0, fmt.Errorf("step of range should be a positive number: %s", expr) 267 } 268 269 return getBits(start, end, step) | extra, nil 270 } 271 272 // parseIntOrName returns the (possibly-named) integer contained in expr. 273 func parseIntOrName(expr string, names map[string]uint) (uint, error) { 274 if names != nil { 275 if namedInt, ok := names[strings.ToLower(expr)]; ok { 276 return namedInt, nil 277 } 278 } 279 return mustParseInt(expr) 280 } 281 282 // mustParseInt parses the given expression as an int or returns an error. 283 func mustParseInt(expr string) (uint, error) { 284 num, err := strconv.Atoi(expr) 285 if err != nil { 286 return 0, fmt.Errorf("failed to parse int from %s: %s", expr, err) 287 } 288 if num < 0 { 289 return 0, fmt.Errorf("negative number (%d) not allowed: %s", num, expr) 290 } 291 292 return uint(num), nil 293 } 294 295 // getBits sets all bits in the range [min, max], modulo the given step size. 296 func getBits(min, max, step uint) uint64 { 297 var bits uint64 298 299 // If step is 1, use shifts. 300 if step == 1 { 301 return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min) 302 } 303 304 // Else, use a simple loop. 305 for i := min; i <= max; i += step { 306 bits |= 1 << i 307 } 308 return bits 309 } 310 311 // all returns all bits within the given bounds. (plus the star bit) 312 func all(r bounds) uint64 { 313 return getBits(r.min, r.max, 1) | starBit 314 } 315 316 // parseDescriptor returns a predefined schedule for the expression, or error if none matches. 317 func parseDescriptor(descriptor string) (Schedule, error) { 318 switch descriptor { 319 case "@yearly", "@annually": 320 return &SpecSchedule{ 321 Second: 1 << seconds.min, 322 Minute: 1 << minutes.min, 323 Hour: 1 << hours.min, 324 Dom: 1 << dom.min, 325 Month: 1 << months.min, 326 Dow: all(dow), 327 }, nil 328 329 case "@monthly": 330 return &SpecSchedule{ 331 Second: 1 << seconds.min, 332 Minute: 1 << minutes.min, 333 Hour: 1 << hours.min, 334 Dom: 1 << dom.min, 335 Month: all(months), 336 Dow: all(dow), 337 }, nil 338 339 case "@weekly": 340 return &SpecSchedule{ 341 Second: 1 << seconds.min, 342 Minute: 1 << minutes.min, 343 Hour: 1 << hours.min, 344 Dom: all(dom), 345 Month: all(months), 346 Dow: 1 << dow.min, 347 }, nil 348 349 case "@daily", "@midnight": 350 return &SpecSchedule{ 351 Second: 1 << seconds.min, 352 Minute: 1 << minutes.min, 353 Hour: 1 << hours.min, 354 Dom: all(dom), 355 Month: all(months), 356 Dow: all(dow), 357 }, nil 358 359 case "@hourly": 360 return &SpecSchedule{ 361 Second: 1 << seconds.min, 362 Minute: 1 << minutes.min, 363 Hour: all(hours), 364 Dom: all(dom), 365 Month: all(months), 366 Dow: all(dow), 367 }, nil 368 } 369 370 const every = "@every " 371 if strings.HasPrefix(descriptor, every) { 372 duration, err := time.ParseDuration(descriptor[len(every):]) 373 if err != nil { 374 return nil, fmt.Errorf("failed to parse duration %s: %s", descriptor, err) 375 } 376 return Every(duration), nil 377 } 378 379 return nil, fmt.Errorf("unrecognized descriptor: %s", descriptor) 380 }