时间戳日志
概述
org-timestamp-fragment, 一个收集时间戳日志的工具。
本文中的“时间戳日志”特指一种嵌于 Org段落 中,以 Org时间戳 打头的文本片段,如下所示:
[1970-01-01 Thu 22:00]这是一条时间戳日志。[1970-01-02 Fri 22:04]这是另一条时间戳日志。[1970-01-01 Thu 22:05]这三条日志均属于同一段落。
另外,时间戳日志所在段落的首个子元素必须是时间戳,换句话说:
因为这个段落的首个子元素非时间戳,故[1970-01-01 Thu 22:00]这个段落中的时间戳日志属于非法定义,将不纳入收集结果。
交互及入口
作为 Emacs 命令:
M-x org-timestamp-fragment o 收集时间戳日志。
M-x org-timestamp-fragment a 返回当前位置的时间戳日志,如存在。
M-x org-timestamp-fragment h 查询 org-timestamp-fragment API 文档。
作为 elisp 接口:
(org-timestamp-fragment 'occur &rest KARGS)
(org-timestamp-fragment 'at-point)
#+name: 2025-09-13-15-55
#+begin_src emacs-lisp
(lambda (op &rest args)
  (interactive
   `(,(pcase (car (read-multiple-choice
                   "" '((?a "at-point")
                        (?h "help")
                        (?o "occur"))))
        (?a at-point) (?h describe) (?o occur))))
  (ignore at-point describe occur)
  (cond
   ((eq this-command #'org-timestamp-fragment)
    (when (commandp op)
      (call-interactively op)))
   ((and-let* ((_ (symbolp op))
               (f org-timestamp-fragment)
               (f (alist-get op (aref f 2)))
               (_ (functionp f)))
      (apply f args)))))
#+end_src
收集时间戳日志
收集指定范围内的所有时间戳日志。
接口
(org-timestamp-fragment 'occur &rest kargs)
#+name: 2025-09-13-15-56
#+begin: elisp-docstring
KARGS
:date          默认 nil. 如 nil, 试从 Property drawer
中获取,取其首个出现的时间戳。输入格式同 ‘org-read-date’ 的
FROM-STRING 参数。
:with-link     默认 t. 如 t, 带上 TIMESTAMP-FRAGMENT
所在的节点的 ID链接。
:as-paragraph  默认 nil. 如 t, 每个 TIMESTAMP-FRAGMENT
独立成段。
:with-context  默认 nil. 如 t, 提取 TIMESTAMP-FRAGMENT
所在的整个段落,段落中未匹配的 FRAGMENT 同样纳入收集结果。
#+end:
示例
给定如下时间戳日志(两个段落):
[1970-01-01 Thu 22:00]这是一条时间戳日志。[1970-01-02 Fri 22:04]这是另一条时间戳日志。[1970-01-01 Thu 22:05]这三条日志均属于同一段落。
[1970-01-03 Sat 22:10]这是一条时间戳日志。[1970-01-02 Fri 22:40]这是另一条时间戳日志。[1970-01-01 Thu 22:11]这三条日志均属于同一段落。
通过 elisp 收集: (org-timestamp-fragment 'occur :date "1970-01-01") 将返回如下文本:
"[1970-01-01 Thu 22:00]这是一条时间戳日志。[1970-01-01 Thu 22:05]这三条日志均属于同一段落。\n\n[1970-01-01 Thu 22:11]这三条日志均属于同一段落。"
通过 Emacs 命令收集: M-x org-timestamp-fragment o 1970-1-1 RET 将弹出带有如下内容的窗口:
[1970-01-01 Thu 22:00]这是一条时间戳日志。[1970-01-01 Thu 22:05]这三条日志均属于同一段落。
[1970-01-01 Thu 22:11]这三条日志均属于同一段落。
通过 Org 动态块收集:
#+begin: ts-text :date "1970-01-01"
[1970-01-01 Thu 22:00]这是一条时间戳日志。[1970-01-01 Thu 22:05]这三条日志均属于同一段落。
[1970-01-01 Thu 22:11]这三条日志均属于同一段落。
#+end:
注:值得注意的是,有些的日志被过滤掉了。换句话说, org-timestamp-fragment 在收集日志的过程中裁剪掉了段落的某些部分。
配置参数对收集结果的影响:
:with-link
#+begin: ts-text :date "1970-01-01" :with-link t
ID-LINK: [1970-01-01 Thu 22:00]这是一条时间戳日志。[1970-01-01 Thu 22:05]这三条日志均属于同一段落。
ID-LINK: [1970-01-01 Thu 22:11]这三条日志均属于同一段落。
#+end:
:as-paragraph
#+begin: ts-text :date "1970-01-01" :as-paragraph t
[1970-01-01 Thu 22:00]这是一条时间戳日志。
[1970-01-01 Thu 22:05]这三条日志均属于同一段落。
[1970-01-01 Thu 22:11]这三条日志均属于同一段落。
#+end:
:with-context
#+begin: ts-text :date "1970-01-01" :with-context t
[1970-01-01 Thu 22:00]这是一条时间戳日志。[1970-01-02 Fri 22:04]这是另一条时间戳日志。[1970-01-01 Thu 22:05]这三条日志均属于同一段落。
[1970-01-03 Sat 22:10]这是一条时间戳日志。[1970-01-02 Fri 22:40]这是另一条时间戳日志。[1970-01-01 Thu 22:11]这三条日志均属于同一段落。
#+end:
实现
借助 (org-inline-element 'occur)
occur
#+name: 2025-09-13-16-09
#+begin_src emacs-lisp :eval no
;; depends: here?
(lambda (&rest kargs)
  "Org timestamp fragment occur.
<<@([[id:org-timestamp-fragment::doc:occur]])>>"
  (interactive (list :date (org-read-date)))
  (let* ((date (plist-get kargs :date))
         (date
          (or (and date (org-read-date
                         nil t date))
              ;; property drawer 中的首个时间戳
              (let ((r (org-get-property-block)))
                (ignore-errors
                  (encode-time
                   (org-parse-time-string
                    (buffer-substring
                     (car r) (cdr r))))))
              (error "Not date found")))
         (date (format-time-string
                "%Y-%m-%d" date)))
    (unless (plist-member kargs :with-link)
      (plist-put kargs :with-link t))
    (apply
     (org-inline-element 'occur)
     (format "\\[%s[^]]*\\]." date)
     here?
     :popup
     (when (or (plist-get kargs :popup)
               (called-interactively-p
                'interactive))
       (format "org-timestamp-fragment:%s" date))
     :key
     (or (plist-get kargs :key)
         (lambda (e)
           (float-time
            (encode-time
             (org-parse-time-string e)))))
     ;; here? 内部用
     :--id (org-entry-get nil "ID" t)
     kargs)))
#+end_src
here?
#+name: 2025-09-13-16-10
#+begin_src emacs-lisp :eval no
;; depends: at-point
(lambda (kargs)
  (!let ((-p (plist-get kargs :as-paragraph))
         (-c (plist-get kargs :with-context))
         (-l (plist-get kargs :with-link))
         (--id (plist-get kargs :--id))
         (--lp (plist-get kargs :--last-par))
         ;; 不匹配那些自动生成于动态块中的文本。
         (exclude '(dynamic-block))
         (id (org-entry-get nil "ID" t))
         (e (org-element-context)) ie par ts item)
   (unless (or (org-element-lineage e exclude)
               ;; external input: entry-id.
               (and --id (equal id --id))
               (not (setq ie (at-point))))
     (setq ts (org-element-property :raw-value e))
     ;; 记录当前段落的范围。
     (setq par (org-element-parent e)
           par `(,(org-element-begin par)
                 ,(org-element-end par)))
     (setq e (if (not -c) ie
               ;; 如 with-context, 返回整个段落。
               (save-restriction
                 (org-narrow-to-element)
                 (org-element-map
                     (org-element-parse-buffer)
                     '(paragraph)
                   #'identity nil t))))
     `(,(org-element-end e)
       ,(let* ((p (org-element-interpret-data e))
               (p (string-trim p)))
          (concat
           ;; ID-LINK
           (cond
            ((equal --lp par) "")
            ((not (and -l id)) "")
            ((let ((item
                    (org-entry-get
                     (org-find-entry-with-id id)
                     "ITEM")))
               (format "[[id:%s::%s][%s]]: "
                       id (substring ts 1 -1)
                       (if (length= item 0)
                           "@" item)))))
           p))
       ,(if -p nil (equal --lp par))
       ,(plist-put kargs :--last-par par)))))
#+end_src