普通のタイマー

xyzzyにはタイマー機能がある。javascriptのsetInterval()と同じようなもので、次のように書ける。

;一回のみ
(start-timer 1 'ding t)

;繰り返し
(progn
  (start-timer 0.5 'ding)
  (start-timer 0.6 'ding)
  (start-timer 0.7 'ding)
  (start-timer 0.8 'ding))

;↑のフォロー
(dotimes (a 4)(stop-timer 'ding))

タイマー機能はこれで良いのだが、時刻指定で設定もしたいし、秒指定だけでなく、分や時間での指定もしたい。というわけで以下のものを作ってみた。

(defun set-timer (fn &key second minute hour day month year relative)
  "タイマーをセットする。"
  (let* ((relate-time (lambda (&optional s m h d mon y)
                        (+ (* (+ (* (+ (* (+ (or d 0) (* (or mon 0) 30)
                                             (* (coerce (or y 0) 'double-float)
                                                365)) 24) (or h 0)) 60) (or m 0)) 60) (or s 0))))
         (time
          (if relative
              (funcall relate-time second minute hour day month year)
            (multiple-value-bind (second0 minute0 hour0 day0 month0 year0)
                (get-decoded-time)
              (counter-default (second  minute  hour  day  month  year)
                               (0       0       0     1    1)
                               (second0 minute0 hour0 day0 month0 year0))
              (- (encode-universal-time second minute hour day month year)
                 (get-universal-time)))))
         (adder (list 1)))
    (while (minusp time)
      (let ((new (+ (rem time 3.1536d7) (apply relate-time adder))))
        (if (<= 0 new) (setq time new) (push nil adder))))
    (make-time-waiter time fn 2147484)
    (message "~A を ~A くらいに実行します。"
             fn (format-date-string "%Y-%m-%d %H:%M:%S" (+ (get-universal-time) (floor time))))))


(defun make-time-waiter (time fn limit)
  (if (< time limit)
      (start-timer time fn t)
    (start-timer (1- limit) (lambda () (make-time-waiter (- time (1- limit)) fn limit)) t)))

(defmacro counter-default (slots resets &optional (defaults nil sv))
  (let ((f (lambda (l l2 &optional l3)
             `(if (null ,(car l))
                  (if (or ,@(cdr l))
                      (setq ,(car l) ,(car l2))
                    ,(if l3 `(setq ,(car l) ,(car l3)) ni)))))
        (args (append `(,slots) `(,(append resets '(nil))) (if sv `(,defaults)))))
    (append '(progn)
            (let ((l (apply 'maplist f args)))
              (rplacd (cdar (last l)) (cdddr (caddar (last l)))) l))))

;時刻を指定してセット
(set-timer 'ding :minute 2 :hour 21)
|
ding を 2009-07-22 21:02:00 くらいに実行します。

(set-timer 'ding :minute 15 :hour 1)
|
ding を 2009-07-23 01:15:00 くらいに実行します。

(set-timer 'ding :year 2112)
|
ding を 2112-01-01 00:00:00 くらいに実行します。

;現在からの時間を指定してセット
(set-timer 'ding :second 1 :relative t)
|
ding を 2009-07-22 20:23:36 くらいに実行します。

(set-timer 'ding :hour 3 :relative t)
|
ding を 2009-07-22 23:27:50 くらいに実行します。

(set-timer 'ding :year -3000 :relative t)
|
ding を 2009-07-22 20:27:16 くらいに実行します。

基本は時刻指定なので、一度しか実行しないが、実行の際にまた呼び出せば繰り返せるので問題はないだろう。


時刻指定モードと相対時間指定モードがある。これは :relative が nil か t かで判断する。


時刻指定モードでは、省略した部分は現在と同じものを入れるが、時間などを設定したら、それ以下の数値をリセットするようにした。9時っていったら普通は9時丁度のことで、9時の今の分、秒ではないだろう。これの記述が面倒なのでマクロになっている*1


また、21時の時点で9時といったら、今日の9時ではなく、明日の9時だろう。ということで、過去の時刻を指定していたら位ごとに進めてみて未来を指すようにしている。


相対時間指定モードでは、時刻設定で使うキーワードをそのまま流用して時間を表すようにしている。ちなみに、こちらのモードでは小数は使えるが、時刻指定では小数は使えない。start-timerは小数も受け付けるが、encode-universal-timeは小数に対応していないのだ。


yearの項目もあるので、適当に入れるとstart-timerのカウンタが溢れてしまう。仕方がないので、必要かどうかは解らないが、時間が来るまで延長し続ける関数も用意してみた。通常だと2週間程度までしか登録できないが、これがあれば何年でもいける。そうなる、とxyzzyを再起動したときも継続してタイマーを使いたくなるのだが、それはまた別の話。

*1:マクロの方が展開形より長いのは秘密である。