schedule.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. package cron
  2. import (
  3. "errors"
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. "time"
  8. )
  9. // Moment represents a parsed single time moment.
  10. type Moment struct {
  11. Minute int `json:"minute"`
  12. Hour int `json:"hour"`
  13. Day int `json:"day"`
  14. Month int `json:"month"`
  15. DayOfWeek int `json:"dayOfWeek"`
  16. }
  17. // NewMoment creates a new Moment from the specified time.
  18. func NewMoment(t time.Time) *Moment {
  19. return &Moment{
  20. Minute: t.Minute(),
  21. Hour: t.Hour(),
  22. Day: t.Day(),
  23. Month: int(t.Month()),
  24. DayOfWeek: int(t.Weekday()),
  25. }
  26. }
  27. // Schedule stores parsed information for each time component when a cron job should run.
  28. type Schedule struct {
  29. Minutes map[int]struct{} `json:"minutes"`
  30. Hours map[int]struct{} `json:"hours"`
  31. Days map[int]struct{} `json:"days"`
  32. Months map[int]struct{} `json:"months"`
  33. DaysOfWeek map[int]struct{} `json:"daysOfWeek"`
  34. }
  35. // IsDue checks whether the provided Moment satisfies the current Schedule.
  36. func (s *Schedule) IsDue(m *Moment) bool {
  37. if _, ok := s.Minutes[m.Minute]; !ok {
  38. return false
  39. }
  40. if _, ok := s.Hours[m.Hour]; !ok {
  41. return false
  42. }
  43. if _, ok := s.Days[m.Day]; !ok {
  44. return false
  45. }
  46. if _, ok := s.DaysOfWeek[m.DayOfWeek]; !ok {
  47. return false
  48. }
  49. if _, ok := s.Months[m.Month]; !ok {
  50. return false
  51. }
  52. return true
  53. }
  54. // NewSchedule creates a new Schedule from a cron expression.
  55. //
  56. // A cron expression is consisted of 5 segments separated by space,
  57. // representing: minute, hour, day of the month, month and day of the week.
  58. //
  59. // Each segment could be in the following formats:
  60. // - wildcard: *
  61. // - range: 1-30
  62. // - step: */n or 1-30/n
  63. // - list: 1,2,3,10-20/n
  64. func NewSchedule(cronExpr string) (*Schedule, error) {
  65. segments := strings.Split(cronExpr, " ")
  66. if len(segments) != 5 {
  67. return nil, errors.New("invalid cron expression - must have exactly 5 space separated segments")
  68. }
  69. minutes, err := parseCronSegment(segments[0], 0, 59)
  70. if err != nil {
  71. return nil, err
  72. }
  73. hours, err := parseCronSegment(segments[1], 0, 23)
  74. if err != nil {
  75. return nil, err
  76. }
  77. days, err := parseCronSegment(segments[2], 1, 31)
  78. if err != nil {
  79. return nil, err
  80. }
  81. months, err := parseCronSegment(segments[3], 1, 12)
  82. if err != nil {
  83. return nil, err
  84. }
  85. daysOfWeek, err := parseCronSegment(segments[4], 0, 6)
  86. if err != nil {
  87. return nil, err
  88. }
  89. return &Schedule{
  90. Minutes: minutes,
  91. Hours: hours,
  92. Days: days,
  93. Months: months,
  94. DaysOfWeek: daysOfWeek,
  95. }, nil
  96. }
  97. // parseCronSegment parses a single cron expression segment and
  98. // returns its time schedule slots.
  99. func parseCronSegment(segment string, min int, max int) (map[int]struct{}, error) {
  100. slots := map[int]struct{}{}
  101. list := strings.Split(segment, ",")
  102. for _, p := range list {
  103. stepParts := strings.Split(p, "/")
  104. // step (*/n, 1-30/n)
  105. var step int
  106. switch len(stepParts) {
  107. case 1:
  108. step = 1
  109. case 2:
  110. parsedStep, err := strconv.Atoi(stepParts[1])
  111. if err != nil {
  112. return nil, err
  113. }
  114. if parsedStep < 1 || parsedStep > max {
  115. return nil, fmt.Errorf("invalid segment step boundary - the step must be between 1 and the %d", max)
  116. }
  117. step = parsedStep
  118. default:
  119. return nil, errors.New("invalid segment step format - must be in the format */n or 1-30/n")
  120. }
  121. // find the min and max range of the segment part
  122. var rangeMin, rangeMax int
  123. if stepParts[0] == "*" {
  124. rangeMin = min
  125. rangeMax = max
  126. } else {
  127. // single digit (1) or range (1-30)
  128. rangeParts := strings.Split(stepParts[0], "-")
  129. switch len(rangeParts) {
  130. case 1:
  131. if step != 1 {
  132. return nil, errors.New("invalid segement step - step > 1 could be used only with the wildcard or range format")
  133. }
  134. parsed, err := strconv.Atoi(rangeParts[0])
  135. if err != nil {
  136. return nil, err
  137. }
  138. if parsed < min || parsed > max {
  139. return nil, errors.New("invalid segment value - must be between the min and max of the segment")
  140. }
  141. rangeMin = parsed
  142. rangeMax = rangeMin
  143. case 2:
  144. parsedMin, err := strconv.Atoi(rangeParts[0])
  145. if err != nil {
  146. return nil, err
  147. }
  148. if parsedMin < min || parsedMin > max {
  149. return nil, fmt.Errorf("invalid segment range minimum - must be between %d and %d", min, max)
  150. }
  151. rangeMin = parsedMin
  152. parsedMax, err := strconv.Atoi(rangeParts[1])
  153. if err != nil {
  154. return nil, err
  155. }
  156. if parsedMax < parsedMin || parsedMax > max {
  157. return nil, fmt.Errorf("invalid segment range maximum - must be between %d and %d", rangeMin, max)
  158. }
  159. rangeMax = parsedMax
  160. default:
  161. return nil, errors.New("invalid segment range format - the range must have 1 or 2 parts")
  162. }
  163. }
  164. // fill the slots
  165. for i := rangeMin; i <= rangeMax; i += step {
  166. slots[i] = struct{}{}
  167. }
  168. }
  169. return slots, nil
  170. }