#+File Created: <2015-05-24 Sun 16:31>
#+Last Updated: <2018-02-21 Wed 19:38>

概要:
emacs/org-mode での予定の作成方法について,
特に繰り返して起こるタスクや予定のスケジュール(日時)作成についてのはなし.


1 はじめに

私はエディタとして emacs を使っており, 予定やタスクを org-mode のファイルで管理している.
日時が定まっている予定は

SCHEDULED: <2015-05-24 Sun 19:00>

のような感じで書いておけばよい.
しかし, 一回やればそれで終わりではないことや,
定期的に繰り返して起こるような予定やタスクに関して,
日付をどのように書けばいいのかよくわからなかったのでしらべてみた.

org-mode の version は 8.2.10, emacs の version は 24.5.4 です.

2 問題点

週単位で動いたり月単位で動くような仕事をしている場合,
それらを周期とした少し複雑な予定を書く必要に迫られることがあったりする.
例えば,

  • 毎月月末の木曜日には定例会議がある.
  • 毎週月, 水, 金にやることがある.
  • 4/11 から 15 週に渡って毎週水曜日にやることがあるが,
    3 週目と 4 週目はやらない.
    その代わり 7/3 と 7/4 にちにその代わりをやる.

とか.
私の場合, 週及び月単位, 四半期単位での繰り返しがあり,
上のような結構めんどくさい条件もあったりすることがわかった.
最初は手で予定を書き換えてたのだが, 一応ルールがあるのに手で修正するのはめんどくさいし, 何とか自動で出来ないかなぁと思っていた.
いくつか調べたが, こういうのはもしかして org-mode のカレンダー日付で書くやりかたではちょっと表現しにくい, というか出来ないんじゃないかなーと思ったのであった.
例えば

<2015-05-24 Sun +1w>

とかは +1w で毎週を示せますが週一回やることしか表現できないし,
月末の最後の金曜日のつもりで

<2015-05-29 Fri +1m>

のように書いても, これが終わった後の次の月の予定は

<2015-06-29 Mon +1m>

に書き換えられてしまいます.
+1m はひと月後という意味なので間違ってはないんだけど…
曜日を揃えるにはどーすればいいんだろう.
こっちの方が何かと使うと思うんだけど. 外人は毎月末の金曜日に会議, とかいう予定は無いんかなぁ.
それとも自分が知らない何かワザがあるんだろうか…
この辺何とかならないかなぁというのがここでの問題点である.

3 解決策

色々調べたけど日付の後ろに +1w とかで修飾していくようなやり方では出来ないみたい?
探してたら出てきたのが diary-float (diary-lib.el) というやつである.

以下のようにかく. diary パッケージの S 式というらしい.

<%%(diary-float t 4 2)>

何これ.

4 結果

調べてみた結果を以下にまとめた.

4.1 毎月月末の特定曜日を指定

毎月第二木曜日 の場合は以下のように書く.

<%%(diary-float t 4 2)>

パラメータの意味は
毎月: t
木曜日: 4 (0:日 1:月 2:火 3:水 4:木 5:金 6:土)
第二: 2

10 月の第二木曜日は,

<%%(diary-float 10 4 2)>

10,11,12 月の第二木曜日は

<%%(diary-float '(10 11 12) 4 2)>

更にしらべたら, 毎月月末の週の木曜日の会議は以下のように書けることが判明した.
-1 で月末になるっぽい. ナイスな感じ.

<%%(diary-float t 4 -1)>

何と, 結構わかりやすいかも.

4.2 毎週特定曜日(複数)を指定

<%%(memq (calendar-day-of-week date) '(1 3 5))>

date というのは何なのか…
書かせてみたりして調べてみると,
どうやらこの行を評価した日付が '(12 13 2015) みたいな形式で date の中に入るっぽい.

ちなみに date の書かせ方は

(y-or-n-p (message "date=%s" date))

4.3 毎週特定曜日にやることがある(始まり, 終わりなどその他複雑な条件あり)

http://orgmode.org/worg/org-faq.html#Appointments/Diary
に例となる関数(diary-limited-cyclic)があったので,
これを参考に見よう見まねで自分でも作ってみよう!!

要は今日(date)が条件を満たしてれば t を返し, 満たしてなければ nil を返す,
そんなプログラムを作ればいいんじゃないでしょうか.
私は断固そー思うわけです.

(defun diary-lecture(stt ival recc &optional exs &rest sbs)
  (let* (
         (sttd  (calendar-absolute-from-gregorian stt ))  ;; stt の日付を 6 桁数値で
         (today (calendar-absolute-from-gregorian date))  ;; 今日の日付
         (diffd (- today sttd)) ;; stt の日付と今日の差分. stt より今日の方が後であれば >0
         (nths   nil)  ;; 今日は何周目かを得る
         (jst    nil)  ;; 戻り値 t or nil
         )
    ;; diary-limited-cyclic を参照
    (if (and (not (minusp diffd))
             (zerop (% diffd ival))
             (< (floor diffd ival) recc))
        (setq nths (+ (/ diffd ival) 1)))

    ;; 上の条件を満たしていれば nths に何か値が入ってる
    (if nths
        (setq jst (diary-lecture-exception nths exs))
      (setq jst (diary-lecture-substitution today sbs))
      )
    jst))

;; 今日が例外週(exs) であれば nil を返す
(defun diary-lecture-exception(nths exs)
  (let ((jst t)
        (ex  nil))
    (while exs
      (setq ex (car exs))
      (if (= nths ex) (setq jst nil))
      (setq exs (cdr exs)))
    jst))

;; 今日が代わりの日付であれば t を返す
(defun diary-lecture-substitution(today sbs)
  (let ((jst nil)
        (sb  nil)
        (sbg nil))
    (while sbs
      (setq sb (car sbs))
      (setq sbg (calendar-absolute-from-gregorian sb))
      (if (= today sbg) (setq jst t))
      (setq sbs (cdr sbs)))
    jst))
(diary-lecture stt ival recc &optional exs &rest sbs)

引数の意味は以下です:

  • stt: 始まりの日 '(4 11 2017)
  • ival: 何日おき 7
  • recc: 繰り返し回数 16
  • exs: 除外週 '(3 4 6)
  • sbs: 代わりの日付(配列) '(5 11 2017) '(3 11 2017)

&optional 以降の引数(exs, sbs) は無くてもいい
&rest 右隣の引数(sbs) は, これ以降の変数のリストは全て sbs に入るという意味

例:
2017/04/14 から毎週, 17 回やる. 4 回目と 10 回目は休み. その代わり 2018/08/08 2018/08/09にやる

SCHEDULED: <%%(diary-lecture '(4 14 2017) 7 17 '(4 10) '(8 8 2018) '(8 9 2018))>

追記:
diary-lecture を半年程使ってましたが, 繰り返し回数, 除外週のような, 始まりの週を 1 週目として何周目をというのを数えるのは超めんどくさいことが判明した.
いちいちカレンダーを見て指折り数えないといけない. やってられないので diary-lecture2 を作った.

;; 繰り返し回数は使いにくいので日付で指定する
;; stt:    始まりの日 '(4 11 2018)
;; ival:   何日おき   7
;; end:    終了日     '(8 10 2018)
;; exdays: 除外日     '((5 11 2018) (3 11 2018))
;; sbdays: 代わり日   '((5 12 2018) (3 12 2018))
(defun diary-lecture2(stt ival end &optional exdays sbdays)
  (let* (
         (sttd  (calendar-absolute-from-gregorian stt ))  ;; stt の日付を 6 桁数値で
         (today (calendar-absolute-from-gregorian date))  ;; 今日の日付
         (endd  (calendar-absolute-from-gregorian end))   ;; end の日付を 6 桁数値で
         (diffd (- today sttd))
         (diffe (- endd  today))
         (nths   nil)  ;; 今日は何周目かを得る
         (jst    nil)  ;; 戻り値 t or nil
         )
    (if (and (not (minusp diffd))
             (not (minusp diffe))
             (zerop (% diffd ival)))
        (setq nths (+ (/ diffd ival) 1)))

    ;; 上の条件を満たしていれば
    (if nths
        (setq jst (not (diary-lecture-substitution today exdays)))
      (setq jst (diary-lecture-substitution today sbdays)))
    jst))

4.4 その他いくつか作ったプログラム

いちおう何となく作り方がわかったんで, 必要に応じていくつか書いてみた.
基本すべて同じぱたーんで書ける筈!!

月末にやることを指定.
月の締めの作業とか.
2015/03/07 以降の月末日にやることを指定.

<%%(diary-habit-last-day-of-month '(3 7 2015)>
(defun diary-habit-last-day-of-month(stt)
  (let* ((jst0 nil)
         (jst  nil)
         (sttd  (calendar-absolute-from-gregorian stt ))
         (today (calendar-absolute-from-gregorian date))
         (diffd (- today sttd))
         ;; (calendar-last-day-of-month 3月 2017年) ;=> 31 日
         (lday (calendar-last-day-of-month (nth 0 date) (nth 2 date)))
         )
    (if (not (minusp diffd)) (setq jst0 t))
    (if (and jst0 (= (nth 1 date) lday)) (setq jst t))
    jst
    ))

2015/03/07 以降の月水金だけやることを指定.

<%%(diary-habit-weekday '(3 7 2015) 1 3 5)>
(defun diary-habit-weekday(stt &rest wds)
  (let* ((jst0 nil)
         (jst  nil)
         (sttd  (calendar-absolute-from-gregorian stt ))
         (today (calendar-absolute-from-gregorian date))
         (diffd (- today sttd)))
    ;(y-or-n-p (message "date=%s" date))
    (if (not (minusp diffd)) (setq jst0 t))
    (if (and jst0 (not wds)) (setq jst  t)) ;; 毎日
    (if (and jst0 wds)       (setq jst (diary-habit-weekday-week date wds)))
    jst
    ))

(defun diary-habit-weekday-week(date wds)
  (let ((wd nil)
        (jst nil))
    (while wds
      (setq wd (car wds))
      (if (= wd (calendar-day-of-week date)) (setq jst t))
      (setq wds (cdr wds))
      )
    jst))

指定した月の毎日やることを指定, 除外曜日があればそれも指定.
2015/05 の毎日. 但し火曜(2)水曜(3)を除く.

<%%(diary-every-day-in-month2 2015 5 2 3)>
(defun diary-every-day-in-month(y m &rest wds)
  (let* (
         (l     (calendar-last-day-of-month m y))
         (endd  (calendar-absolute-from-gregorian (list m l y)))
         (sttd  (calendar-absolute-from-gregorian (list m 1 y)))
         (today (calendar-absolute-from-gregorian date))
         (jst0  nil)
         (jst   nil)
         )
    (if (and (not (minusp (- today sttd)))
             (not (minusp (- endd  today))))
        (setq jst0 t))
    (if (and jst0 (not wds)) (setq jst  t)) ;; 毎日
    (if (and jst0 wds)       (setq jst (not (diary-habit-weekday-week date wds))))
    jst
   ))

毎月やる会議の指定.
2017/04/01 から 2018/03/31 まで, ある委員に任命されてしまった…
月末の金曜日に会議がある.
だけど夏休みの 8 月と春休みの 2 月には会議がない.
その代わり 2017/05/08 と 2017/03/11 に余計な会議がある予定なのであった.
そんな場合の予定として, 以下のように書く私であった.

<%%(diary-monthly-meeting '(4 1 2017) '(3 31 2018) 5 -1 (8 2) '(5 8 2017) '(3 11 2017))>
  • stt: はじまりの日: '(4 1 2017)
  • end: 終わりの日: '(3 31 2018)
  • 何曜日(week)
  • 第何週(num)
  • 除外月(exs): (5 8)
  • 代わりの日付(配列) sbs = '(5 8 2017) '(3 11 2017)
(defun diary-monthly-meeting(stt end week num &optional exs &rest sbs)
  (let* (
         (doweek (calendar-day-of-week   date)) ;; 今日の曜日(1-7)
         (month  (calendar-extract-month date)) ;; 月(1-12)
         (sttd   (calendar-absolute-from-gregorian stt))
         (endd   (calendar-absolute-from-gregorian end))
         (today  (calendar-absolute-from-gregorian date))
         (diffs  (- today sttd))
         (diffe  (- endd  today))
         (ist    nil)
         (dst    nil)
         (mst    t)
         (sst    nil)
         (jst    nil) ;; 戻り値 t or nil
         )
    ;; 少なくとも今日が stt - end の間に無いと nil
    (if (and (not (minusp diffs)) (not (minusp diffe))) (setq ist t))
    ;; 毎月 num 週 week 曜日なら t
    (if (diary-float t week num) (setq dst t))
    ;; 除外月が指定されてて丁度その月なら nil
    (setq mst (diary-months-excepts month exs))
    ;; 普通の場合
    (setq jst (and ist dst mst))
    ;(y-or-n-p (message "ist=%s" ist))
    ;(y-or-n-p (message "dst=%s" dst))
    ;(y-or-n-p (message "mst=%s" mst))
    ;(y-or-n-p (message "jst=%s" jst))
    ;(if (and ist (and dst mst)) (setq jst t))
    ;; 今日が代わりの日であれば t
    (unless jst
      (setq jst (diary-lecture-substitution today sbs)))
    jst
    ))

(defun diary-months-excepts(month exs)
  (let ((mst t) (ex nil))
    (if exs
        (progn
          (while exs
            (setq ex (car exs))
            (if (= ex month) (setq mst nil))
            (setq exs (cdr exs))
            )
          )
      )
    mst))

5 感想

結構すっきりしてわかりやすいと思ったので, 繰り返し予定やタスクのスケジューリングは
全てこの形式で統一した方がいいんじゃないかなーとか思ったが…
org-habit とか org-gcal とかを使おうとするといまいちな感じになってしまう.
これらに対する修正はまた別の記事で書こうかと思う.

6 参考 URL

Org-mode Frequently Asked Questions
http://orgmode.org/worg/org-faq.html#Appointments/Diary

GNU Emacs Lispリファレンスマニュアル: Sexp Diary Entries
http://www.geocities.co.jp/SiliconValley-Bay/9285/ELISP-JA/elisp_654.html

GNU Emacs Manual: カレンダーとダイアリー
http://www.bookshelf.jp/texi/emacs-24.5/emacs_33.html

GNU Emacs Lispリファレンス・マニュアル - 関数
http://bit.ly/2mZvhnD

Lispプログラミング入門
http://bach.istc.kobe-u.ac.jp/lect/ProLang/org/lisp.html

Org-mode, Emacs, and Getting Things Done
http://members.optusnet.com.au/~charles57/GTD/index.html

Org mode for Emacs: あなたの生活をプレーンテキストで
http://orgmode.org/ja/index.html

Org Mode マニュアル
http://orgmode.jp/doc-ja/org-ja.html

Emacs org-modeを使ってみる - 屯遁のパズルとプログラミングの日記
http://d.hatena.ne.jp/tamura70/20100203/org

How to create calendar entry for last weekday of every month? - Emacs Stack Exchange
https://emacs.stackexchange.com/questions/30448/how-to-create-calendar-entry-for-last-weekday-of-every-month