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
signature.asc
Description: PGP signature
