diary-float による org-mode の日付作成 (2)
elisp org-modeTable of Contents
#+File Created:
#+Last Updated:
このメモは, diary-float による org-mode の日付作成 の続きである.
1 これまでのあらすじ
emacs org-mode でタスクを管理している.
複雑な繰り返し日付をルールに基づき書いておきたい.
いろいろ調べたら, diary-float 形式で日付を書くという方法があることを知った.
今日がそのタスクを行う日であれば t, そうじゃなければ nil を返す関数を自分で作ることで,
*** TODO タスク1 SCHEDULED: <%%(diary-lecture '(04 01 2019) 7 15 '(3 10))>
こんな感じで書いておくと, この関数が t を返す日付でこのタスクがスケジューリングされる.
(一応 diary-lecture 関数の意味を書いとくと, 2019/04/01 から, 7 日おきに 15 回やる. ただし 3 回目と 10 回目は休み)
これでいちいち具体的な日付を書く必要がなくなった.
よかった.
ここまでが以前のはなし.
2 はじめに
これはこれでまぁ使えるようにはなったのだが, org-mode のプログラムの中では日付が
例えば org-habit とか. このままでは agenda-view で STYLE: habit としたときのグラフが出てこない.
あるいは org-gcal とか. 具体的日付じゃないと Google Calendar との連携ができない.
これをなんとかするためには, かなり色んな関数を advice を使って書き換えたり直接上書きしたりしないといけないのであった.
出来たことはできたのだが結構汚いプログラムになってしまい何か嫌になってきた.
org-mode の version up についていくためにも, もう少し何かシンプルにならんもんか…
一応現在の環境を書いておく.
emacs は 26.1, org-mode は 8.2.10 である.
3 問題点
タスクスケジューリングの日付に diary-float 形式の関数を直接書かないで同様の機能を持たせることは出来ないか?
4 解決策
こんな感じで書くことが出来ないか考えてみた.
*** TODO タスク1 SCHEDULED: <2019-04-01 Mon 10:40 +7d> :PROPERTIES: :DIARYF: %%(diary-lecture '(04 01 2019) 7 15 '(3 10)) :END:
diary-float 形式の日付ルールは, DIARYF property の中に書いておくようにする.
スケジュールの最初の日付は具体的な 形式で書かれている.
問題は次が何日後かってことなので, ここを diary-float 形式の式を評価することで計算し書き出し(直し)たい.
例えばこのタスクを DONE したとき, +7d を見ることで次回の日付が書き出される.
*** TODO タスク1 SCHEDULED: <2019-04-08 Mon 10:40 +7d> :LOGBOOK: - State "DONE" from "TODO" [2019-04-01 Fri 12:20] :END: :PROPERTIES: :DIARYF: %%(diary-lecture '(04 01 2019) 7 15 '(3 10)) :END:
普通はこんな感じだけど, DIARYF property がある場合にはこれを見て次のスケジュールがほんとは何日後かを再計算する.
この例の場合, 3 回目は休講だから次は 14 日後になる.
最終的には以下のようになる.
*** TODO タスク1 SCHEDULED: <2019-04-08 Mon 10:40 +14d> :LOGBOOK: - State "DONE" from "TODO" [2019-04-01 Fri 12:20] :END: :PROPERTIES: :DIARYF: %%(diary-lecture '(04 01 2019) 7 15 '(3 10)) :END:
これがうまく出来ればなんとかなるんではないだろうか.
5 結果
まずは以下のような引数をもった簡単な関数をつくる.
sexp="%%(diary-lecture '(04 01 2019) 7 15 '(3 10))"
day ="'(4 7 2019)"
pdays=60
day から pdays 日以内で sexp を満たすもっとも近い日付が今日から何日後かを計算する.
1: (defun diary-float-to-diff(sexp day &optional pdays) 2: "sexp を満たす一番近い日付が day から何日後か?" 3: (let ((ii 0) 4: (cdate nil) 5: (result nil)) 6: (unless pdays (setq pdays 0)) 7: (if (string-match "^%%" sexp) (setq sexp (substring-no-properties sexp 2))) 8: (catch 'break 9: (while (< ii (1+ pdays)) 10: (setq cdate (my-calendar-format-nth-day-after day ii)) 11: (setq result (org-diary-sexp-entry sexp t cdate)) 12: (if result (throw 'break nil)) 13: (incf ii) 14: )) 15: (if result 16: ii ;; 次の予定日まで何日分あるか?を返す. 17: -1 ;; 見つからなければ -1 を返す. 18: ) 19: )) 20: 21: (defun my-calendar-format-nth-day-after(dayl n) 22: "dayl='(50 7 20 12 5 2017 5 nil 32400) n=2 => return '(5 14 2017)" 23: (let ((unday (apply #'encode-time dayl))) 24: (my-calendar-format (decode-time (time-add unday (days-to-time n)))) 25: )) 26: (defun my-calendar-format(dayl) 27: "dayl='(50 7 20 12 5 2017 5 nil 32400) => return '(5 12 2017)" 28: (list (nth 4 dayl) (nth 3 dayl) (nth 5 dayl)))
これを利用して,
SCHEDULED: <2019-04-08 10:40>
とか
SCHEDULED: <2019-04-08 10:40 +7d>
を DIARYF に基づき
SCHEDULED: <2019-04-08 10:40 +14d>
に書き換えるプログラムとして以下を作成.
1: (defun my-org-diary-float-next-schedule-diff() 2: "DIARYF: プロパティの diary-float 形式日付(habit)があれば 3: 読み込んで次回のスケジューリングを行う. 4: 具体的には, SCHUEDULED: <日付> => SCHEDULED: <日付 +14d> とする. 5: (次の予定が 14 日後にあると diary-float 内の関数で 6: 計算された場合)" 7: (interactive) 8: (let (elem sexp unix-sch-date unix-sch-next-date lst-sch-next-date 9: next-diff sch-format sch-format-with-repeat) 10: (save-excursion 11: (end-of-line) 12: (org-back-to-heading) 13: (setq elem (org-element-headline-parser (point-max) t)) 14: (setq sexp (org-element-property :DIARYF elem)) ;; PROPERTIES: の中身のデータを取り出す方法 15: (if sexp 16: (progn 17: ;; SCHEDULED の日付 18: (setq unix-sch-date (org-get-scheduled-time (point))) ;;UNIX date 19: ;; 次の日にする. 20: (setq unix-sch-next-date (time-add unix-sch-date (days-to-time 1))) 21: ;; 日付リスト形式 (0 0 0 11 3 2019 0 nil 32400) に変換 22: (setq lst-sch-next-date (decode-time unix-sch-next-date)) 23: ;; 次のスケジュールまでの日付計算(次の日から計算してるので + 1 を入れておく) 24: (setq next-diff (+ 1 (diary-float-to-diff sexp lst-sch-next-date org-gcal-down-days))) 25: (setq sch-format (format-time-string "%Y-%m-%d %a %H:%M" unix-sch-date)) 26: ;; 時間が指定されてないときは " 00:00" を削除する. 27: (if (string-match " 00:00" sch-format) (setf (substring sch-format (match-beginning 0) (match-end 0)) "")) 28: (if (= next-diff 0) 29: ;; diary-float での次スケジュール日付が見つからないとき. 30: (progn 31: (setq sch-format-with-repeat (format "SCHEDULED: <%s>" sch-format)) 32: (org-back-to-heading) 33: (re-search-forward "\\(SCHEDULED: <\\(.*?\\)>\\)" (save-excursion (outline-next-heading) (point))) 34: (replace-match sch-format-with-repeat) 35: ) 36: ;; スケジュールが見つかったら +nd の追加 37: (setq sch-format-with-repeat (format "SCHEDULED: <%s %s>" sch-format (concat "+" (number-to-string next-diff) "d"))) 38: (org-back-to-heading) 39: (re-search-forward "\\(SCHEDULED: <\\(.*?\\)>\\)" (save-excursion (outline-next-heading) (point))) 40: (replace-match sch-format-with-repeat) 41: ) 42: ) 43: )) 44: ))
タスクを DONE にしたときに自動で呼ばれるようにする. advice を使った.
1: (defadvice org-todo(after my-org-diary-float-property-hook) 2: "" 3: (let* ((end (my-org-get-end-head)) 4: (elem (org-element-headline-parser end t))) 5: (if (org-element-property :DIARYF elem) (my-org-diary-float-next-schedule-diff)) 6: )) 7: (ad-activate-regexp "my-org-diary-float-property-hook")
とりあえずこれで望みの動きになった.