On 2025-04-20  13:27, Ihor Radchenko wrote:

> Although, I would add some comments to the code, decrypting what 5 and 7
> refer to.

Done, and a bit more.

> Not that hard. All you need is to re-implement your reproducer in pure
> Elisp. Even a single test will do.

Here I could not resist to add more general test coverage for
`org-timestamp-change'.  Basically to tweak timestamps at every
item position up and down, both with and w/o time and both with
and w/o weekdays.

Please check the attached patch and let me know what you think.

Thanks.
From 33b25f91a50101f67e8a998c370e007831a35b5d Mon Sep 17 00:00:00 2001
From: Jens Schmidt <jschmidt4...@vodafonemail.de>
Date: Sat, 26 Apr 2025 21:33:46 +0200
Subject: [PATCH] Improve support for weekday-less timestamps

* lisp/org.el (org-ts-regexp1, org-timestamp-formats): Doc fixes.
(org-timestamp-change): Test for presence of optional groups before
accessing them.
* testing/lisp/test-org.el (test-org/at-timestamp-p): Add tests for
timestamps with time and without weekday name.
(test-org/org-timestamp-change): New test.

Link: https://list.orgmode.org/5bfeb020-821b-49a4-9380-1927adc05...@vodafonemail.de/
---
 lisp/org.el              | 28 +++++++++++++++---
 testing/lisp/test-org.el | 64 ++++++++++++++++++++++++++++++++++++++++
 2 files changed, 88 insertions(+), 4 deletions(-)

diff --git a/lisp/org.el b/lisp/org.el
index 78e8c0692..b5f4f3b89 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -448,7 +448,16 @@ This one does not require the space after the date, so it can be used
 on a string that terminates immediately after the date.")
 
 (defconst org-ts-regexp1 "\\(\\([0-9]\\{4\\}\\)-\\([0-9]\\{2\\}\\)-\\([0-9]\\{2\\}\\)\\(?: *\\([^]+0-9>\r\n -]+\\)\\)?\\( \\([0-9]\\{1,2\\}\\):\\([0-9]\\{2\\}\\)\\)?\\)"
-  "Regular expression matching time strings for analysis.")
+  "Regular expression matching time strings for analysis.
+This regular expression provides the following groups:
+  1:   everything (required for embedding)
+   2:  year
+   3:  month
+   4:  day
+   5:  weekday name (optional)
+   6:  time part (optional)
+    7: hour
+    8: minute")
 
 (defconst org-ts-regexp2 (concat "<" org-ts-regexp1 "[^>\n]\\{0,16\\}>")
   "Regular expression matching time stamps, with groups.")
@@ -479,6 +488,13 @@ The time stamps may be either active or inactive.")
   "Regular expression for specifying repeated events.
 After a match, group 1 contains the repeat expression.")
 
+;; The weekday name "%a" is considered semi-optional in these formats,
+;; see https://list.orgmode.org/87fricxatw.fsf@localhost/.  It is
+;; "optional" because the `org-timestamp-*' functions work alright on
+;; weekday-less timestamps in paragraphs when one omits the "%a".  But
+;; it is only "semi"-optional since Org cannot process properly
+;; timestamps in CLOCK, DEADLINE, and SCHEDULED lines when one omits
+;; the "%a".
 (defvaralias 'org-time-stamp-formats 'org-timestamp-formats)
 (defconst org-timestamp-formats '("%Y-%m-%d %a" . "%Y-%m-%d %a %H:%M")
   "Formats for `format-time-string' which are used for time stamps.
@@ -15472,9 +15488,13 @@ When SUPPRESS-TMP-DELAY is non-nil, suppress delays like
 	(looking-at org-ts-regexp3)
 	(goto-char
 	 (pcase origin-cat
-	   ;; `day' category ends before `hour' if any, or at the end
-	   ;; of the day name.
-	   (`day (min (or (match-beginning 7) (1- (match-end 5))) origin))
+	   ;; `day' category ends at the end of the weekday name if
+	   ;; any (group 5), or before `hour' if any (group 7), or at
+	   ;; the end of the timestamp (group 1).
+	   (`day (min (cond ((match-end 5) (1- (match-end 5)))
+                            ((match-beginning 7))
+                            (t (1- (match-end 1))))
+                      origin))
 	   (`hour (min (match-end 7) origin))
 	   (`minute (min (1- (match-end 8)) origin))
 	   ((pred integerp) (min (1- (match-end 0)) origin))
diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el
index eefc9b692..af307f70b 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -8993,6 +8993,14 @@ CLOSED: %s
    (eq 'day
        (org-test-with-temp-text "<2012-03-29 T<point>hu>"
 	 (org-at-timestamp-p))))
+  (should
+   (eq 'hour
+       (org-test-with-temp-text "<2012-03-29 Thu <point>12:34>"
+	 (org-at-timestamp-p))))
+  (should
+   (eq 'minute
+       (org-test-with-temp-text "<2012-03-29 Thu 12:<point>34>"
+	 (org-at-timestamp-p))))
   (should
    (wholenump
     (org-test-with-temp-text "<2012-03-29 Thu +2<point>y>"
@@ -9005,6 +9013,23 @@ CLOSED: %s
    (eq 'after
        (org-test-with-temp-text "<2012-03-29 Thu><point>ยป"
 	 (org-at-timestamp-p))))
+  ;; Test optional weekday name.
+  (should
+   (eq 'day
+       (org-test-with-temp-text "<2012-03-2<point>9>"
+	 (org-at-timestamp-p))))
+  (should
+   (eq 'day
+       (org-test-with-temp-text "<2012-03-29<point> 12:34>"
+	 (org-at-timestamp-p))))
+  (should
+   (eq 'hour
+       (org-test-with-temp-text "<2012-03-29 <point>12:34>"
+	 (org-at-timestamp-p))))
+  (should
+   (eq 'minute
+       (org-test-with-temp-text "<2012-03-29 12:<point>34>"
+	 (org-at-timestamp-p))))
   ;; Test `inactive' optional argument.
   (should
    (org-test-with-temp-text "[2012-03-29 Thu]"
@@ -9067,6 +9092,45 @@ CLOSED: %s
    (org-test-with-temp-text "# [2012-03-29 Thu]<point>"
      (org-at-timestamp-p 'lax))))
 
+(ert-deftest test-org/org-timestamp-change ()
+  "Test `org-timestamp-change' specifications."
+  (let ((now (current-time)) now-ts point)
+    (message "Testing with timestamps <%s> and <%s>"
+             (format-time-string (car org-timestamp-formats) now)
+             (format-time-string (cdr org-timestamp-formats) now))
+    ;; loop over regular timestamp formats and weekday-less timestamp
+    ;; formats
+    (dolist (org-timestamp-formats
+             (list org-timestamp-formats
+                   (cons (replace-regexp-in-string
+                          " %a" "" (car org-timestamp-formats))
+                         (replace-regexp-in-string
+                           " %a" "" (cdr org-timestamp-formats)))))
+      ;; loop over timestamps that do not and do contain time
+      (dolist (format (list (car org-timestamp-formats)
+                            (cdr org-timestamp-formats)))
+        (setq now-ts
+              (concat "<" (format-time-string format now) ">"))
+        (org-test-with-temp-text now-ts
+          (forward-char 1)
+          (while (not (eq (char-after) ?>))
+            (skip-syntax-forward "-")
+            ;; change the timestamp unit at point one down, two up,
+            ;; one down, which should give us the original timestamp
+            ;; again.  However, point can move backward during that
+            ;; operation, so take care of that.  *Not* using
+            ;; `save-excursion', which fails to restore point since
+            ;; the timestamp gets completely replaced.
+            (setq point (point))
+            (org-timestamp-change -1 nil nil nil)
+            (org-timestamp-change  2 nil nil nil)
+            (org-timestamp-change -1 nil nil nil)
+            (goto-char point)
+            (should (string=
+                     (buffer-substring (point-min) (point-max))
+                     now-ts))
+            (forward-char 1)))))))
+
 (ert-deftest test-org/timestamp ()
   "Test `org-timestamp' specifications."
   ;; Insert chosen time stamp at point.
-- 
2.39.5

Reply via email to