【经验分享】用org-mode快速制作课程表

折腾Emacs也有一段时间了,最近发现学校查看近期的课程很不方便,于是就想用org-mode来录入学校的课程表,然后用org-agenda来进行显示和时间规划, 顺便分享一下自己的一点经验,希望大家能有所收获。 :grinning:

本人学校的课表如下:

本人开始录入课程表时主要面临有几个问题:

  1. 学校课程的日期安排,不是以日期的形式来安排的,而是以开学以来的周数+周几来表示的,如果录入时手动换算会比较麻烦。
  2. 学校课程的安排比较灵活,很多课是以周数的区间形式来安排的,比如 1~14,15,17~18周,我希望能以类似的格式直接进行录入。
  3. 学校每天课程时间安排比较固定,比如每天的第一节课就固定是 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-numsschool-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)>

虽然看上去字数反而多了,但是每次输入只要进行复制粘贴后只更改一个数即可了。

最终效果如下:

第一次在论坛分享经验,若有写得不好的地方欢迎各位大佬批评指正!

13赞

用邮箱自带的日历服务可以得到更好的跨平台体验吧。

牛逼啊这是。

年轻人能使用课表,真开心。

用宏展时间段的想法不错。

我之前遇到楼主类似的问题用 tiny 解决的。 一个学期内大部分周都是重复的, 只用对个别周进行一下微调。Org-mode 有对时间戳重复的语法,但我个人还是 习惯在每个周下面有一个单独的标题,用来处理相关笔记。

比如:

m1\n14|**** TODO 2021-诗歌鉴赏 第%s周 李白 @甘肃静宁 \n SCHEDULED: <%(date "Thu" (* x 7)) 16:30 - 18:30>

展开成:

**** TODO 2021-诗歌鉴赏 第1周 李白 @甘肃静宁 
     SCHEDULED: <2021-10-07 Thu 16:30 - 18:30>
**** TODO 2021-诗歌鉴赏 第2周 李白 @甘肃静宁 
     SCHEDULED: <2021-10-14 Thu 16:30 - 18:30>
**** TODO 2021-诗歌鉴赏 第3周 李白 @甘肃静宁 
     SCHEDULED: <2021-10-21 Thu 16:30 - 18:30>
**** TODO 2021-诗歌鉴赏 第4周 李白 @甘肃静宁 
     SCHEDULED: <2021-10-28 Thu 16:30 - 18:30>
**** TODO 2021-诗歌鉴赏 第5周 李白 @甘肃静宁 
     SCHEDULED: <2021-11-04 Thu 16:30 - 18:30>
**** TODO 2021-诗歌鉴赏 第6周 李白 @甘肃静宁 
     SCHEDULED: <2021-11-11 Thu 16:30 - 18:30>
**** TODO 2021-诗歌鉴赏 第7周 李白 @甘肃静宁 
     SCHEDULED: <2021-11-18 Thu 16:30 - 18:30>
**** TODO 2021-诗歌鉴赏 第8周 李白 @甘肃静宁 
     SCHEDULED: <2021-11-25 Thu 16:30 - 18:30>
**** TODO 2021-诗歌鉴赏 第9周 李白 @甘肃静宁 
     SCHEDULED: <2021-12-02 Thu 16:30 - 18:30>
**** TODO 2021-诗歌鉴赏 第10周 李白 @甘肃静宁 
     SCHEDULED: <2021-12-09 Thu 16:30 - 18:30>
**** TODO 2021-诗歌鉴赏 第11周 李白 @甘肃静宁 
     SCHEDULED: <2021-12-16 Thu 16:30 - 18:30>
**** TODO 2021-诗歌鉴赏 第12周 李白 @甘肃静宁 
     SCHEDULED: <2021-12-23 Thu 16:30 - 18:30>
**** TODO 2021-诗歌鉴赏 第13周 李白 @甘肃静宁 
     SCHEDULED: <2021-12-30 Thu 16:30 - 18:30>
**** TODO 2021-诗歌鉴赏 第14周 李白 @甘肃静宁 
     SCHEDULED: <2022-01-06 Thu 16:30 - 18:30>

PS: 楼主最后一个 Gif 中,向前或者向后查看周日历,调用的是什么函数?

直接按 fb 就好了,也就是 org-agenda-laterorg-agenda-earlier

1赞

集美大学?这些楼的名字好熟悉

哈哈是的 :joy:

好熟悉的课,自动化的吗

电子信息工程

动手能力不错,在学校编程能力就不错了👍