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 }