时间戳日志
概述
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