github.com/sandwich-go/boost@v1.3.29/xtime/cron/cronexpr.go (about)

     1  package cron
     2  
     3  // reference: https://github.com/gorhill/cronexpr
     4  import (
     5  	"github.com/sandwich-go/boost/xerror"
     6  	"math"
     7  	"strconv"
     8  	"strings"
     9  	"time"
    10  )
    11  
    12  // Expression
    13  // 5列会自动在头添加秒标记
    14  // Field name   | Mandatory? | Allowed values | Allowed special characters
    15  // ----------   | ---------- | -------------- | --------------------------
    16  // Seconds      | No         | 0-59           | * / , -
    17  // Minutes      | Yes        | 0-59           | * / , -
    18  // Hours        | Yes        | 0-23           | * / , -
    19  // Day          | Yes        | 1-31           | * / , -
    20  // Month        | Yes        | 1-12           | * / , -
    21  // Day of week  | Yes        | 0-6            | * / , -
    22  type Expression struct {
    23  	sec   uint64
    24  	min   uint64
    25  	hour  uint64
    26  	dom   uint64
    27  	month uint64
    28  	dow   uint64
    29  }
    30  
    31  // MustParse returns a new Expression pointer. It expects a well-formed cron
    32  // expression. If a malformed cron expression is supplied, it will `panic`.
    33  func MustParse(cronLine string) *Expression {
    34  	expr, err := Parse(cronLine)
    35  	if err != nil {
    36  		panic(err)
    37  	}
    38  	return expr
    39  }
    40  
    41  // Parse returns a new Expression pointer. An error is returned if a malformed
    42  // cron expression is supplied.
    43  func Parse(expr string) (cronExpr *Expression, err error) {
    44  	fields := strings.Fields(expr)
    45  	if len(fields) != 5 && len(fields) != 6 {
    46  		err = xerror.NewText("invalid expr %v: expected 5 or 6 fields, got %v", expr, len(fields))
    47  		return
    48  	}
    49  
    50  	if len(fields) == 5 {
    51  		fields = append([]string{"0"}, fields...)
    52  	}
    53  
    54  	cronExpr = new(Expression)
    55  	// Seconds
    56  	cronExpr.sec, err = parseCronField(fields[0], 0, 59)
    57  	if err != nil {
    58  		goto onError
    59  	}
    60  	// Minutes
    61  	cronExpr.min, err = parseCronField(fields[1], 0, 59)
    62  	if err != nil {
    63  		goto onError
    64  	}
    65  	// Hours
    66  	cronExpr.hour, err = parseCronField(fields[2], 0, 23)
    67  	if err != nil {
    68  		goto onError
    69  	}
    70  	// Day of month
    71  	cronExpr.dom, err = parseCronField(fields[3], 1, 31)
    72  	if err != nil {
    73  		goto onError
    74  	}
    75  	// Month
    76  	cronExpr.month, err = parseCronField(fields[4], 1, 12)
    77  	if err != nil {
    78  		goto onError
    79  	}
    80  	// Day of week
    81  	cronExpr.dow, err = parseCronField(fields[5], 0, 6)
    82  	if err != nil {
    83  		goto onError
    84  	}
    85  	return
    86  
    87  onError:
    88  	err = xerror.NewText("invalid expr %v: %v", expr, err)
    89  	return
    90  }
    91  
    92  // 1. *
    93  // 2. num
    94  // 3. num-num
    95  // 4. */num
    96  // 5. num/num (means num-max/num)
    97  // 6. num-num/num
    98  func parseCronField(field string, min int, max int) (cronField uint64, err error) {
    99  	fields := strings.Split(field, ",")
   100  	for _, field := range fields {
   101  		rangeAndIncr := strings.Split(field, "/")
   102  		if len(rangeAndIncr) > 2 {
   103  			err = xerror.NewText("too many slashes: %v", field)
   104  			return
   105  		}
   106  
   107  		// range
   108  		startAndEnd := strings.Split(rangeAndIncr[0], "-")
   109  		if len(startAndEnd) > 2 {
   110  			err = xerror.NewText("too many hyphens: %v", rangeAndIncr[0])
   111  			return
   112  		}
   113  
   114  		var start, end int
   115  		if startAndEnd[0] == "*" {
   116  			if len(startAndEnd) != 1 {
   117  				err = xerror.NewText("invalid range p1: %v", rangeAndIncr[0])
   118  				return
   119  			}
   120  			start = min
   121  			end = max
   122  		} else {
   123  			// start
   124  			start, err = strconv.Atoi(startAndEnd[0])
   125  			if err != nil {
   126  				err = xerror.NewText("invalid range p2: %v", rangeAndIncr[0])
   127  				return
   128  			}
   129  			// end
   130  			if len(startAndEnd) == 1 {
   131  				if len(rangeAndIncr) == 2 {
   132  					end = max
   133  				} else {
   134  					end = start
   135  				}
   136  			} else {
   137  				end, err = strconv.Atoi(startAndEnd[1])
   138  				if err != nil {
   139  					err = xerror.NewText("invalid range p3: %v", rangeAndIncr[0])
   140  					return
   141  				}
   142  			}
   143  		}
   144  
   145  		if start > end {
   146  			err = xerror.NewText("invalid range p4: %v", rangeAndIncr[0])
   147  			return
   148  		}
   149  		if start < min {
   150  			err = xerror.NewText("start out of range [%v, %v]: %v", min, max, rangeAndIncr[0])
   151  			return
   152  		}
   153  		if end > max {
   154  			err = xerror.NewText("end out of range [%v, %v]: %v", min, max, rangeAndIncr[0])
   155  			return
   156  		}
   157  
   158  		// increment
   159  		var incr int
   160  		if len(rangeAndIncr) == 1 {
   161  			incr = 1
   162  		} else {
   163  			incr, err = strconv.Atoi(rangeAndIncr[1])
   164  			if err != nil {
   165  				err = xerror.NewText("invalid increment: %v", rangeAndIncr[1])
   166  				return
   167  			}
   168  			if incr <= 0 {
   169  				err = xerror.NewText("invalid increment: %v", rangeAndIncr[1])
   170  				return
   171  			}
   172  		}
   173  
   174  		// cronField
   175  		if incr == 1 {
   176  			cronField |= ^(math.MaxUint64 << uint(end+1)) & (math.MaxUint64 << uint(start))
   177  		} else {
   178  			for i := start; i <= end; i += incr {
   179  				cronField |= 1 << uint(i)
   180  			}
   181  		}
   182  	}
   183  
   184  	return
   185  }
   186  
   187  func (e *Expression) matchDay(t time.Time) bool {
   188  	// day-of-month blank
   189  	if e.dom == 0xfffffffe {
   190  		return 1<<uint(t.Weekday())&e.dow != 0
   191  	}
   192  
   193  	// day-of-week blank
   194  	if e.dow == 0x7f {
   195  		return 1<<uint(t.Day())&e.dom != 0
   196  	}
   197  
   198  	return 1<<uint(t.Weekday())&e.dow != 0 ||
   199  		1<<uint(t.Day())&e.dom != 0
   200  }
   201  
   202  // goroutine safe
   203  func (e *Expression) Next(t time.Time) time.Time {
   204  	// the upcoming second
   205  	t = t.Truncate(time.Second).Add(time.Second)
   206  
   207  	year := t.Year()
   208  	initFlag := false
   209  
   210  retry:
   211  	// Year
   212  	if t.Year() > year+1 {
   213  		return time.Time{}
   214  	}
   215  
   216  	// Month
   217  	for 1<<uint(t.Month())&e.month == 0 {
   218  		if !initFlag {
   219  			initFlag = true
   220  			t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
   221  		}
   222  
   223  		t = t.AddDate(0, 1, 0)
   224  		if t.Month() == time.January {
   225  			goto retry
   226  		}
   227  	}
   228  
   229  	// Day
   230  	for !e.matchDay(t) {
   231  		if !initFlag {
   232  			initFlag = true
   233  			t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
   234  		}
   235  
   236  		t = t.AddDate(0, 0, 1)
   237  		if t.Day() == 1 {
   238  			goto retry
   239  		}
   240  	}
   241  
   242  	// Hours
   243  	for 1<<uint(t.Hour())&e.hour == 0 {
   244  		if !initFlag {
   245  			initFlag = true
   246  			t = t.Truncate(time.Hour)
   247  		}
   248  
   249  		t = t.Add(time.Hour)
   250  		if t.Hour() == 0 {
   251  			goto retry
   252  		}
   253  	}
   254  
   255  	// Minutes
   256  	for 1<<uint(t.Minute())&e.min == 0 {
   257  		if !initFlag {
   258  			initFlag = true
   259  			t = t.Truncate(time.Minute)
   260  		}
   261  
   262  		t = t.Add(time.Minute)
   263  		if t.Minute() == 0 {
   264  			goto retry
   265  		}
   266  	}
   267  
   268  	// Seconds
   269  	for 1<<uint(t.Second())&e.sec == 0 {
   270  		if !initFlag {
   271  			initFlag = true
   272  		}
   273  
   274  		t = t.Add(time.Second)
   275  		if t.Second() == 0 {
   276  			goto retry
   277  		}
   278  	}
   279  
   280  	return t
   281  }