schedule.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. package cron
  2. import (
  3. "log"
  4. "math"
  5. "strconv"
  6. "strings"
  7. )
  8. // A cron schedule that specifies a duty cycle (to the second granularity).
  9. // Schedules are computed initially and stored as bit sets.
  10. type Schedule struct {
  11. Second, Minute, Hour, Dom, Month, Dow uint64
  12. }
  13. // A range of acceptable values.
  14. type bounds struct {
  15. min, max uint
  16. names map[string]uint
  17. }
  18. // The bounds for each field.
  19. var (
  20. seconds = bounds{0, 59, nil}
  21. minutes = bounds{0, 59, nil}
  22. hours = bounds{0, 23, nil}
  23. dom = bounds{1, 31, nil}
  24. months = bounds{1, 12, map[string]uint{
  25. "jan": 1,
  26. "feb": 2,
  27. "mar": 3,
  28. "apr": 4,
  29. "may": 5,
  30. "jun": 6,
  31. "jul": 7,
  32. "aug": 8,
  33. "sep": 9,
  34. "oct": 10,
  35. "nov": 11,
  36. "dec": 12,
  37. }}
  38. dow = bounds{0, 7, map[string]uint{
  39. "sun": 0,
  40. "mon": 1,
  41. "tue": 2,
  42. "wed": 3,
  43. "thu": 4,
  44. "fri": 5,
  45. "sat": 6,
  46. }}
  47. )
  48. const (
  49. // Set the top bit if a star was included in the expression.
  50. STAR_BIT = 1 << 63
  51. )
  52. // Returns a new crontab schedule representing the given spec.
  53. // Panics with a descriptive error if the spec is not valid.
  54. func Parse(spec string) *Schedule {
  55. if spec[0] == '@' {
  56. return parseDescriptor(spec)
  57. }
  58. // Split on whitespace. We require 4 or 5 fields.
  59. // (minute) (hour) (day of month) (month) (day of week, optional)
  60. fields := strings.Fields(spec)
  61. if len(fields) != 5 && len(fields) != 6 {
  62. log.Panicf("Expected 5 or 6 fields, found %d: %s", len(fields), spec)
  63. }
  64. // If a fifth field is not provided (DayOfWeek), then it is equivalent to star.
  65. if len(fields) == 5 {
  66. fields = append(fields, "*")
  67. }
  68. schedule := &Schedule{
  69. Second: getField(fields[0], seconds),
  70. Minute: getField(fields[1], minutes),
  71. Hour: getField(fields[2], hours),
  72. Dom: getField(fields[3], dom),
  73. Month: getField(fields[4], months),
  74. Dow: getField(fields[5], dow),
  75. }
  76. // If either bit 0 or 7 are set, set both. (both accepted as Sunday)
  77. if 1&schedule.Dow|1<<7&schedule.Dow > 0 {
  78. schedule.Dow = schedule.Dow | 1 | 1<<7
  79. }
  80. return schedule
  81. }
  82. // Return an Int with the bits set representing all of the times that the field represents.
  83. // A "field" is a comma-separated list of "ranges".
  84. func getField(field string, r bounds) uint64 {
  85. // list = range {"," range}
  86. var bits uint64
  87. ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' })
  88. for _, expr := range ranges {
  89. bits |= getRange(expr, r)
  90. }
  91. return bits
  92. }
  93. func getRange(expr string, r bounds) uint64 {
  94. // number | number "-" number [ "/" number ]
  95. var (
  96. start, end, step uint
  97. rangeAndStep = strings.Split(expr, "/")
  98. lowAndHigh = strings.Split(rangeAndStep[0], "-")
  99. singleDigit = len(lowAndHigh) == 1
  100. )
  101. var extra_star uint64
  102. if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" {
  103. start = r.min
  104. end = r.max
  105. extra_star = STAR_BIT
  106. } else {
  107. start = parseIntOrName(lowAndHigh[0], r.names)
  108. switch len(lowAndHigh) {
  109. case 1:
  110. end = start
  111. case 2:
  112. end = parseIntOrName(lowAndHigh[1], r.names)
  113. default:
  114. log.Panicf("Too many hyphens: %s", expr)
  115. }
  116. }
  117. switch len(rangeAndStep) {
  118. case 1:
  119. step = 1
  120. case 2:
  121. step = mustParseInt(rangeAndStep[1])
  122. // Special handling: "N/step" means "N-max/step".
  123. if singleDigit {
  124. end = r.max
  125. }
  126. default:
  127. log.Panicf("Too many slashes: %s", expr)
  128. }
  129. if start < r.min {
  130. log.Panicf("Beginning of range (%d) below minimum (%d): %s", start, r.min, expr)
  131. }
  132. if end > r.max {
  133. log.Panicf("End of range (%d) above maximum (%d): %s", end, r.max, expr)
  134. }
  135. if start > end {
  136. log.Panicf("Beginning of range (%d) beyond end of range (%d): %s", start, end, expr)
  137. }
  138. return getBits(start, end, step) | extra_star
  139. }
  140. func parseIntOrName(expr string, names map[string]uint) uint {
  141. if names != nil {
  142. if namedInt, ok := names[strings.ToLower(expr)]; ok {
  143. return namedInt
  144. }
  145. }
  146. return mustParseInt(expr)
  147. }
  148. func mustParseInt(expr string) uint {
  149. num, err := strconv.Atoi(expr)
  150. if err != nil {
  151. log.Panicf("Failed to parse int from %s: %s", expr, err)
  152. }
  153. if num < 0 {
  154. log.Panicf("Negative number (%d) not allowed: %s", num, expr)
  155. }
  156. return uint(num)
  157. }
  158. func getBits(min, max, step uint) uint64 {
  159. var bits uint64
  160. // If step is 1, use shifts.
  161. if step == 1 {
  162. return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min)
  163. }
  164. // Else, use a simple loop.
  165. for i := min; i <= max; i += step {
  166. bits |= 1 << i
  167. }
  168. return bits
  169. }
  170. func all(r bounds) uint64 {
  171. return getBits(r.min, r.max, 1) | STAR_BIT
  172. }
  173. func first(r bounds) uint64 {
  174. return getBits(r.min, r.min, 1)
  175. }
  176. func parseDescriptor(spec string) *Schedule {
  177. switch spec {
  178. case "@yearly", "@annually":
  179. return &Schedule{
  180. Second: 1 << seconds.min,
  181. Minute: 1 << minutes.min,
  182. Hour: 1 << hours.min,
  183. Dom: 1 << dom.min,
  184. Month: 1 << months.min,
  185. Dow: all(dow),
  186. }
  187. case "@monthly":
  188. return &Schedule{
  189. Second: 1 << seconds.min,
  190. Minute: 1 << minutes.min,
  191. Hour: 1 << hours.min,
  192. Dom: 1 << dom.min,
  193. Month: all(months),
  194. Dow: all(dow),
  195. }
  196. case "@weekly":
  197. return &Schedule{
  198. Second: 1 << seconds.min,
  199. Minute: 1 << minutes.min,
  200. Hour: 1 << hours.min,
  201. Dom: all(dom),
  202. Month: all(months),
  203. Dow: 1 << dow.min,
  204. }
  205. case "@daily", "@midnight":
  206. return &Schedule{
  207. Second: 1 << seconds.min,
  208. Minute: 1 << minutes.min,
  209. Hour: 1 << hours.min,
  210. Dom: all(dom),
  211. Month: all(months),
  212. Dow: all(dow),
  213. }
  214. case "@hourly":
  215. return &Schedule{
  216. Second: 1 << seconds.min,
  217. Minute: 1 << minutes.min,
  218. Hour: all(hours),
  219. Dom: all(dom),
  220. Month: all(months),
  221. Dow: all(dow),
  222. }
  223. }
  224. log.Panicf("Unrecognized descriptor: %s", spec)
  225. return nil
  226. }