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  }