do not edit — generated by btf.
git.druid.rocksindexdruid520radiumradium-lang-cron.el

radium-lang-cron.el


;;; radium-lang-cron.el, crontab editing + next-run preview -*- lexical-binding: t; -*-
 
(defun radium-cron-parse-field (str lo hi)
  "i parse cron field STR into the sorted list of valid ints in [LO,HI]
(it supports lists \"a,b\", ranges \"a-b\", steps \"a-b/n\" or \"*/n\")."
  (let (result)
    (dolist (part (split-string str ","))
      (let (base step)
        (if (string-match "\\`\\(.+\\)/\\([0-9]+\\)\\'" part)
            (setq base (match-string 1 part) step (string-to-number (match-string 2 part)))
          (setq base part step 1))
        (let (range-lo range-hi)
          (cond
           ((string= base "*") (setq range-lo lo range-hi hi))
           ((string-match "\\`\\([0-9]+\\)-\\([0-9]+\\)\\'" base)
            (setq range-lo (string-to-number (match-string 1 base))
                  range-hi (string-to-number (match-string 2 base))))
           (t (setq range-lo (string-to-number base) range-hi range-lo)))
          (let ((v range-lo))
            (while (<= v range-hi)
              (push v result)
              (setq v (+ v step)))))))
    (sort (delete-dups result) #'<)))
 
(defun radium-cron-days-in-month (y m)
  (let ((days [31 28 31 30 31 30 31 31 30 31 30 31]))
    (if (and (= m 2) (or (zerop (mod y 400))
                         (and (zerop (mod y 4)) (not (zerop (mod y 100))))))
        29
      (aref days (1- m)))))
 
(defconst radium-cron-search-years 100
  "how many years ahead radium-cron-next-runs searches before giving
up - matters only for schedules sparse enough to need more than a
handful of years to find n runs (day=29 month=2 fires roughly once
every 4-8 years, the sparsest a real 5-field schedule gets).")
 
(defun radium-cron-next-runs (line n)
  "i compute the next N times cron LINE (a 5-field schedule, with an
optional trailing command which i ignore) will fire, starting after
now - searching at most radium-cron-search-years years ahead. may
return fewer than N: either the schedule matches no real date at all
\(a bogus day=30 month=2, say - true forever, not a search-window
artifact) or genuinely needs longer than radium-cron-search-years to
find N runs - this function alone can't tell the two apart, so callers
that care should compare the result length against N themselves."
  (let* ((fields (split-string (string-trim line) "[ \t]+" t)))
    (unless (>= (length fields) 5)
      (user-error "err: not a 5-field cron schedule: %S." line))
    (let* ((mins (radium-cron-parse-field (nth 0 fields) 0 59))
           (hrs  (radium-cron-parse-field (nth 1 fields) 0 23))
           (doms (radium-cron-parse-field (nth 2 fields) 1 31))
           (mons (radium-cron-parse-field (nth 3 fields) 1 12))
           (dows (mapcar (lambda (d) (if (= d 7) 0 d)) (radium-cron-parse-field (nth 4 fields) 0 7)))
           (dom-star (string= (nth 2 fields) "*"))
           (dow-star (string= (nth 4 fields) "*"))
           (now (current-time))
           (year0 (nth 5 (decode-time now)))
           (results '()))
      (catch 'enough
        (dolist (y (number-sequence year0 (+ year0 radium-cron-search-years)))
          (dolist (mo mons)
            (dolist (d (number-sequence 1 (radium-cron-days-in-month y mo)))
              (let ((dow (nth 6 (decode-time (encode-time 0 0 0 d mo y)))))
                (when (cond ((and dom-star dow-star) t)
                            (dom-star (memq dow dows))
                            (dow-star (memq d doms))
                            (t (or (memq d doms) (memq dow dows))))
                  (dolist (h hrs)
                    (dolist (mi mins)
                      (let ((candidate (encode-time 0 mi h d mo y)))
                        (when (time-less-p now candidate)
                          (push candidate results)
                          (when (>= (length results) n)
                            (throw 'enough nil))))))))))))
      (nreverse results))))
 
(defun radium-cron-preview-next-runs ()
  "i show the next 5 times the cron schedule on the current line will fire."
  (interactive)
  (let* ((line (thing-at-point 'line t))
         (times (radium-cron-next-runs line 5)))
    (message "ok: next 5 runs: %s.%s"
             (mapconcat (lambda (tm) (format-time-string "%Y-%m-%d %H:%M %a" tm)) times ", ")
             (if (< (length times) 5)
                 (format " (only %d found within %dy - a sparse schedule, or check for a bogus date like feb 30)"
                         (length times) radium-cron-search-years)
               ""))))
 
(use-package crontab-mode
  :mode (("\\.cron\\(tab\\)?\\'" . crontab-mode)
         ("cron\\.d/" . crontab-mode))
  :bind (:map crontab-mode-map ("C-c C-r" . radium-cron-preview-next-runs)))
 
(provide 'radium-lang-cron)
powered by btf.