spec.go 4.5 KB

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