git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/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  	// Override location for this schedule.
    11  	Location *time.Location
    12  }
    13  
    14  // bounds provides a range of acceptable values (plus a map of name to value).
    15  type bounds struct {
    16  	min, max uint
    17  	names    map[string]uint
    18  }
    19  
    20  // The bounds for each field.
    21  var (
    22  	seconds = bounds{0, 59, nil}
    23  	minutes = bounds{0, 59, nil}
    24  	hours   = bounds{0, 23, nil}
    25  	dom     = bounds{1, 31, nil}
    26  	months  = bounds{1, 12, map[string]uint{
    27  		"jan": 1,
    28  		"feb": 2,
    29  		"mar": 3,
    30  		"apr": 4,
    31  		"may": 5,
    32  		"jun": 6,
    33  		"jul": 7,
    34  		"aug": 8,
    35  		"sep": 9,
    36  		"oct": 10,
    37  		"nov": 11,
    38  		"dec": 12,
    39  	}}
    40  	dow = bounds{0, 6, map[string]uint{
    41  		"sun": 0,
    42  		"mon": 1,
    43  		"tue": 2,
    44  		"wed": 3,
    45  		"thu": 4,
    46  		"fri": 5,
    47  		"sat": 6,
    48  	}}
    49  )
    50  
    51  const (
    52  	// Set the top bit if a star was included in the expression.
    53  	starBit = 1 << 63
    54  )
    55  
    56  // Next returns the next time this schedule is activated, greater than the given
    57  // time.  If no time can be found to satisfy the schedule, return the zero time.
    58  func (s *SpecSchedule) Next(t time.Time) time.Time {
    59  	// General approach
    60  	//
    61  	// For Month, Day, Hour, Minute, Second:
    62  	// Check if the time value matches.  If yes, continue to the next field.
    63  	// If the field doesn't match the schedule, then increment the field until it matches.
    64  	// While incrementing the field, a wrap-around brings it back to the beginning
    65  	// of the field list (since it is necessary to re-verify previous field
    66  	// values)
    67  
    68  	// Convert the given time into the schedule's timezone, if one is specified.
    69  	// Save the original timezone so we can convert back after we find a time.
    70  	// Note that schedules without a time zone specified (time.Local) are treated
    71  	// as local to the time provided.
    72  	origLocation := t.Location()
    73  	loc := s.Location
    74  	if loc == time.Local {
    75  		loc = t.Location()
    76  	}
    77  	if s.Location != time.Local {
    78  		t = t.In(s.Location)
    79  	}
    80  
    81  	// Start at the earliest possible time (the upcoming second).
    82  	t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond)
    83  
    84  	// This flag indicates whether a field has been incremented.
    85  	added := false
    86  
    87  	// If no time is found within five years, return zero.
    88  	yearLimit := t.Year() + 5
    89  
    90  WRAP:
    91  	if t.Year() > yearLimit {
    92  		return time.Time{}
    93  	}
    94  
    95  	// Find the first applicable month.
    96  	// If it's this month, then do nothing.
    97  	for 1<<uint(t.Month())&s.Month == 0 {
    98  		// If we have to add a month, reset the other parts to 0.
    99  		if !added {
   100  			added = true
   101  			// Otherwise, set the date at the beginning (since the current time is irrelevant).
   102  			t = time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, loc)
   103  		}
   104  		t = t.AddDate(0, 1, 0)
   105  
   106  		// Wrapped around.
   107  		if t.Month() == time.January {
   108  			goto WRAP
   109  		}
   110  	}
   111  
   112  	// Now get a day in that month.
   113  	//
   114  	// NOTE: This causes issues for daylight savings regimes where midnight does
   115  	// not exist.  For example: Sao Paulo has DST that transforms midnight on
   116  	// 11/3 into 1am. Handle that by noticing when the Hour ends up != 0.
   117  	for !dayMatches(s, t) {
   118  		if !added {
   119  			added = true
   120  			t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc)
   121  		}
   122  		t = t.AddDate(0, 0, 1)
   123  		// Notice if the hour is no longer midnight due to DST.
   124  		// Add an hour if it's 23, subtract an hour if it's 1.
   125  		if t.Hour() != 0 {
   126  			if t.Hour() > 12 {
   127  				t = t.Add(time.Duration(24-t.Hour()) * time.Hour)
   128  			} else {
   129  				t = t.Add(time.Duration(-t.Hour()) * time.Hour)
   130  			}
   131  		}
   132  
   133  		if t.Day() == 1 {
   134  			goto WRAP
   135  		}
   136  	}
   137  
   138  	for 1<<uint(t.Hour())&s.Hour == 0 {
   139  		if !added {
   140  			added = true
   141  			t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, loc)
   142  		}
   143  		t = t.Add(1 * time.Hour)
   144  
   145  		if t.Hour() == 0 {
   146  			goto WRAP
   147  		}
   148  	}
   149  
   150  	for 1<<uint(t.Minute())&s.Minute == 0 {
   151  		if !added {
   152  			added = true
   153  			t = t.Truncate(time.Minute)
   154  		}
   155  		t = t.Add(1 * time.Minute)
   156  
   157  		if t.Minute() == 0 {
   158  			goto WRAP
   159  		}
   160  	}
   161  
   162  	for 1<<uint(t.Second())&s.Second == 0 {
   163  		if !added {
   164  			added = true
   165  			t = t.Truncate(time.Second)
   166  		}
   167  		t = t.Add(1 * time.Second)
   168  
   169  		if t.Second() == 0 {
   170  			goto WRAP
   171  		}
   172  	}
   173  
   174  	return t.In(origLocation)
   175  }
   176  
   177  // dayMatches returns true if the schedule's day-of-week and day-of-month
   178  // restrictions are satisfied by the given time.
   179  func dayMatches(s *SpecSchedule, t time.Time) bool {
   180  	var (
   181  		domMatch bool = 1<<uint(t.Day())&s.Dom > 0
   182  		dowMatch bool = 1<<uint(t.Weekday())&s.Dow > 0
   183  	)
   184  	if s.Dom&starBit > 0 || s.Dow&starBit > 0 {
   185  		return domMatch && dowMatch
   186  	}
   187  	return domMatch || dowMatch
   188  }