| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- (in-package :clon)
- ;;;; Generic interface
- (defparameter *default-next-time-limit*
- (encode-universal-time 0 0 0 1 1 3000)
- "The default time limit for NEXT-TIME searches.")
- (defgeneric next-time (schedule &key now allow-now-p limit)
- (:documentation "Return the next time according to SCHEDULE or NIL
- if there is no next time. If ALLOW-NOW-P the earliest possible time to
- be returned is NOW, else it is usually NOW + the resolution of the
- schedule. The default value of NOW is (GET-UNIVERSAL-TIME),
- ALLOW-NOW-P is NIL and LIMIT is *DEFAULT-NEXT-TIME-LIMIT*"))
- (defun make-scheduler (schedule &key (now (get-universal-time))
- allow-now-p (limit *default-next-time-limit*))
- "Return a `scheduler' function of no arguments that returns times
- from NOW on by repeatedly calling NEXT-TIME on SCHEDULE. ALLOW-NOW-P
- is passed to the first invocation of NEXT-TIME."
- (lambda ()
- (prog1
- (setf now (next-time schedule :now now :allow-now-p allow-now-p
- :limit limit))
- (setf allow-now-p nil))))
- (defun schedule-function (function scheduler &key name
- (thread (bt:current-thread)))
- "Create a timer just as with TRIVIAL-TIMERS:MAKE-TIMER but schedule
- and reschedule FUNCTION according to SCHEDULER that is a function of
- no parameters that returns a universal time or NIL. The returned timer
- can be shut down with TRIVIAL-TIMERS:UNSCHEDULE-TIMER."
- (let (timer)
- (flet ((foo ()
- (let ((next-time (funcall scheduler)))
- (when next-time
- (trivial-timers:schedule-timer timer next-time
- :absolute-p t)))
- (funcall function)))
- (setf timer
- (trivial-timers:make-timer #'foo
- :name name
- :thread thread)))
- (let ((first-time (funcall scheduler)))
- (when first-time
- (trivial-timers:schedule-timer timer first-time :absolute-p t)))
- timer))
- ;;;; Time utilities
- ;;; From sbcl's time.lisp
- #-sbcl
- (defun leap-years-before (year)
- (let ((years (- year 1901)))
- (+ (- (truncate years 4)
- (truncate years 100))
- (truncate (+ years 300) 400))))
- (defun decode-universal-time* (time)
- "Return the decoded time components as a list instead of multiple
- values."
- (multiple-value-list (decode-universal-time time)))
- (defun encode-universal-time* (decoded-time)
- "Encode DECODED-TIME that is a decoded time in list form such as one
- that was returned by DECODE-UNIVERSAL-TIME*."
- (apply #'encode-universal-time (subseq decoded-time 0 6)))
- (defun days-of-month (decoded-time)
- "Return the number of days in the month DECODED-TIME."
- (let ((year (elt decoded-time 5))
- (month (elt decoded-time 4)))
- (+ (aref #(31 28 31 30 31 30 31 31 30 31 30 31) (1- month))
- (if (= 2 month)
- (- (leap-years-before (1+ year))
- (leap-years-before year))
- 0))))
- (defun min-valid-decoded-time-component (n)
- "Return the smallest valid value for the Nth decoded time component."
- (if (or (= n 3) (= n 4))
- 1
- 0))
- (defun max-valid-decoded-time-component (n &optional decoded-time)
- "Return the largest valid value for the Nth component of
- DECODED-TIME or NIL if there is no limit. Passing DECODED-TIME is
- necessary only for the day of month component because the number of
- days in a month varies."
- (ecase n
- ;; second and minute
- ((0 1) 59)
- ;; hour
- ((2) 23)
- ;; day of month
- ((3) (days-of-month decoded-time))
- ;; month
- ((4) 12)
- ;; year
- ((5) nil)
- ;; day of week
- ((6) 6)))
- (defun valid-decoded-time-compenent-p (n value)
- "See if value can ever be a valid value as the Nth component of a
- decoded time."
- (and (<= (min-valid-decoded-time-component n) value)
- (let ((limit (max-valid-decoded-time-component n '(0 0 0 1 1 2000))))
- (or (null limit) (<= value limit)))))
- (defun zero-decoded-time-below (decoded-time n)
- "Set the first N components of DECODED-TIME to their
- MIN-VALID-DECODED-TIME-COMPONENT values."
- (loop for i below n
- do (setf (elt decoded-time i)
- (min-valid-decoded-time-component i))))
- (defun bump-decoded-time (time n &optional (m 1))
- "Increment the Nth component of decoded TIME, handle overflows, zero
- the lower components. Return changed decoded time and the highest index
- that was changed."
- (let ((max n))
- (labels ((bump (time n)
- (setf max (max max n))
- (let ((limit (max-valid-decoded-time-component n time)))
- (cond ((or (null limit) (< (elt time n) limit))
- (incf (elt time n)))
- (t
- (setf (elt time n)
- (min-valid-decoded-time-component n))
- (bump time (1+ n)))))))
- (loop repeat m do (bump time n))
- (zero-decoded-time-below time max)
- (values time max))))
- ;;;; Cron-like schedule
- (defun next-bump (bumper decoded-time n)
- "Invoke BUMPER on the Nth component of DECODED-TIME. Return its
- value if it can ever be valid (not necessarily in the context of
- DECODED-TIME) or else NIL."
- (let* ((original (elt decoded-time n))
- (next (cond ((null bumper) original)
- ((eql bumper '*) original)
- ((numberp bumper) bumper)
- (t (funcall bumper original decoded-time n)))))
- (when (and next (not (valid-decoded-time-compenent-p n next)))
- (error "Invalid value ~S for decoded time component ~S." next n))
- (if (or (null next)
- (< next original)
- (and (= n 3)
- (< (days-of-month decoded-time) next)))
- nil
- next)))
- (defun bump-day-of-month-and-day-of-week (dom-bumper dow-bumper decoded-time)
- "Extra hair due to the circular dependency between DAY-OF-MONTH and
- DAY-OF-WEEK bumpers. This function rolls the two bumpers into one."
- ;; First let DOM-BUMPER decide what's NEXT. If it is NIL we're done.
- ;; Else ask DOW-BUMPER what it thinks. If he likes it (i.e. returns
- ;; the same day of week) then NEXT is the result. Else skip to the
- ;; day of week it said or next Monday for NIL if it is still in the
- ;; same month. Repeat.
- (let* ((decoded-time (copy-list decoded-time))
- (last-day-of-month (days-of-month decoded-time)))
- (flet ((skip-days (n)
- (incf (elt decoded-time 3) n)
- ;; Keep DECODED-TIME consistent, update DAY-OF-WEEK.
- (setf (elt decoded-time 6) (mod (+ (elt decoded-time 6) n) 7))))
- (loop while (<= (elt decoded-time 3) last-day-of-month) do
- (let* ((current-dom (elt decoded-time 3))
- (next-dom (next-bump dom-bumper decoded-time 3)))
- (unless next-dom
- (return nil))
- (skip-days (- next-dom current-dom))
- (assert (<= next-dom last-day-of-month)))
- (let* ((current-dow (elt decoded-time 6))
- (next-dow (next-bump dow-bumper decoded-time 6)))
- ;; See if the dom is also a blessed dow.
- (when (eql next-dow current-dow)
- (return (elt decoded-time 3)))
- ;; Skip until the prescribed day or next Monday.
- (skip-days (- (or next-dow 7) current-dow)))))))
- (defclass cron-schedule ()
- ((bumpers :initarg :bumpers :reader bumpers
- :documentation "The bumpers in decoded time component order."))
- (:documentation "A cron-like schedule. See MAKE-CRON-SCHEDULE for details."))
- (defun make-cron-schedule (&key second minute hour day-of-month
- month year day-of-week)
- "Construct a cron-like scheduler from the SECOND, MINUTE, etc
- bumpers for components of decoded times (using the default time zone
- for the time being). A bumper in its most generic from a function of
- three arguments: the current value, the whole decoded time, and the
- index of the current value in the decoded time. A bumper is expected
- to return the smallest value that is valid for that component and not
- less than the current value or NIL if it cannot find a valid value.
- Returning a value that is smaller than the current one is the same as
- returning NIL. A bumper can simply be a number which is equivalent to
- \(CONSTANTLY NUMBER).
- Bumpers are not allowed to depend on `lower' components of the decoded
- time. The allowed dependency graph is this:
- SECOND -> MINUTE -> HOUR -> (DAY-OF-MONTH <-> DAY-OF-WEEK) -> MONTH -> YEAR
- That is, the SECOND bumper may look at the whole decoded time but the
- MINUTE bumper may not look at seconds. DAY-OF-WEEK and DAY-OF-MONTH
- may depend on each other to allow specifying the 'last Friday of the
- month'.
- The resolution of the schedule is defined implicitly by the lowest
- bumper. NEXT-TIME always bumps the component of the decoded time that
- belongs to the lowest bumper before trying to find mathces if its
- LAST-TIME argument is not NIL. Of course, DAY-OF-WEEK is the odd one
- out: it is the day-of-month component that is bumped if DAY-OF-WEEK is
- the lowest bumper.
- This scheme allows (MAKE-CRON-SCHEDULE :MONTH 12) trigger only once
- per year instead of every second in December. For a more packed
- schedule one can use the symbol '* as a bumper: (MAKE-CRON-SCHEDULE
- :HOUR '* :MONTH 12) which makes hour the lowest bumper and the
- schedule triggers every hour in December.
- It is an error if all bumpers are NIL."
- (let ((bumpers (list second minute hour day-of-month month year day-of-week)))
- (unless (some #'identity bumpers)
- (error "Cannot determine resolution of an empty schedule."))
- (make-instance 'cron-schedule :bumpers bumpers)))
- (defun bumper-index->component-index (i)
- "Return the index of the decoded time component that is effect by
- bumper I. Day of week is lumped together with day of month."
- (if (= i 6) 3 i))
- (defun lowest-component-index-with-a-bumper (bumpers)
- "Return the the index of what is basically the root of current
- dependency graph."
- (loop for i upfrom 0
- for bumper in bumpers
- when bumper
- minimize (bumper-index->component-index i)))
- (defun bump-lowest-component (bumpers time)
- "Bump the lowest component of decoded TIME that has a bumper. Return
- it as a universal time."
- (let ((decoded-time (decode-universal-time* time)))
- (encode-universal-time*
- (bump-decoded-time decoded-time
- (lowest-component-index-with-a-bumper
- bumpers)))))
- (defmethod next-time ((schedule cron-schedule) &key (now (get-universal-time))
- allow-now-p (limit *default-next-time-limit*))
- (let ((bumpers (bumpers schedule)))
- (unless allow-now-p
- (setf now (bump-lowest-component bumpers now)))
- (loop while (< now limit)
- with n = 5
- for decoded-time = (decode-universal-time* now)
- for next = (if (= n 3)
- (bump-day-of-month-and-day-of-week (elt bumpers 3)
- (elt bumpers 6)
- decoded-time)
- (next-bump (elt bumpers n) decoded-time n))
- do
- (cond ((null next)
- (when (= n 5)
- ;; The desired year is in the past, there is no next
- ;; time.
- (return nil))
- ;; No valid value for this component, bump the next one
- ;; and come again.
- (multiple-value-setq (decoded-time n)
- (bump-decoded-time decoded-time (1+ n))))
- (t
- (when (< (elt decoded-time n) next)
- (setf (elt decoded-time n) next)
- (zero-decoded-time-below decoded-time n))
- (decf n)))
- (setf now (encode-universal-time* decoded-time))
- (when (minusp n)
- (return now)))))
- ;;;; The convenience case: typed cron schedule
- (defun find-decoded-time-component-by-type (type value decoded-time n)
- "Return the first valid value not less than VALUE that is of TYPE."
- (loop with limit = (max-valid-decoded-time-component n decoded-time)
- for x upfrom value below limit
- do (when (typep x type)
- (return-from find-decoded-time-component-by-type x))))
- (defun make-typed-cron-bumper (type)
- "Return a bumper function suitable for MAKE-CRON-SCHEDULE that
- returns the first valid value according to TYPE. Convenience function
- on top of FIND-DECODED-TIME-COMPONENT-BY-TYPE."
- (lambda (value decoded-time n)
- (find-decoded-time-component-by-type type value decoded-time n)))
- (defun make-typed-cron-schedule (&key second minute hour day-of-month
- month year day-of-week)
- "A convenience function much like MAKE-CRON-SCHEDULE but assumes
- that no bumper can be a function designator so it must be a number,
- the symbol * or a type specifier in which case it calls
- MAKE-TYPED-CRON-BUMPER on it providing a terser syntax."
- (flet ((convert-bumper (bumper)
- (if (or (null bumper)
- (typep bumper 'number)
- (eq bumper '*))
- bumper
- (make-typed-cron-bumper bumper))))
- (let ((bumpers (list second minute hour day-of-month month year
- day-of-week)))
- (make-instance 'cron-schedule
- :bumpers (mapcar #'convert-bumper bumpers)))))
|