github.com/isyscore/isc-gobase@v1.5.3-0.20231218061332-cbc7451899e9/cron/spec.go (about)

     1  package cron
     2  
     3  import "time"
     4  
     5  // SpecSchedule specifies a duty cycle (to the second granularity), based on a
     6  // traditional crontab specification. It is computed initially and stored as bit sets.
     7  type SpecSchedule struct {
     8  	Second, Minute, Hour, Dom, Month, Dow uint64
     9  }
    10  
    11  // bounds provides a range of acceptable values (plus a map of name to value).
    12  type bounds struct {
    13  	min, max uint
    14  	names    map[string]uint
    15  }
    16  
    17  // The bounds for each field.
    18  var (
    19  	seconds = bounds{0, 59, nil}
    20  	minutes = bounds{0, 59, nil}
    21  	hours   = bounds{0, 23, nil}
    22  	dom     = bounds{1, 31, nil}
    23  	months  = bounds{1, 12, map[string]uint{
    24  		"jan": 1,
    25  		"feb": 2,
    26  		"mar": 3,
    27  		"apr": 4,
    28  		"may": 5,
    29  		"jun": 6,
    30  		"jul": 7,
    31  		"aug": 8,
    32  		"sep": 9,
    33  		"oct": 10,
    34  		"nov": 11,
    35  		"dec": 12,
    36  	}}
    37  	dow = bounds{0, 6, map[string]uint{
    38  		"sun": 0,
    39  		"mon": 1,
    40  		"tue": 2,
    41  		"wed": 3,
    42  		"thu": 4,
    43  		"fri": 5,
    44  		"sat": 6,
    45  	}}
    46  )
    47  
    48  const (
    49  	// Set the top bit if a star was included in the expression.
    50  	starBit = 1 << 63
    51  )
    52  
    53  // Next returns the next time this schedule is activated, greater than the given
    54  // time.  If no time can be found to satisfy the schedule, return the zero time.
    55  func (s *SpecSchedule) Next(t time.Time) time.Time {
    56  	// General approach:
    57  	// For Month, Day, Hour, Minute, Second:
    58  	// Check if the time value matches.  If yes, continue to the next field.
    59  	// If the field doesn't match the schedule, then increment the field until it matches.
    60  	// While incrementing the field, a wrap-around brings it back to the beginning
    61  	// of the field list (since it is necessary to re-verify previous field
    62  	// values)
    63  
    64  	// Start at the earliest possible time (the upcoming second).
    65  	t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
    66  
    67  	// This flag indicates whether a field has been incremented.
    68  	added := false
    69  
    70  	// If no time is found within five years, return zero.
    71  	yearLimit := t.Year() + 5
    72  
    73  WRAP:
    74  	if t.Year() > yearLimit {
    75  		return time.Time{}
    76  	}
    77  
    78  	// Find the first applicable month.
    79  	// If it's this month, then do nothing.
    80  	for 1<<uint(t.Month())&s.Month == 0 {
    81  		// If we have to add a month, reset the other parts to 0.
    82  		if !added {
    83  			added = true
    84  			// Otherwise, set the date at the beginning (since the current time is irrelevant).
    85  			t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, t.Location())
    86  		}
    87  		t = t.AddDate(0, 1, 0)
    88  
    89  		// Wrapped around.
    90  		if t.Month() == time.January {
    91  			goto WRAP
    92  		}
    93  	}
    94  
    95  	// Now get a day in that month.
    96  	for !dayMatches(s, t) {
    97  		if !added {
    98  			added = true
    99  			t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
   100  		}
   101  		t = t.AddDate(0, 0, 1)
   102  
   103  		if t.Day() == 1 {
   104  			goto WRAP
   105  		}
   106  	}
   107  
   108  	for 1<<uint(t.Hour())&s.Hour == 0 {
   109  		if !added {
   110  			added = true
   111  			t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location())
   112  		}
   113  		t = t.Add(1 * time.Hour)
   114  
   115  		if t.Hour() == 0 {
   116  			goto WRAP
   117  		}
   118  	}
   119  
   120  	for 1<<uint(t.Minute())&s.Minute == 0 {
   121  		if !added {
   122  			added = true
   123  			t = t.Truncate(time.Minute)
   124  		}
   125  		t = t.Add(1 * time.Minute)
   126  
   127  		if t.Minute() == 0 {
   128  			goto WRAP
   129  		}
   130  	}
   131  
   132  	for 1<<uint(t.Second())&s.Second == 0 {
   133  		if !added {
   134  			added = true
   135  			t = t.Truncate(time.Second)
   136  		}
   137  		t = t.Add(1 * time.Second)
   138  
   139  		if t.Second() == 0 {
   140  			goto WRAP
   141  		}
   142  	}
   143  
   144  	return t
   145  }
   146  
   147  // dayMatches returns true if the schedule's day-of-week and day-of-month
   148  // restrictions are satisfied by the given time.
   149  func dayMatches(s *SpecSchedule, t time.Time) bool {
   150  	var (
   151  		domMatch = 1<<uint(t.Day())&s.Dom > 0
   152  		dowMatch = 1<<uint(t.Weekday())&s.Dow > 0
   153  	)
   154  	if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
   155  		return domMatch && dowMatch
   156  	}
   157  	return domMatch || dowMatch
   158  }