clon.lisp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. (in-package :clon)
  2. ;;;; Generic interface
  3. (defparameter *default-next-time-limit*
  4. (encode-universal-time 0 0 0 1 1 3000)
  5. "The default time limit for NEXT-TIME searches.")
  6. (defgeneric next-time (schedule &key now allow-now-p limit)
  7. (:documentation "Return the next time according to SCHEDULE or NIL
  8. if there is no next time. If ALLOW-NOW-P the earliest possible time to
  9. be returned is NOW, else it is usually NOW + the resolution of the
  10. schedule. The default value of NOW is (GET-UNIVERSAL-TIME),
  11. ALLOW-NOW-P is NIL and LIMIT is *DEFAULT-NEXT-TIME-LIMIT*"))
  12. (defun make-scheduler (schedule &key (now (get-universal-time))
  13. allow-now-p (limit *default-next-time-limit*))
  14. "Return a `scheduler' function of no arguments that returns times
  15. from NOW on by repeatedly calling NEXT-TIME on SCHEDULE. ALLOW-NOW-P
  16. is passed to the first invocation of NEXT-TIME."
  17. (lambda ()
  18. (prog1
  19. (setf now (next-time schedule :now now :allow-now-p allow-now-p
  20. :limit limit))
  21. (setf allow-now-p nil))))
  22. (defun schedule-function (function scheduler &key name
  23. (thread (bt:current-thread)))
  24. "Create a timer just as with TRIVIAL-TIMERS:MAKE-TIMER but schedule
  25. and reschedule FUNCTION according to SCHEDULER that is a function of
  26. no parameters that returns a universal time or NIL. The returned timer
  27. can be shut down with TRIVIAL-TIMERS:UNSCHEDULE-TIMER."
  28. (let (timer)
  29. (flet ((foo ()
  30. (let ((next-time (funcall scheduler)))
  31. (when next-time
  32. (trivial-timers:schedule-timer timer next-time
  33. :absolute-p t)))
  34. (funcall function)))
  35. (setf timer
  36. (trivial-timers:make-timer #'foo
  37. :name name
  38. :thread thread)))
  39. (let ((first-time (funcall scheduler)))
  40. (when first-time
  41. (trivial-timers:schedule-timer timer first-time :absolute-p t)))
  42. timer))
  43. ;;;; Time utilities
  44. ;;; From sbcl's time.lisp
  45. #-sbcl
  46. (defun leap-years-before (year)
  47. (let ((years (- year 1901)))
  48. (+ (- (truncate years 4)
  49. (truncate years 100))
  50. (truncate (+ years 300) 400))))
  51. (defun decode-universal-time* (time)
  52. "Return the decoded time components as a list instead of multiple
  53. values."
  54. (multiple-value-list (decode-universal-time time)))
  55. (defun encode-universal-time* (decoded-time)
  56. "Encode DECODED-TIME that is a decoded time in list form such as one
  57. that was returned by DECODE-UNIVERSAL-TIME*."
  58. (apply #'encode-universal-time (subseq decoded-time 0 6)))
  59. (defun days-of-month (decoded-time)
  60. "Return the number of days in the month DECODED-TIME."
  61. (let ((year (elt decoded-time 5))
  62. (month (elt decoded-time 4)))
  63. (+ (aref #(31 28 31 30 31 30 31 31 30 31 30 31) (1- month))
  64. (if (= 2 month)
  65. (- (leap-years-before (1+ year))
  66. (leap-years-before year))
  67. 0))))
  68. (defun min-valid-decoded-time-component (n)
  69. "Return the smallest valid value for the Nth decoded time component."
  70. (if (or (= n 3) (= n 4))
  71. 1
  72. 0))
  73. (defun max-valid-decoded-time-component (n &optional decoded-time)
  74. "Return the largest valid value for the Nth component of
  75. DECODED-TIME or NIL if there is no limit. Passing DECODED-TIME is
  76. necessary only for the day of month component because the number of
  77. days in a month varies."
  78. (ecase n
  79. ;; second and minute
  80. ((0 1) 59)
  81. ;; hour
  82. ((2) 23)
  83. ;; day of month
  84. ((3) (days-of-month decoded-time))
  85. ;; month
  86. ((4) 12)
  87. ;; year
  88. ((5) nil)
  89. ;; day of week
  90. ((6) 6)))
  91. (defun valid-decoded-time-compenent-p (n value)
  92. "See if value can ever be a valid value as the Nth component of a
  93. decoded time."
  94. (and (<= (min-valid-decoded-time-component n) value)
  95. (let ((limit (max-valid-decoded-time-component n '(0 0 0 1 1 2000))))
  96. (or (null limit) (<= value limit)))))
  97. (defun zero-decoded-time-below (decoded-time n)
  98. "Set the first N components of DECODED-TIME to their
  99. MIN-VALID-DECODED-TIME-COMPONENT values."
  100. (loop for i below n
  101. do (setf (elt decoded-time i)
  102. (min-valid-decoded-time-component i))))
  103. (defun bump-decoded-time (time n &optional (m 1))
  104. "Increment the Nth component of decoded TIME, handle overflows, zero
  105. the lower components. Return changed decoded time and the highest index
  106. that was changed."
  107. (let ((max n))
  108. (labels ((bump (time n)
  109. (setf max (max max n))
  110. (let ((limit (max-valid-decoded-time-component n time)))
  111. (cond ((or (null limit) (< (elt time n) limit))
  112. (incf (elt time n)))
  113. (t
  114. (setf (elt time n)
  115. (min-valid-decoded-time-component n))
  116. (bump time (1+ n)))))))
  117. (loop repeat m do (bump time n))
  118. (zero-decoded-time-below time max)
  119. (values time max))))
  120. ;;;; Cron-like schedule
  121. (defun next-bump (bumper decoded-time n)
  122. "Invoke BUMPER on the Nth component of DECODED-TIME. Return its
  123. value if it can ever be valid (not necessarily in the context of
  124. DECODED-TIME) or else NIL."
  125. (let* ((original (elt decoded-time n))
  126. (next (cond ((null bumper) original)
  127. ((eql bumper '*) original)
  128. ((numberp bumper) bumper)
  129. (t (funcall bumper original decoded-time n)))))
  130. (when (and next (not (valid-decoded-time-compenent-p n next)))
  131. (error "Invalid value ~S for decoded time component ~S." next n))
  132. (if (or (null next)
  133. (< next original)
  134. (and (= n 3)
  135. (< (days-of-month decoded-time) next)))
  136. nil
  137. next)))
  138. (defun bump-day-of-month-and-day-of-week (dom-bumper dow-bumper decoded-time)
  139. "Extra hair due to the circular dependency between DAY-OF-MONTH and
  140. DAY-OF-WEEK bumpers. This function rolls the two bumpers into one."
  141. ;; First let DOM-BUMPER decide what's NEXT. If it is NIL we're done.
  142. ;; Else ask DOW-BUMPER what it thinks. If he likes it (i.e. returns
  143. ;; the same day of week) then NEXT is the result. Else skip to the
  144. ;; day of week it said or next Monday for NIL if it is still in the
  145. ;; same month. Repeat.
  146. (let* ((decoded-time (copy-list decoded-time))
  147. (last-day-of-month (days-of-month decoded-time)))
  148. (flet ((skip-days (n)
  149. (incf (elt decoded-time 3) n)
  150. ;; Keep DECODED-TIME consistent, update DAY-OF-WEEK.
  151. (setf (elt decoded-time 6) (mod (+ (elt decoded-time 6) n) 7))))
  152. (loop while (<= (elt decoded-time 3) last-day-of-month) do
  153. (let* ((current-dom (elt decoded-time 3))
  154. (next-dom (next-bump dom-bumper decoded-time 3)))
  155. (unless next-dom
  156. (return nil))
  157. (skip-days (- next-dom current-dom))
  158. (assert (<= next-dom last-day-of-month)))
  159. (let* ((current-dow (elt decoded-time 6))
  160. (next-dow (next-bump dow-bumper decoded-time 6)))
  161. ;; See if the dom is also a blessed dow.
  162. (when (eql next-dow current-dow)
  163. (return (elt decoded-time 3)))
  164. ;; Skip until the prescribed day or next Monday.
  165. (skip-days (- (or next-dow 7) current-dow)))))))
  166. (defclass cron-schedule ()
  167. ((bumpers :initarg :bumpers :reader bumpers
  168. :documentation "The bumpers in decoded time component order."))
  169. (:documentation "A cron-like schedule. See MAKE-CRON-SCHEDULE for details."))
  170. (defun make-cron-schedule (&key second minute hour day-of-month
  171. month year day-of-week)
  172. "Construct a cron-like scheduler from the SECOND, MINUTE, etc
  173. bumpers for components of decoded times (using the default time zone
  174. for the time being). A bumper in its most generic from a function of
  175. three arguments: the current value, the whole decoded time, and the
  176. index of the current value in the decoded time. A bumper is expected
  177. to return the smallest value that is valid for that component and not
  178. less than the current value or NIL if it cannot find a valid value.
  179. Returning a value that is smaller than the current one is the same as
  180. returning NIL. A bumper can simply be a number which is equivalent to
  181. \(CONSTANTLY NUMBER).
  182. Bumpers are not allowed to depend on `lower' components of the decoded
  183. time. The allowed dependency graph is this:
  184. SECOND -> MINUTE -> HOUR -> (DAY-OF-MONTH <-> DAY-OF-WEEK) -> MONTH -> YEAR
  185. That is, the SECOND bumper may look at the whole decoded time but the
  186. MINUTE bumper may not look at seconds. DAY-OF-WEEK and DAY-OF-MONTH
  187. may depend on each other to allow specifying the 'last Friday of the
  188. month'.
  189. The resolution of the schedule is defined implicitly by the lowest
  190. bumper. NEXT-TIME always bumps the component of the decoded time that
  191. belongs to the lowest bumper before trying to find mathces if its
  192. LAST-TIME argument is not NIL. Of course, DAY-OF-WEEK is the odd one
  193. out: it is the day-of-month component that is bumped if DAY-OF-WEEK is
  194. the lowest bumper.
  195. This scheme allows (MAKE-CRON-SCHEDULE :MONTH 12) trigger only once
  196. per year instead of every second in December. For a more packed
  197. schedule one can use the symbol '* as a bumper: (MAKE-CRON-SCHEDULE
  198. :HOUR '* :MONTH 12) which makes hour the lowest bumper and the
  199. schedule triggers every hour in December.
  200. It is an error if all bumpers are NIL."
  201. (let ((bumpers (list second minute hour day-of-month month year day-of-week)))
  202. (unless (some #'identity bumpers)
  203. (error "Cannot determine resolution of an empty schedule."))
  204. (make-instance 'cron-schedule :bumpers bumpers)))
  205. (defun bumper-index->component-index (i)
  206. "Return the index of the decoded time component that is effect by
  207. bumper I. Day of week is lumped together with day of month."
  208. (if (= i 6) 3 i))
  209. (defun lowest-component-index-with-a-bumper (bumpers)
  210. "Return the the index of what is basically the root of current
  211. dependency graph."
  212. (loop for i upfrom 0
  213. for bumper in bumpers
  214. when bumper
  215. minimize (bumper-index->component-index i)))
  216. (defun bump-lowest-component (bumpers time)
  217. "Bump the lowest component of decoded TIME that has a bumper. Return
  218. it as a universal time."
  219. (let ((decoded-time (decode-universal-time* time)))
  220. (encode-universal-time*
  221. (bump-decoded-time decoded-time
  222. (lowest-component-index-with-a-bumper
  223. bumpers)))))
  224. (defmethod next-time ((schedule cron-schedule) &key (now (get-universal-time))
  225. allow-now-p (limit *default-next-time-limit*))
  226. (let ((bumpers (bumpers schedule)))
  227. (unless allow-now-p
  228. (setf now (bump-lowest-component bumpers now)))
  229. (loop while (< now limit)
  230. with n = 5
  231. for decoded-time = (decode-universal-time* now)
  232. for next = (if (= n 3)
  233. (bump-day-of-month-and-day-of-week (elt bumpers 3)
  234. (elt bumpers 6)
  235. decoded-time)
  236. (next-bump (elt bumpers n) decoded-time n))
  237. do
  238. (cond ((null next)
  239. (when (= n 5)
  240. ;; The desired year is in the past, there is no next
  241. ;; time.
  242. (return nil))
  243. ;; No valid value for this component, bump the next one
  244. ;; and come again.
  245. (multiple-value-setq (decoded-time n)
  246. (bump-decoded-time decoded-time (1+ n))))
  247. (t
  248. (when (< (elt decoded-time n) next)
  249. (setf (elt decoded-time n) next)
  250. (zero-decoded-time-below decoded-time n))
  251. (decf n)))
  252. (setf now (encode-universal-time* decoded-time))
  253. (when (minusp n)
  254. (return now)))))
  255. ;;;; The convenience case: typed cron schedule
  256. (defun find-decoded-time-component-by-type (type value decoded-time n)
  257. "Return the first valid value not less than VALUE that is of TYPE."
  258. (loop with limit = (max-valid-decoded-time-component n decoded-time)
  259. for x upfrom value below limit
  260. do (when (typep x type)
  261. (return-from find-decoded-time-component-by-type x))))
  262. (defun make-typed-cron-bumper (type)
  263. "Return a bumper function suitable for MAKE-CRON-SCHEDULE that
  264. returns the first valid value according to TYPE. Convenience function
  265. on top of FIND-DECODED-TIME-COMPONENT-BY-TYPE."
  266. (lambda (value decoded-time n)
  267. (find-decoded-time-component-by-type type value decoded-time n)))
  268. (defun make-typed-cron-schedule (&key second minute hour day-of-month
  269. month year day-of-week)
  270. "A convenience function much like MAKE-CRON-SCHEDULE but assumes
  271. that no bumper can be a function designator so it must be a number,
  272. the symbol * or a type specifier in which case it calls
  273. MAKE-TYPED-CRON-BUMPER on it providing a terser syntax."
  274. (flet ((convert-bumper (bumper)
  275. (if (or (null bumper)
  276. (typep bumper 'number)
  277. (eq bumper '*))
  278. bumper
  279. (make-typed-cron-bumper bumper))))
  280. (let ((bumpers (list second minute hour day-of-month month year
  281. day-of-week)))
  282. (make-instance 'cron-schedule
  283. :bumpers (mapcar #'convert-bumper bumpers)))))