UP | HOME

▼ 本文更新于 [2026-03-06 周五 16:49]

emacs-为每日重复事项添加工作日判定


有的时候,我们只想在工作日处理一些工作相关的每日重复事项。

1. 设置节假日

排除掉周六、周日以及法定节假日(也许还有自定义的假日)之后,就是工作日。在 org-agenda-files 中任一文件加入以下内容:

;; 2025中国法定节假日
%%(diary-date 1 1 2025) 🏮元旦🏮
%%(diary-block 1 28 2025 2 4 2025) 🏮春节🏮
%%(diary-date 1 26 2025) 💼春节-上班💼
%%(diary-date 2 8 2025) 💼春节-上班💼
%%(diary-block 4 4 2025 4 6 2025) 🏮清明🏮
%%(diary-block 5 1 2025 5 5 2025) 🏮劳动节🏮
%%(diary-date 4 27 2025) 💼劳动节-上班💼
%%(diary-block 5 31 2025 6 2 2025) 🏮端午🏮
%%(diary-block 10 1 2025 10 8 2025) 🏮国庆、中秋🏮
%%(diary-date 9 28 2025) 💼国庆中秋-上班💼
%%(diary-date 10 11 2025) 💼国庆中秋-上班💼

即可在org-agenda中生成sexp表达式的日历条目,后续会用到。

2. 设置函数

(defun my/date-is-workday (date &optional offset)
  "判断日期是否为工作日(包含调休)。DATE格式为 'YYYY-MM-DD'。"
  (let* ((d (date-to-time date))
         ;; 处理偏移量
         (target-time (time-add d (days-to-time (or offset 0))))
         (greg (calendar-gregorian-from-absolute (time-to-days target-time)))
         ;; 获取当天的 Agenda 字符串
         (entries (mapconcat (lambda (f) 
                               (mapconcat #'identity (org-agenda-get-day-entries f greg :sexp) ""))
                             (org-agenda-files) ""))
         (is-holiday (string-match-p "🏮" entries))
         (is-workday (string-match-p "💼" entries))
         (day-of-week (calendar-day-of-week greg)))
    
    (cond
     ;; 优先级1:如果标记为调休日(💼),强制为工作日
     (is-workday t)
     ;; 优先级2:如果标记为节假日(🏮),强制为非工作日
     (is-holiday nil)
     ;; 优先级3:周末为非工作日,周一至周五为工作日
     ((memq day-of-week '(0 6)) nil)
     (t t))))

这个函数会根据输入日期+可选的偏移日期,通过匹配emoji来判断是否为工作日。

以下函数参考了这篇文章1

(defun my/org-hook-for-repeat-on-workday ()
  "如果任务有WORKDAY属性且是重复任务,自动跳过节假日调度到下一个工作日。"
  (let ((workday-attr (org-entry-get nil "WORKDAY")))
    (when (and workday-attr (org-get-repeat))
      (let* ((offset (string-to-number workday-attr))
             ;; 获取原始时间戳
             (time (org-time-string-to-time (org-entry-get nil "SCHEDULED")))
             (day-sec (* 24 60 60)))
        
        ;; 循环直到找到一个既不是假期,也不是“带偏移的假期”的日期
        ;; 注意:这里保留了你原有的逻辑:必须同时满足两个条件才停止循环
        (while (and (not (my/date-is-workday (format-time-string "%Y-%m-%d" time)))
                    (not (my/date-is-workday (format-time-string "%Y-%m-%d" time) (- offset))))
          (setq time (time-add time (seconds-to-time day-sec))))
        
        ;; 重新调度
        (org-schedule nil (format-time-string 
                           (if (string-match-p ":" (org-entry-get nil "SCHEDULED"))
                               "%Y-%m-%d %a %H:%M"
                             "%Y-%m-%d %a")
                           time))))))

(add-hook 'org-todo-repeat-hook 'my/org-hook-for-repeat-on-workday)

这个函数会提取属性中的 WORKDAY ,如果有值且按日重复,则会启动。

偏移值的目的主要是用来处理「节假日第一天」和「节假日最后一天」的需求。如果有任务需要在「节假日第一天」和工作日重复,则将 WORKDAY 的值设定为1;如果有任务需要在「节假日最后一天」和工作日重复,则将 WORKDAY 的值设定为-1;如果都不需要,则将 WORKDAY 的值设定为0,仅会匹配工作日。

3. 使用效果

根据sexp表达式的条目中🏮和💼的emoji来判断。

-> 是周末
—> 有💼
–—> 工作日
—> 无💼
–—> 节假日
-> 不是周末
—> 有🏮
–—> 节假日
—> 无🏮
–—> 工作日

Footnotes:

© Published by Emacs 31.0.50 (Org mode 10.0-pre) | RSS English-Index