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  }