diff options
| author | Craig Jennings <c@cjennings.net> | 2026-04-30 09:25:09 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-04-30 09:25:09 -0500 |
| commit | f90a087d5025952f0ca1b81d322f29891a40e540 (patch) | |
| tree | 3d95e20b7e4a9f4022c461fbf8d330e6eff164fb | |
| parent | c5ef22aacd5954106f55c8c7b6ae52f7f7bbfa76 (diff) | |
| download | dotemacs-f90a087d5025952f0ca1b81d322f29891a40e540.tar.gz dotemacs-f90a087d5025952f0ca1b81d322f29891a40e540.zip | |
fix(config-utilities): repair validate-org-agenda-timestamps property check
Two bugs in cj/validate-org-agenda-timestamps surfaced while extracting
testable helpers.
1. The DEADLINE / SCHEDULED / TIMESTAMP property lookup used
(intern (downcase prop)) as the key, producing 'deadline,
'scheduled, 'timestamp. org-element-property expects keywords
(:deadline, :scheduled, :timestamp) and returns nil for plain
symbols. The property-check branch had never reported anything
since the function was written. Only inline-regex matches inside
headline contents have ever been flagged. Fixed by building the
keyword form: (intern (concat ":" (downcase prop))).
2. Once #1 is fixed, every property timestamp would also match the
inline-timestamp regex during the contents scan (since the
DEADLINE: / SCHEDULED: / TIMESTAMP lines fall inside
contents-begin/end on a parsed headline), producing duplicate
reports. Added a per-headline list of property timestamp strings
and a member check before pushing an inline match.
The function is also restructured into three pieces to make it
testable:
- cj/--validate-timestamps-in-buffer FILE — pure-ish: walks the
current buffer, returns a list of (FILE POS HEAD PROP TS) tuples.
- cj/--format-validation-report-section FILE INVALID — pure: returns
the per-file org-formatted string.
- cj/validate-org-agenda-timestamps (interactive) — orchestrates
both helpers across org-agenda-files into a report buffer.
The interactive entry-point's behaviour is unchanged from the user's
side except that DEADLINE / SCHEDULED / TIMESTAMP property timestamps
are now actually checked.
| -rw-r--r-- | modules/config-utilities.el | 96 |
1 files changed, 65 insertions, 31 deletions
diff --git a/modules/config-utilities.el b/modules/config-utilities.el index 9b913d17..8d094fda 100644 --- a/modules/config-utilities.el +++ b/modules/config-utilities.el @@ -306,6 +306,69 @@ Returns the count of files deleted." ;; ------------------------ Validate Org Agenda Entries ------------------------ +(defun cj/--validate-timestamps-in-buffer (file) + "Scan the current buffer for invalid org timestamps. +Walks every headline. Checks DEADLINE / SCHEDULED / TIMESTAMP +properties plus inline timestamps in headline contents. An inline +match whose raw text equals a property timestamp on the same headline +is not reported a second time. + +Returns a list of (FILE POS HEADLINE-TEXT PROP TIMESTAMP-STRING) tuples +in document order. FILE is the value passed in; the function does not +look it up itself." + (require 'org) + (require 'org-element) + (let ((invalid '()) + (props '("DEADLINE" "SCHEDULED" "TIMESTAMP")) + (parse-tree (org-element-parse-buffer 'headline))) + (org-element-map parse-tree 'headline + (lambda (hl) + (let ((headline-text (org-element-property :raw-value hl)) + (begin-pos (org-element-property :begin hl)) + (property-timestamps '())) + (dolist (prop props) + (let ((timestamp (org-element-property + (intern (concat ":" (downcase prop))) hl))) + (when timestamp + (let ((time-str (org-element-property :raw-value timestamp))) + (push time-str property-timestamps) + (unless (ignore-errors (org-time-string-to-absolute time-str)) + (push (list file begin-pos headline-text prop time-str) + invalid)))))) + (let ((contents-begin (org-element-property :contents-begin hl)) + (contents-end (org-element-property :contents-end hl))) + (when (and contents-begin contents-end) + (save-excursion + (goto-char contents-begin) + (while (re-search-forward org-ts-regexp contents-end t) + (let ((ts-string (match-string 0))) + (unless (or (member ts-string property-timestamps) + (ignore-errors + (org-time-string-to-absolute ts-string))) + (push (list file begin-pos headline-text + "inline timestamp" ts-string) + invalid)))))))))) + (nreverse invalid))) + +(defun cj/--format-validation-report-section (file invalid-entries) + "Return the per-FILE string section for the timestamp validation report. +INVALID-ENTRIES is a list of (FILE POS HEADLINE PROP TS) tuples as +returned by `cj/--validate-timestamps-in-buffer'. An empty list +produces a section with the \"No invalid timestamps found.\" line." + (concat + (format "* %s\n" file) + (if invalid-entries + (mapconcat + (lambda (entry) + (cl-destructuring-bind (f pos head prop ts) entry + (format + "- [[file:%s::%d][%s]]\n - Property/Type: %s\n - Invalid timestamp: \"%s\"\n" + f pos head prop ts))) + invalid-entries + "") + "No invalid timestamps found.\n") + "\n")) + (defun cj/validate-org-agenda-timestamps () "Scan all files in `org-agenda-files' for invalid timestamps. Checks DEADLINE, SCHEDULED, TIMESTAMP properties and inline timestamps in @@ -322,38 +385,9 @@ entries, property/type, and raw timestamp string." (insert "* Overview\nScan of org-agenda-files for invalid timestamps.\n\n")) (dolist (file org-agenda-files) (with-current-buffer (find-file-noselect file) - (let ((invalid-entries '()) - (props '("DEADLINE" "SCHEDULED" "TIMESTAMP")) - (parse-tree (org-element-parse-buffer 'headline))) - (org-element-map parse-tree 'headline - (lambda (hl) - (let ((headline-text (org-element-property :raw-value hl)) - (begin-pos (org-element-property :begin hl))) - (dolist (prop props) - (let ((timestamp (org-element-property (intern (downcase prop)) hl))) - (when timestamp - (let ((time-str (org-element-property :raw-value timestamp))) - (unless (ignore-errors (org-time-string-to-absolute time-str)) - (push (list file begin-pos headline-text prop time-str) invalid-entries)))))) - (let ((contents-begin (org-element-property :contents-begin hl)) - (contents-end (org-element-property :contents-end hl))) - (when (and contents-begin contents-end) - (save-excursion - (goto-char contents-begin) - (while (re-search-forward org-ts-regexp contents-end t) - (let ((ts-string (match-string 0))) - (unless (ignore-errors (org-time-string-to-absolute ts-string)) - (push (list file begin-pos headline-text "inline timestamp" ts-string) invalid-entries)))))))))) - + (let ((invalid (cj/--validate-timestamps-in-buffer file))) (with-current-buffer report-buffer - (insert (format "* %s\n" file)) - (if invalid-entries - (dolist (entry (reverse invalid-entries)) - (cl-destructuring-bind (f pos head prop ts) entry - (insert (format "- [[file:%s::%d][%s]]\n - Property/Type: %s\n - Invalid timestamp: \"%s\"\n" - f pos head prop ts)))) - (insert "No invalid timestamps found.\n"))) - (with-current-buffer report-buffer (insert "\n"))))) + (insert (cj/--format-validation-report-section file invalid)))))) (pop-to-buffer report-buffer))) ;; --------------------------- Org-Alert-Check Timers -------------------------- |
