pkg.re/essentialkaos/ek.10@v12.41.0+incompatible/cron/cron.go (about)

     1  // Package cron provides methods for working with cron expressions
     2  package cron
     3  
     4  // ////////////////////////////////////////////////////////////////////////////////// //
     5  //                                                                                    //
     6  //                         Copyright (c) 2022 ESSENTIAL KAOS                          //
     7  //      Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0>     //
     8  //                                                                                    //
     9  // ////////////////////////////////////////////////////////////////////////////////// //
    10  
    11  import (
    12  	"errors"
    13  	"strconv"
    14  	"strings"
    15  	"time"
    16  
    17  	"pkg.re/essentialkaos/ek.v12/strutil"
    18  )
    19  
    20  // ////////////////////////////////////////////////////////////////////////////////// //
    21  
    22  // Aliases
    23  const (
    24  	YEARLY   = "0 0 1 1 *"
    25  	ANNUALLY = "0 0 1 1 *"
    26  	MONTHLY  = "0 0 1 * *"
    27  	WEEKLY   = "0 0 * * 0"
    28  	DAILY    = "0 0 * * *"
    29  	HOURLY   = "0 * * * *"
    30  )
    31  
    32  // ////////////////////////////////////////////////////////////////////////////////// //
    33  
    34  const (
    35  	_SYMBOL_PERIOD   = "-"
    36  	_SYMBOL_INTERVAL = "/"
    37  	_SYMBOL_ENUM     = ","
    38  	_SYMBOL_ANY      = "*"
    39  )
    40  
    41  const (
    42  	_NAMES_NONE   uint8 = 0
    43  	_NAMES_DAYS   uint8 = 1
    44  	_NAMES_MONTHS uint8 = 2
    45  )
    46  
    47  // ////////////////////////////////////////////////////////////////////////////////// //
    48  
    49  // Expr cron expression struct
    50  type Expr struct {
    51  	expression string
    52  	minutes    []uint8
    53  	hours      []uint8
    54  	doms       []uint8
    55  	months     []uint8
    56  	dows       []uint8
    57  }
    58  
    59  // ////////////////////////////////////////////////////////////////////////////////// //
    60  
    61  type exprInfo struct {
    62  	min uint8
    63  	max uint8
    64  	nt  uint8 // Naming type
    65  }
    66  
    67  // ////////////////////////////////////////////////////////////////////////////////// //
    68  
    69  var (
    70  	// ErrMalformedExpression is returned by the Parse method if given cron expression has
    71  	// wrong or unsupported format
    72  	ErrMalformedExpression = errors.New("Expression must have 5 tokens")
    73  
    74  	// ErrZeroInterval is returned if interval part of expression is empty
    75  	ErrZeroInterval = errors.New("Interval can't be less or equals 0")
    76  )
    77  
    78  // ////////////////////////////////////////////////////////////////////////////////// //
    79  
    80  var info = []exprInfo{
    81  	{0, 59, _NAMES_NONE},
    82  	{0, 23, _NAMES_NONE},
    83  	{1, 31, _NAMES_NONE},
    84  	{1, 12, _NAMES_MONTHS},
    85  	{0, 6, _NAMES_DAYS},
    86  }
    87  
    88  // ////////////////////////////////////////////////////////////////////////////////// //
    89  
    90  // codebeat:disable[LOC,ABC]
    91  
    92  // Parse parse cron expression
    93  // https://en.wikipedia.org/wiki/Cron
    94  func Parse(expr string) (*Expr, error) {
    95  	expr = strings.Replace(expr, "\t", " ", -1)
    96  	expr = getAliasExpression(expr)
    97  
    98  	if strings.Count(expr, " ") < 4 {
    99  		return nil, ErrMalformedExpression
   100  	}
   101  
   102  	result := &Expr{expression: expr}
   103  
   104  	for tn, ei := range info {
   105  		var data []uint8
   106  		var err error
   107  
   108  		token := strutil.ReadField(expr, tn, true, " ")
   109  
   110  		switch {
   111  		case isAnyToken(token):
   112  			data = fillUintSlice(ei.min, ei.max, 1)
   113  		case isEnumToken(token):
   114  			data, err = parseEnumToken(token, ei)
   115  		case isPeriodToken(token):
   116  			data, err = parsePeriodToken(token, ei)
   117  		case isIntervalToken(token):
   118  			data, err = parseIntervalToken(token, ei)
   119  		default:
   120  			data, err = parseSimpleToken(token, ei)
   121  		}
   122  
   123  		if err != nil {
   124  			return nil, err
   125  		}
   126  
   127  		switch tn {
   128  		case 0:
   129  			result.minutes = data
   130  		case 1:
   131  			result.hours = data
   132  		case 2:
   133  			result.doms = data
   134  		case 3:
   135  			result.months = data
   136  		case 4:
   137  			result.dows = data
   138  		}
   139  	}
   140  
   141  	return result, nil
   142  }
   143  
   144  // codebeat:enable[LOC,ABC]
   145  
   146  // ////////////////////////////////////////////////////////////////////////////////// //
   147  
   148  // codebeat:disable[LOC]
   149  
   150  // IsDue check if current moment is match for expression
   151  func (expr *Expr) IsDue(args ...time.Time) bool {
   152  	var t time.Time
   153  
   154  	if len(args) >= 1 {
   155  		t = args[0]
   156  	} else {
   157  		t = time.Now()
   158  	}
   159  
   160  	if !contains(expr.minutes, uint8(t.Minute())) {
   161  		return false
   162  	}
   163  
   164  	if !contains(expr.hours, uint8(t.Hour())) {
   165  		return false
   166  	}
   167  
   168  	if !contains(expr.doms, uint8(t.Day())) {
   169  		return false
   170  	}
   171  
   172  	if !contains(expr.months, uint8(t.Month())) {
   173  		return false
   174  	}
   175  
   176  	if !contains(expr.dows, uint8(t.Weekday())) {
   177  		return false
   178  	}
   179  
   180  	return true
   181  }
   182  
   183  // I don't have an idea how we can implement this without this conditions
   184  // codebeat:disable[BLOCK_NESTING,LOC,CYCLO]
   185  
   186  // Next get time of next matched moment
   187  func (expr *Expr) Next(args ...time.Time) time.Time {
   188  	var t time.Time
   189  
   190  	if len(args) >= 1 {
   191  		t = args[0]
   192  	} else {
   193  		t = time.Now()
   194  	}
   195  
   196  	year := t.Year()
   197  
   198  	mStart := getNearPrevIndex(expr.months, uint8(t.Month()))
   199  	dStart := getNearPrevIndex(expr.doms, uint8(t.Day()))
   200  
   201  	for y := year; y < year+5; y++ {
   202  		for i := mStart; i < len(expr.months); i++ {
   203  			for j := dStart; j < len(expr.doms); j++ {
   204  				for k := 0; k < len(expr.hours); k++ {
   205  					for l := 0; l < len(expr.minutes); l++ {
   206  						d := time.Date(
   207  							y,
   208  							time.Month(expr.months[i]),
   209  							int(expr.doms[j]),
   210  							int(expr.hours[k]),
   211  							int(expr.minutes[l]),
   212  							0, 0, t.Location(),
   213  						)
   214  
   215  						if d.Unix() <= t.Unix() {
   216  							continue
   217  						}
   218  
   219  						switch {
   220  						case uint8(d.Month()) != expr.months[i],
   221  							uint8(d.Day()) != expr.doms[j],
   222  							uint8(d.Hour()) != expr.hours[k],
   223  							uint8(d.Minute()) != expr.minutes[l],
   224  							!contains(expr.dows, uint8(d.Weekday())):
   225  							continue
   226  						}
   227  
   228  						return d
   229  					}
   230  				}
   231  			}
   232  
   233  			dStart = 0
   234  		}
   235  
   236  		mStart = 0
   237  	}
   238  
   239  	return time.Unix(0, 0)
   240  }
   241  
   242  // Prev get time of prev matched moment
   243  func (expr *Expr) Prev(args ...time.Time) time.Time {
   244  	var t time.Time
   245  
   246  	if len(args) >= 1 {
   247  		t = args[0]
   248  	} else {
   249  		t = time.Now()
   250  	}
   251  
   252  	year := t.Year()
   253  
   254  	mStart := getNearNextIndex(expr.months, uint8(t.Month()))
   255  	dStart := getNearNextIndex(expr.doms, uint8(t.Day()))
   256  
   257  	for y := year; y >= year-5; y-- {
   258  		for i := mStart; i >= 0; i-- {
   259  			for j := dStart; j >= 0; j-- {
   260  				for k := len(expr.hours) - 1; k >= 0; k-- {
   261  					for l := len(expr.minutes) - 1; l >= 0; l-- {
   262  						d := time.Date(
   263  							y,
   264  							time.Month(expr.months[i]),
   265  							int(expr.doms[j]),
   266  							int(expr.hours[k]),
   267  							int(expr.minutes[l]),
   268  							0, 0, t.Location(),
   269  						)
   270  
   271  						if d.Unix() >= t.Unix() {
   272  							continue
   273  						}
   274  
   275  						switch {
   276  						case uint8(d.Month()) != expr.months[i],
   277  							uint8(d.Day()) != expr.doms[j],
   278  							uint8(d.Hour()) != expr.hours[k],
   279  							uint8(d.Minute()) != expr.minutes[l],
   280  							!contains(expr.dows, uint8(d.Weekday())):
   281  							continue
   282  						}
   283  
   284  						return d
   285  					}
   286  				}
   287  			}
   288  
   289  			dStart = len(expr.doms) - 1
   290  		}
   291  
   292  		mStart = len(expr.months) - 1
   293  	}
   294  
   295  	return time.Unix(0, 0)
   296  }
   297  
   298  // codebeat:enable[BLOCK_NESTING,LOC,CYCLO]
   299  
   300  // String return raw expression
   301  func (expr *Expr) String() string {
   302  	return expr.expression
   303  }
   304  
   305  // ////////////////////////////////////////////////////////////////////////////////// //
   306  
   307  func isAnyToken(t string) bool {
   308  	return t == _SYMBOL_ANY
   309  }
   310  
   311  func isEnumToken(t string) bool {
   312  	return strings.Contains(t, _SYMBOL_ENUM)
   313  }
   314  
   315  func isPeriodToken(t string) bool {
   316  	return strings.Contains(t, _SYMBOL_PERIOD)
   317  }
   318  
   319  func isIntervalToken(t string) bool {
   320  	return strings.Contains(t, _SYMBOL_INTERVAL)
   321  }
   322  
   323  func parseEnumToken(t string, ei exprInfo) ([]uint8, error) {
   324  	var result []uint8
   325  
   326  	for i := 0; i <= strings.Count(t, _SYMBOL_ENUM); i++ {
   327  		tt := strutil.ReadField(t, i, false, _SYMBOL_ENUM)
   328  
   329  		switch {
   330  		case isPeriodToken(tt):
   331  			d, err := parsePeriodToken(tt, ei)
   332  
   333  			if err != nil {
   334  				return nil, err
   335  			}
   336  
   337  			result = append(result, d...)
   338  
   339  		default:
   340  			t, err := parseToken(tt, ei.nt)
   341  
   342  			if err != nil {
   343  				return nil, err
   344  			}
   345  
   346  			result = append(result, t)
   347  		}
   348  	}
   349  
   350  	return result, nil
   351  }
   352  
   353  func parsePeriodToken(t string, ei exprInfo) ([]uint8, error) {
   354  	t1, err := parseToken(strutil.ReadField(t, 0, false, _SYMBOL_PERIOD), ei.nt)
   355  
   356  	if err != nil {
   357  		return nil, err
   358  	}
   359  
   360  	t2, err := parseToken(strutil.ReadField(t, 1, false, _SYMBOL_PERIOD), ei.nt)
   361  
   362  	if err != nil {
   363  		return nil, err
   364  	}
   365  
   366  	return fillUintSlice(
   367  		between(t1, ei.min, ei.max),
   368  		between(t2, ei.min, ei.max),
   369  		1,
   370  	), nil
   371  }
   372  
   373  func parseIntervalToken(t string, ei exprInfo) ([]uint8, error) {
   374  	i, err := str2uint(strutil.ReadField(t, 1, false, _SYMBOL_INTERVAL))
   375  
   376  	if err != nil {
   377  		return nil, err
   378  	}
   379  
   380  	if i == 0 {
   381  		return nil, ErrZeroInterval
   382  	}
   383  
   384  	return fillUintSlice(ei.min, ei.max, i), nil
   385  }
   386  
   387  func parseSimpleToken(t string, ei exprInfo) ([]uint8, error) {
   388  	v, err := parseToken(t, ei.nt)
   389  
   390  	if err != nil {
   391  		return nil, err
   392  	}
   393  
   394  	return []uint8{v}, nil
   395  }
   396  
   397  func getAliasExpression(expr string) string {
   398  	switch expr {
   399  	case "@yearly":
   400  		return YEARLY
   401  	case "@annually":
   402  		return ANNUALLY
   403  	case "@monthly":
   404  		return MONTHLY
   405  	case "@weekly":
   406  		return WEEKLY
   407  	case "@daily":
   408  		return DAILY
   409  	case "@hourly":
   410  		return HOURLY
   411  	}
   412  
   413  	return expr
   414  }
   415  
   416  func parseToken(t string, nt uint8) (uint8, error) {
   417  	switch nt {
   418  	case _NAMES_DAYS:
   419  		tu, ok := getDayNumByName(t)
   420  		if ok {
   421  			return tu, nil
   422  		}
   423  
   424  	case _NAMES_MONTHS:
   425  		tu, ok := getMonthNumByName(t)
   426  		if ok {
   427  			return tu, nil
   428  		}
   429  	}
   430  
   431  	return str2uint(t)
   432  }
   433  
   434  func getDayNumByName(token string) (uint8, bool) {
   435  	switch strings.ToLower(token) {
   436  	case "sun":
   437  		return 0, true
   438  	case "mon":
   439  		return 1, true
   440  	case "tue":
   441  		return 2, true
   442  	case "wed":
   443  		return 3, true
   444  	case "thu":
   445  		return 4, true
   446  	case "fri":
   447  		return 5, true
   448  	case "sat":
   449  		return 6, true
   450  	}
   451  
   452  	return 0, false
   453  }
   454  
   455  func getMonthNumByName(token string) (uint8, bool) {
   456  	switch strings.ToLower(token) {
   457  	case "jan":
   458  		return 1, true
   459  	case "feb":
   460  		return 2, true
   461  	case "mar":
   462  		return 3, true
   463  	case "apr":
   464  		return 4, true
   465  	case "may":
   466  		return 5, true
   467  	case "jun":
   468  		return 6, true
   469  	case "jul":
   470  		return 7, true
   471  	case "aug":
   472  		return 8, true
   473  	case "sep":
   474  		return 9, true
   475  	case "oct":
   476  		return 10, true
   477  	case "nov":
   478  		return 11, true
   479  	case "dec":
   480  		return 12, true
   481  	}
   482  
   483  	return 0, false
   484  }
   485  
   486  func fillUintSlice(start, end, interval uint8) []uint8 {
   487  	var result []uint8
   488  
   489  	for i := start; i <= end; i += interval {
   490  		result = append(result, i)
   491  	}
   492  
   493  	return result
   494  }
   495  
   496  func str2uint(t string) (uint8, error) {
   497  	u, err := strconv.ParseUint(t, 10, 8)
   498  
   499  	if err != nil {
   500  		return 0, err
   501  	}
   502  
   503  	return uint8(u), nil
   504  }
   505  
   506  func getNearNextIndex(items []uint8, item uint8) int {
   507  	for i := 0; i < len(items); i++ {
   508  		if items[i] >= item {
   509  			return i
   510  		}
   511  	}
   512  
   513  	return 0
   514  }
   515  
   516  func getNearPrevIndex(items []uint8, item uint8) int {
   517  	for i := len(items) - 1; i >= 0; i-- {
   518  		if items[i] <= item {
   519  			return i
   520  		}
   521  	}
   522  
   523  	return len(items) - 1
   524  }
   525  
   526  func between(val, min, max uint8) uint8 {
   527  	switch {
   528  	case val < min:
   529  		return min
   530  	case val > max:
   531  		return max
   532  	default:
   533  		return val
   534  	}
   535  }
   536  
   537  func contains(data []uint8, item uint8) bool {
   538  	for _, v := range data {
   539  		if item == v {
   540  			return true
   541  		}
   542  	}
   543  
   544  	return false
   545  }