On Tue, Mar 28, 2023 at 10:52:18AM +0000, Ihor Radchenko wrote:
> So, when Org is trying to change the data to 2023-03-26 2:05, Emacs date
> library refuses and instead sets the closes valid time.
> 
> I am not sure would be the best course of action here.
> 1. We can jump over the invalid hours in the direction requested by user
> 2. We can throw an error, making the user aware about the daylight
>    thing.
> 
> I am more in favour of (2) because things like this are easy to overlook.

I've attached a patch that uses a comparison of the date before and
after the shift to ensure that the shift resulted in a proper change
(as opposed to a wrap that can end up shifting in the wrong direction
or not at all). Per the other thread, I think this is independent of
the current work on `org-timestamp-change'.

Cheers,

Derek


-- 
+---------------------------------------------------------------+
| Derek Chen-Becker                                             |
| http://chen-becker.org                                        |
|                                                               |
| GPG Key available at https://keybase.io/dchenbecker and       |
| https://pgp.mit.edu/pks/lookup?search=derek%40chen-becker.org |
| Fngrprnt: EB8A 6480 F0A3 C8EB C1E7  7F42 AFC5 AFEE 96E4 6ACC  |
+---------------------------------------------------------------+
From f46ea6f30c997e41031e9a28fe929432bf137290 Mon Sep 17 00:00:00 2001
From: Derek Chen-Becker <[email protected]>
Date: Thu, 2 Apr 2026 07:00:42 -0600
Subject: [PATCH] lisp/org.el: Error when timestamp shift hits DST gap

* lisp/org.el (org-timestamp-change): Detect when `org-timestamp-change'
silently normalizes invalid times in a spring-forward daylight savings time
gap.  When this happens, restore the original timestamp and raise an error
with the missing time and the timezone.

* testing/lisp/test-org.el (test-org/org-timestamp-change-dst): Add
tests for shifting hours and minutes into a spring-forward gap.
---
 lisp/org.el              | 21 ++++++++++++++++++++-
 testing/lisp/test-org.el | 19 +++++++++++++++++++
 2 files changed, 39 insertions(+), 1 deletion(-)

diff --git a/lisp/org.el b/lisp/org.el
index 222af988a..9f8b43bc3 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -15639,7 +15639,7 @@ When SUPPRESS-TMP-DELAY is non-nil, suppress delays like
 	with-hm inactive
 	(dm (max (nth 1 org-time-stamp-rounding-minutes) 1))
 	extra rem
-	ts time time0 fixnext clrgx)
+	ts time time-original time0 fixnext clrgx)
     (unless timestamp? (user-error "Not at a timestamp"))
     (if (and (not what) (eq timestamp? 'bracket))
 	(org-toggle-timestamp-type)
@@ -15671,6 +15671,8 @@ When SUPPRESS-TMP-DELAY is non-nil, suppress delays like
       (when (string-match "^.\\{10\\}.*?[0-9]+:[0-9][0-9]" ts)
 	(setq with-hm t))
       (setq time0 (org-parse-time-string ts))
+      ;; Capture the original timestamp for direction validation later
+      (setq time-original (org-encode-time time0))
       (let ((increment n))
         (if (and updown
 	         (eq timestamp? 'minute)
@@ -15698,6 +15700,23 @@ When SUPPRESS-TMP-DELAY is non-nil, suppress delays like
                    (month :month)
                    (year :year))
                  increment)))))
+      ;; Validation if we're modifying hour or minute fields
+      (when (and with-hm ;; timestamp has hour/minute fields
+                 (memq timestamp? '(hour minute)) ;; we're modifying hour or minute
+                 (not (zerop n))) ;; non-zero increment
+        ;; Use time-less-p to compare the original timestamp to the
+        ;; new one so that we ensure that the direction was
+        ;; respected. In the case of the DST gap, normalization of the
+        ;; timestamp post-shift results in wrapping that does not
+        ;; match the intended direction e.g. 3:00 on the DST date
+        ;; shifted down by 5 minutes results in 3:55
+        (unless (if (> n 0)
+                    (time-less-p time-original time)
+                  (time-less-p time time-original))
+          (insert ts) ;; restore the original timestamp
+          (user-error "Cannot shift %s into the DST gap (according to current timezone '%s')"
+                      ts
+                      (cadr (current-time-zone time-original)))))
       (when (and (memq timestamp? '(hour minute))
 		 extra
 		 (string-match "-\\([012][0-9]\\):\\([0-5][0-9]\\)" extra))
diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el
index 82beb9d58..0f062c144 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -9536,6 +9536,25 @@ Behavior can be modified by setting `org-log-into-drawer', by keywords in
       (test-time-stamp-rounding "<2026-03-14 12:00>" (1+ i) -1
                                 (concat "11:5" (number-to-string (- 9 i)))))))
 
+(ert-deftest test-org/org-timestamp-change-dst ()
+  "Test that `org-timestamp-change' properly errors at DST boundaries."
+  ;; Using the process environment to ensure the correct timezone for this test
+  (let ((process-environment (cons "TZ=America/New_York" process-environment)))
+    ;; Shifting the hour from 3:05 to 2:05 falls into the gap and should error
+    (should-error
+     (org-test-with-temp-text "<2026-03-08 Sun 03:05>"
+                              (forward-char 16) ;; Put the cursor on the hour
+                              (org-timestamp-change -1 'hour)
+                              :type 'user-error
+                              ))
+    ;; Shifting the minutes from 3:00 to 2:55 falls into the gap and should error
+    (should-error
+     (org-test-with-temp-text "<2026-03-08 Sun 03:00>"
+                              (forward-char 19) ;; Put the cursor on the minute
+                              (org-timestamp-change -1 'minute)
+                              :type 'user-error
+                              ))))
+
 (ert-deftest test-org/timestamp ()
   "Test `org-timestamp' specifications."
   ;; Insert chosen time stamp at point.
-- 
2.39.5

Attachment: signature.asc
Description: PGP signature

Reply via email to