折腾Emacs也有一段时间了,最近发现学校查看近期的课程很不方便,于是就想用org-mode来录入学校的课程表,然后用org-agenda来进行显示和时间规划, 顺便分享一下自己的一点经验,希望大家能有所收获。
本人学校的课表如下:
本人开始录入课程表时主要面临有几个问题:
- 学校课程的日期安排,不是以日期的形式来安排的,而是以开学以来的周数+周几来表示的,如果录入时手动换算会比较麻烦。
- 学校课程的安排比较灵活,很多课是以周数的区间形式来安排的,比如 1~14,15,17~18周,我希望能以类似的格式直接进行录入。
- 学校每天课程时间安排比较固定,比如每天的第一节课就固定是 8:00~9:35 ,重复地输入这个时间段显然是比较繁琐,也比较容易输错。
所以本人就开始思考解决的办法: 首先是针对第一和第二个问题,可以使用sexp时间戳,然后在里面调用自定义的函数。
(require 'cal-iso)
(setq school-term-start-date '(9 6 2021)) ;定义开学周
(defun iso-week-to-date (week day year)
"从ISO时间标准周转换为日期"
(calendar-gregorian-from-absolute
(calendar-iso-to-absolute (list week day year))))
(defun iso-week-from-date (month day year)
"从日期转换为ISO时间标准周"
(calendar-iso-from-absolute
(calendar-absolute-from-gregorian (list month day year))))
(defun school-week-to-date (week day)
"把学期开始的周数和周几转换为日期"
(let ((week (+ (nth 0 (apply 'iso-week-from-date school-term-start-date)) week -1)))
(iso-week-to-date week day (calendar-extract-year school-term-start-date))))
(defun school-class (weeks day)
"输入周数和周几,并判断和org-agenda当前检索的日期是否匹配"
(let ((ret nil))
(dolist (week weeks)
(pcase week
((pred consp)
(let* ((date1 (school-week-to-date (car week) day))
(date2 (school-week-to-date (cdr week) day))
(year1 (calendar-extract-year date1))
(month1 (calendar-extract-month date1))
(day1 (calendar-extract-day date1))
(year2 (calendar-extract-year date2))
(month2 (calendar-extract-month date2))
(day2 (calendar-extract-day date2))
(ent (org-class year1 month1 day1 year2 month2 day2 day)))
(if ent (setq ret ent))))
((pred integerp)
(let* ((date1 (school-week-to-date week day))
(ent (and (equal date date1) entry)))
(if ent (setq ret ent))))))
ret))
接着就可以在agenda文件中使用了:
* 数字电路与逻辑设计
:PROPERTIES:
:CATEGORY: 数字电路与逻辑设计 方怡冰
:END:
# 设置CATEGORY属性是为了在 Org Agenda View 中显示课程和主讲人,否则将只会显示时间段和上课地点
** 10:05-11:40 禹州406
<%%(school-class '((1 . 12) (14 . 16)) 1)>
** 10:05-11:40 美岭314
<%%(school-class '((1 . 12) (14 . 16)) 4)>
school-class
这个函数接受一个列表和整数,列表里面的元素可以是整数或者是一个表示周范围的二元组,第二个整数参数表明是周几,这样就解决了第一个和第二个问题。
然后现在要解决第三个问题,也就是时间段的重复输入问题。本人想到的是使用 org-mode 的宏替换来解决,
但是在 Org Agenda View 中并不会进行宏替换的,所以要修改 org-agenda-get-day-entries
函数。
在这个函数中,先对agenda文件的buffer进行宏替换,再从buffer中获取一个日期的所有待办事项,最后恢复buffer的内容(这里我是用undo来实现的,不知道是否有更好的方案)。
(defun org-agenda-get-day-entries (file date &rest args)
"Does the work for `org-diary' and `org-agenda'.
FILE is the path to a file to be checked for entries. DATE is date like
the one returned by `calendar-current-date'. ARGS are symbols indicating
which kind of entries should be extracted. For details about these, see
the documentation of `org-diary'."
(let* ((org-startup-folded nil)
(org-startup-align-all-tables nil)
(buffer (if (file-exists-p file) (org-get-agenda-file-buffer file)
(error "No such file %s" file))))
(if (not buffer)
;; If file does not exist, signal it in diary nonetheless.
(list (format "ORG-AGENDA-ERROR: No such org-file %s" file))
(with-current-buffer buffer
(unless (derived-mode-p 'org-mode)
(error "Agenda file %s is not in Org mode" file))
(setq org-agenda-buffer (or org-agenda-buffer buffer))
(setf org-agenda-current-date date)
(undo-boundary) ;创建新的undo边界
(org-macro-replace-all org-macro-templates) ;执行宏替换
(let ((ret (save-excursion
(save-restriction
(if (eq buffer org-agenda-restrict)
(narrow-to-region org-agenda-restrict-begin
org-agenda-restrict-end)
(widen))
;; Rationalize ARGS. Also make sure `:deadline' comes
;; first in order to populate DEADLINES before passing it.
;;
;; We use `delq' since `org-uniquify' duplicates ARGS,
;; guarding us from modifying `org-agenda-entry-types'.
(setf args (org-uniquify (or args org-agenda-entry-types)))
(when (and (memq :scheduled args) (memq :scheduled* args))
(setf args (delq :scheduled* args)))
(cond
((memq :deadline args)
(setf args (cons :deadline
(delq :deadline (delq :deadline* args)))))
((memq :deadline* args)
(setf args (cons :deadline* (delq :deadline* args)))))
;; Collect list of headlines. Return them flattened.
(let ((case-fold-search nil) results deadlines)
(dolist (arg args (apply #'nconc (nreverse results)))
(pcase arg
((and :todo (guard (org-agenda-today-p date)))
(push (org-agenda-get-todos) results))
(:timestamp
(push (org-agenda-get-blocks) results)
(push (org-agenda-get-timestamps deadlines) results))
(:sexp
(push (org-agenda-get-sexps) results))
(:scheduled
(push (org-agenda-get-scheduled deadlines) results))
(:scheduled*
(push (org-agenda-get-scheduled deadlines t) results))
(:closed
(push (org-agenda-get-progress) results))
(:deadline
(setf deadlines (org-agenda-get-deadlines))
(push deadlines results))
(:deadline*
(setf deadlines (org-agenda-get-deadlines t))
(push deadlines results)))))))))
(primitive-undo 1 buffer-undo-list) ;undo宏替换
ret)))))
然后就可以在agenda文件中定义一个宏:
#+MACRO: school-time (eval (concat (cdr (assoc (string-to-number $1) school-time-periods)) "(" (cdr (assoc (string-to-number $1) school-time-class-nums)) ")"))
其中的 school-time-class-nums
和 school-time-periods
如下:
(setq school-time-class-nums '((1 . "上午1、2节")
(2 . "上午3、4节")
(3 . "下午5、6节")
(4 . "下午7、8节")
(5 . "晚上9、10节")))
(setq school-time-periods '((1 . "8:00-9:35")
(2 . "10:05-11:40")
(3 . "14:00-15:35")
(4 . "15:55-17:30")
(5 . "19:00-20:35")))
然后就可以愉快地把heading中的时间段用宏来代替了:
** {{{school-time(2)}}} 禹州406
<%%(school-class '((1 . 12) (14 . 16)) 1)>
** {{{school-time(2)}}} 美岭314
<%%(school-class '((1 . 12) (14 . 16)) 4)>
虽然看上去字数反而多了,但是每次输入只要进行复制粘贴后只更改一个数即可了。
最终效果如下:
第一次在论坛分享经验,若有写得不好的地方欢迎各位大佬批评指正!