Ihor Radchenko <[email protected]> writes: > Morgan Smith <[email protected]> writes: > >> Ihor Radchenko <[email protected]> writes: >>> >>> Ok, but then we need to make sure that we are actually using the time in >>> the expected format. Your patch org-timestamp-change: Use proper time >>> structure accessor functions uses decoded-time-minute on time0, which is >>> from org-parse-time-string. And org-parse-time-string just returns a >>> list. Note that decoded-time-minute is a struct method. That struct >>> might change in subtle ways, so we should not rely on the list and >>> struct being equivalent. >> >> These are fair points. However, I don't want to go back to the obscure >> list accessor functions that where there before. I also don't want to >> duplicate the decoded-time accessor functions. So what do you think of >> the attached patch? All the tests still pass and there are no compile >> warnings. > > The patch is fine in general. But now we also need to go through all the > users of `org-parse-time-string' and check whether they need to be changed.
While the decoded-time struct might be a real "cl-defstruct", I don't think it actually exists as it's own object. As in I think the list is literally indistinguishable from any kind of "decoded-time" struct. I think this because the constructor for "decoded-time" is nil and the definition of the function "make-decoded-time" (please do read that definition). I could be wrong though since I don't understand a lot of the cl- stuff so do let me know. I did only come to the above conclusion after spending far too long making two patches to make org use time access functions though so I'm still submitting those. The rest of the patches are the same except the last patch where I now limit the day to be 27 instead of 28 as I was getting test failures for the following string which includes a day of 29 since I add an hour for the second timestamp: "CLOCK: [2026-03-28 Sat 23:33]--[2026-03-29 Sun 00:33] => 1:00" And the usual blurb: Tests pass after each patch on emacs 30.2. Tests pass after final patch on emacs 28 and 29 and a recent emacs/master build. Tests pass after final patch with TZ set to UTC, Europe/Istanbul, and America/New_York.
>From 7139a2957dc70a225f59c94640569800a928fec4 Mon Sep 17 00:00:00 2001 From: Morgan Smith <[email protected]> Date: Sat, 28 Mar 2026 12:45:31 -0400 Subject: [PATCH 01/10] Use calendar accessor functions * lisp/ol-bbdb.el: Require calendar. (org-bbdb-anniversaries): Use calendar accessor functions. * lisp/ol.el: Require calendar. (org-store-link): Use calendar accessor functions. * lisp/org-agenda.el (org-agenda-format-date-aligned) (org-agenda-get-timestamps, org-agenda-get-progress) (org-agenda-compute-starting-span, org-agenda-diary-entry-in-org-file) (org-agenda-add-entry-to-org-agenda-diary-file) (org-agenda-execute-calendar-command, org-agenda-bulk-action): Use calendar accessor functions. * lisp/org-clock.el (org-clock-special-range, org-clocktable-shift) (org-clock-get-table-data): Use calendar accessor functions. * lisp/org-datetree.el (org-datetree-cleanup): Use calendar accessor functions. * lisp/org.el (org-read-date-analyze, org-funcall-in-calendar) (org-calendar-select, org-calendar-select-mouse) (org-time-from-absolute, org-closest-date, org-date-from-calendar) (org-get-cursor-date): Use calendar accessor functions. --- lisp/ol-bbdb.el | 12 +++----- lisp/ol.el | 8 +++-- lisp/org-agenda.el | 47 ++++++++++++++++++----------- lisp/org-clock.el | 34 +++++++++++++++------ lisp/org-datetree.el | 6 ++-- lisp/org.el | 72 +++++++++++++++++++++++++++++--------------- 6 files changed, 116 insertions(+), 63 deletions(-) diff --git a/lisp/ol-bbdb.el b/lisp/ol-bbdb.el index ede4890ea..a83996d08 100644 --- a/lisp/ol-bbdb.el +++ b/lisp/ol-bbdb.el @@ -101,6 +101,8 @@ (require 'org-macs) (require 'ol) +(require 'calendar) + ;;; Declare functions and variables (declare-function bbdb "ext:bbdb-com" (string elidep)) @@ -122,10 +124,6 @@ ;; `bbdb-record-xfield' replaces it in recent BBDB v3.x+ (declare-function bbdb-record-xfield "ext:bbdb" (record label)) -(declare-function calendar-absolute-from-gregorian "calendar" (date)) -(declare-function calendar-gregorian-from-absolute "calendar" (date)) -(declare-function calendar-leap-year-p "calendar" (year)) - (declare-function diary-ordinal-suffix "diary-lib" (n)) (with-no-warnings (defvar date)) ; unprefixed, from calendar.el @@ -379,9 +377,9 @@ org-bbdb-anniversaries (= 0 (hash-table-count org-bbdb-anniv-hash))) (org-bbdb-make-anniv-hash)) - (let* ((m (car date)) ; month - (d (nth 1 date)) ; day - (y (nth 2 date)) ; year + (let* ((m (calendar-extract-month date)) + (d (calendar-extract-day date)) + (y (calendar-extract-year date)) (annivs (gethash (list m d) org-bbdb-anniv-hash)) (text ()) rec recs) diff --git a/lisp/ol.el b/lisp/ol.el index 5fdff1de8..86c9bd9e6 100644 --- a/lisp/ol.el +++ b/lisp/ol.el @@ -34,6 +34,8 @@ (require 'org-macs) (require 'org-fold) +(require 'calendar) + (defvar clean-buffer-list-kill-buffer-names) (defvar org-agenda-buffer-name) (defvar org-comment-string) @@ -44,7 +46,6 @@ org-outline-regexp-bol (defvar org-src-source-file-name) (defvar org-ts-regexp) -(declare-function calendar-cursor-to-date "calendar" (&optional error event)) (declare-function dired-get-filename "dired" (&optional localp no-error-if-not-filep)) (declare-function org-back-to-heading "org" (&optional invisible-ok)) (declare-function org-before-first-heading-p "org" ()) @@ -2621,7 +2622,10 @@ org-store-link (setq link (format-time-string (org-time-stamp-format) - (org-encode-time 0 0 0 (nth 1 cd) (nth 0 cd) (nth 2 cd)))) + (org-encode-time 0 0 0 + (calendar-extract-day cd) + (calendar-extract-month cd) + (calendar-extract-year cd)))) (org-link-store-props :type "calendar" :date cd))) ;; Image mode diff --git a/lisp/org-agenda.el b/lisp/org-agenda.el index dd86a716a..a63063d12 100644 --- a/lisp/org-agenda.el +++ b/lisp/org-agenda.el @@ -1216,11 +1216,11 @@ org-agenda-format-date-aligned This function makes sure that dates are aligned for easy reading." (require 'cal-iso) (let* ((dayname (calendar-day-name date)) - (day (cadr date)) + (day (calendar-extract-day date)) (day-of-week (calendar-day-of-week date)) - (month (car date)) + (month (calendar-extract-month date)) (monthname (calendar-month-name month)) - (year (nth 2 date)) + (year (calendar-extract-year date)) (iso-week (org-days-to-iso-week (calendar-absolute-from-gregorian date))) ;; (weekyear (cond ((and (= month 1) (>= iso-week 52)) @@ -5828,8 +5828,11 @@ org-agenda-get-timestamps (regexp-quote (format-time-string "%Y-%m-%d" ; We do not use `org-time-stamp-format' to not demand day name in timestamps. - (org-encode-time ; DATE bound by calendar - 0 0 0 (nth 1 date) (car date) (nth 2 date)))) + (org-encode-time ; DATE bound by calendar + 0 0 0 + (calendar-extract-day date) + (calendar-extract-month date) + (calendar-extract-year date)))) "\\|\\(<[0-9]+-[0-9]+-[0-9]+[^>\n]+?\\+[0-9]+[hdwmy]>\\)" "\\|\\(<%%\\(([^>\n]+)\\)\\([^\n>]*\\)>\\)")) timestamp-items) @@ -6110,8 +6113,11 @@ org-agenda-get-progress (regexp-quote (format-time-string "%Y-%m-%d" ; We do not use `org-time-stamp-format' to not demand day name in timestamps. - (org-encode-time ; DATE bound by calendar - 0 0 0 (nth 1 date) (car date) (nth 2 date)))))) + (org-encode-time ; DATE bound by calendar + 0 0 0 + (calendar-extract-day date) + (calendar-extract-month date) + (calendar-extract-year date)))))) (org-agenda-search-headline-for-time nil) marker hdmarker priority category level tags closedp type statep clockp state ee txt extra timestr rest clocked inherited-tags @@ -8919,9 +8925,9 @@ org-agenda-compute-starting-span is a cons cell with the starting date and the number of days, so that the date SD will be in that range." (let* ((greg (calendar-gregorian-from-absolute sd)) - ;; (dg (nth 1 greg)) - (mg (car greg)) - (yg (nth 2 greg))) + ;; (dg (calendar-extract-day greg)) + (mg (calendar-extract-month greg)) + (yg (calendar-extract-year greg))) (cond ((eq span 'day) (when n @@ -10331,9 +10337,10 @@ org-agenda-diary-entry-in-org-file (org-agenda-add-entry-to-org-agenda-diary-file 'day text d1) (and (equal (buffer-name) org-agenda-buffer-name) (org-agenda-redo))) ((equal char ?a) - (setq d1 (list (car d1) (nth 1 d1) - (read-number (format "Reference year [%d]: " (nth 2 d1)) - (nth 2 d1)))) + (setq d1 (list (calendar-extract-month d1) (calendar-extract-day d1) + (read-number (format "Reference year [%d]: " + (calendar-extract-year d1)) + (calendar-extract-year d1)))) (setq text (read-string "Anniversary (use %d to show years): ")) (org-agenda-add-entry-to-org-agenda-diary-file 'anniversary text d1) (and (equal (buffer-name) org-agenda-buffer-name) (org-agenda-redo))) @@ -10400,7 +10407,10 @@ org-agenda-add-entry-to-org-agenda-diary-file (backward-char 1) (insert "\n") (insert (format "%%%%(org-anniversary %d %2d %2d) %s" - (nth 2 d1) (car d1) (nth 1 d1) text))) + (calendar-extract-year d1) + (calendar-extract-month d1) + (calendar-extract-day d1) + text))) (day (let ((org-prefix-has-time t) (org-agenda-time-leading-zero t) @@ -10548,8 +10558,8 @@ org-agenda-execute-calendar-command (get-text-property point 'day)))) ;; the following 2 vars are needed in the calendar (org-dlet - ((displayed-month (car date)) - (displayed-year (nth 2 date))) + ((displayed-month (calendar-extract-month date)) + (displayed-year (calendar-extract-year date))) (unwind-protect (progn (fset 'calendar-cursor-to-date @@ -10949,7 +10959,10 @@ org-agenda-bulk-action (let* ((date (calendar-gregorian-from-absolute (+ (org-today) distance))) (time (org-encode-time - 0 0 0 (nth 1 date) (nth 0 date) (nth 2 date)))) + 0 0 0 + (calendar-extract-day date) + (calendar-extract-month date) + (calendar-extract-year date)))) (org-agenda-schedule nil time)))))))) (?f diff --git a/lisp/org-clock.el b/lisp/org-clock.el index ce2d23a9b..f96d10285 100644 --- a/lisp/org-clock.el +++ b/lisp/org-clock.el @@ -2471,9 +2471,9 @@ org-clock-special-range (list (string-to-number (match-string 2 skey)) 1 (string-to-number (match-string 1 skey))))))) - (setq d (nth 1 date) - month (car date) - y (nth 2 date) + (setq d (calendar-extract-day date) + month (calendar-extract-month date) + y (calendar-extract-year date) dow 1 key 'week))) ((string-match "\\`\\([0-9]+\\)-[qQ]\\([1-4]\\)\\'" skey) @@ -2483,9 +2483,9 @@ org-clock-special-range (calendar-iso-to-absolute (org-quarter-to-date q (string-to-number (match-string 1 skey))))))) - (setq d (nth 1 date) - month (car date) - y (nth 2 date) + (setq d (calendar-extract-day date) + month (calendar-extract-month date) + y (calendar-extract-year date) dow 1 key 'quarter))) ((string-match @@ -2637,7 +2637,11 @@ org-clocktable-shift (calendar-iso-to-absolute (list (+ mw n) 1 y)))) (setq ins (format-time-string "%G-W%V" - (org-encode-time 0 0 0 (nth 1 date) (car date) (nth 2 date))))) + (org-encode-time + 0 0 0 + (calendar-extract-day date) + (calendar-extract-month date) + (calendar-extract-year date))))) ((and wp (string-match "q\\|Q" wp) mw (> (length wp) 0)) (require 'cal-iso) ; if the 4th + 1 quarter is requested we flip to the 1st quarter of the next year @@ -2654,7 +2658,11 @@ org-clocktable-shift (calendar-iso-to-absolute (org-quarter-to-date (+ mw n) y)))) (setq ins (format-time-string (concat (number-to-string y) "-Q" (number-to-string (+ mw n))) - (org-encode-time 0 0 0 (nth 1 date) (car date) (nth 2 date))))) + (org-encode-time + 0 0 0 + (calendar-extract-day date) + (calendar-extract-month date) + (calendar-extract-year date))))) (mw (setq ins (format-time-string "%Y-%m" @@ -3162,9 +3170,15 @@ org-clock-get-table-data (when (integerp ts) (setq ts (calendar-gregorian-from-absolute ts))) (when (integerp te) (setq te (calendar-gregorian-from-absolute te))) (when (and ts (listp ts)) - (setq ts (format "%4d-%02d-%02d" (nth 2 ts) (car ts) (nth 1 ts)))) + (setq ts (format "%4d-%02d-%02d" + (calendar-extract-year ts) + (calendar-extract-month ts) + (calendar-extract-day ts)))) (when (and te (listp te)) - (setq te (format "%4d-%02d-%02d" (nth 2 te) (car te) (nth 1 te)))) + (setq te (format "%4d-%02d-%02d" + (calendar-extract-year te) + (calendar-extract-month te) + (calendar-extract-day te)))) ;; Now the times are strings we can parse. (if ts (setq ts (org-matcher-time ts))) (if te (setq te (org-matcher-time te))) diff --git a/lisp/org-datetree.el b/lisp/org-datetree.el index a1d565c44..ed261a514 100644 --- a/lisp/org-datetree.el +++ b/lisp/org-datetree.el @@ -343,9 +343,9 @@ org-datetree-cleanup (throw 'next nil)) (let* ((dct (decode-time (org-time-string-to-time (match-string 0)))) (date (list (nth 4 dct) (nth 3 dct) (nth 5 dct))) - (year (nth 2 date)) - (month (car date)) - (day (nth 1 date)) + (year (calendar-extract-year date)) + (month (calendar-extract-month date)) + (day (calendar-extract-day date)) (pos (point)) (hdl-pos (progn (org-back-to-heading t) (point)))) (unless (org-up-heading-safe) diff --git a/lisp/org.el b/lisp/org.el index fda23ee89..211195bca 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -14687,9 +14687,9 @@ org-read-date-analyze ; iso-date (calendar-gregorian-from-absolute ; (calendar-iso-to-absolute ; (list iso-week day year))))) - (setq month (car iso-date) - year (nth 2 iso-date) - day (nth 1 iso-date))) + (setq month (calendar-extract-month iso-date) + year (calendar-extract-year iso-date) + day (calendar-extract-day iso-date))) (deltan (setq futurep nil) (unless deltadef @@ -14787,7 +14787,11 @@ org-funcall-in-calendar (apply func args) (when (and (not keepdate) (calendar-cursor-to-date)) (let* ((date (calendar-cursor-to-date)) - (time (org-encode-time 0 0 0 (nth 1 date) (nth 0 date) (nth 2 date)))) + (time (org-encode-time + 0 0 0 + (calendar-extract-day date) + (calendar-extract-month date) + (calendar-extract-year date)))) (setq org-ans2 (format-time-string "%Y-%m-%d" time)))) (move-overlay org-date-ovl (1- (point)) (1+ (point)) (current-buffer)))) @@ -14898,7 +14902,11 @@ org-calendar-select (interactive) (when (calendar-cursor-to-date) (let* ((date (calendar-cursor-to-date)) - (time (org-encode-time 0 0 0 (nth 1 date) (nth 0 date) (nth 2 date)))) + (time (org-encode-time + 0 0 0 + (calendar-extract-day date) + (calendar-extract-month date) + (calendar-extract-year date)))) (setq org-ans1 (format-time-string "%Y-%m-%d" time))) (when (active-minibuffer-window) (exit-minibuffer)))) @@ -15018,7 +15026,11 @@ org-calendar-select-mouse (mouse-set-point ev) (when (calendar-cursor-to-date) (let* ((date (calendar-cursor-to-date)) - (time (org-encode-time 0 0 0 (nth 1 date) (nth 0 date) (nth 2 date)))) + (time (org-encode-time + 0 0 0 + (calendar-extract-day date) + (calendar-extract-month date) + (calendar-extract-year date)))) (setq org-ans1 (format-time-string "%Y-%m-%d" time))) (when (active-minibuffer-window) (exit-minibuffer)))) @@ -15285,7 +15297,11 @@ org-time-from-absolute "Return the time corresponding to date D. D may be an absolute day number, or a calendar-type list (month day year)." (when (numberp d) (setq d (calendar-gregorian-from-absolute d))) - (org-encode-time 0 0 0 (nth 1 d) (car d) (nth 2 d))) + (org-encode-time + 0 0 0 + (calendar-extract-day d) + (calendar-extract-month d) + (calendar-extract-year d))) (defvar org-agenda-current-date) (defun org-calendar-holiday () @@ -15405,16 +15421,16 @@ org-closest-date ;; Add N months to gregorian date D, i.e., ;; a list (MONTH DAY YEAR). Return a valid ;; gregorian date. - (let ((m (+ (nth 0 d) n))) + (let ((m (+ (calendar-extract-month d) n))) (list (mod m 12) - (nth 1 d) - (+ (/ m 12) (nth 2 d)))))) + (calendar-extract-day d) + (+ (/ m 12) (calendar-extract-year d)))))) (months ; Complete months to TARGET. - (* (/ (+ (* 12 (- (nth 2 target) (nth 2 base))) - (- (nth 0 target) (nth 0 base)) + (* (/ (+ (* 12 (- (calendar-extract-year target) (calendar-extract-year base))) + (- (calendar-extract-month target) (calendar-extract-month base)) ;; If START's day is greater than ;; TARGET's, remove incomplete month. - (if (> (nth 1 target) (nth 1 base)) 0 -1)) + (if (> (calendar-extract-day target) (calendar-extract-day base)) 0 -1)) value) value)) (before (funcall add-months base months))) @@ -15423,18 +15439,18 @@ org-closest-date (calendar-absolute-from-gregorian (funcall add-months before value))))) (_ - (let* ((d (nth 1 base)) - (m (nth 0 base)) - (y (nth 2 base)) + (let* ((d (calendar-extract-day base)) + (m (calendar-extract-month base)) + (y (calendar-extract-year base)) (years ; Complete years to TARGET. - (* (/ (- (nth 2 target) + (* (/ (- (calendar-extract-year target) y ;; If START's month and day are ;; greater than TARGET's, remove ;; incomplete year. - (if (or (> (nth 0 target) m) - (and (= (nth 0 target) m) - (> (nth 1 target) d))) + (if (or (> (calendar-extract-month target) m) + (and (= (calendar-extract-month target) m) + (> (calendar-extract-day target) d))) 0 1)) value) @@ -15442,7 +15458,7 @@ org-closest-date (before (list m d (+ y years)))) (setf n1 (calendar-absolute-from-gregorian before)) (setf n2 (calendar-absolute-from-gregorian - (list m d (+ (nth 2 before) value))))))) + (list m d (+ (calendar-extract-year before) value))))))) ;; Handle PREFER parameter, if any. (cond ((eq prefer 'past) (if (= cday n2) n2 n1)) @@ -15867,7 +15883,11 @@ org-date-from-calendar (org-timestamp-change 0 'calendar) (let ((cal-date (org-get-date-from-calendar))) (org-insert-timestamp - (org-encode-time 0 0 0 (nth 1 cal-date) (car cal-date) (nth 2 cal-date)))))) + (org-encode-time + 0 0 0 + (calendar-extract-day cal-date) + (calendar-extract-month cal-date) + (calendar-extract-year cal-date)))))) (defcustom org-image-actual-width t "When non-nil, use the actual width of images when inlining them. @@ -19464,13 +19484,17 @@ org-get-cursor-date ((eq major-mode 'calendar-mode) (setq date (calendar-cursor-to-date) defd (org-encode-time 0 (or mod 0) (or hod org-extend-today-until) - (nth 1 date) (nth 0 date) (nth 2 date)))) + (calendar-extract-day date) + (calendar-extract-month date) + (calendar-extract-year date)))) ((eq major-mode 'org-agenda-mode) (setq day (get-text-property (point) 'day)) (when day (setq date (calendar-gregorian-from-absolute day) defd (org-encode-time 0 (or mod 0) (or hod org-extend-today-until) - (nth 1 date) (nth 0 date) (nth 2 date)))))) + (calendar-extract-day date) + (calendar-extract-month date) + (calendar-extract-year date)))))) (or defd (current-time)))) (defun org-mark-subtree (&optional up) base-commit: 645e4d4a6bfb4746b574adfbb93bb8c7f8493195 -- 2.52.0
>From 4f46d9afa2935764fcd03d0050f31442ec95f083 Mon Sep 17 00:00:00 2001 From: Morgan Smith <[email protected]> Date: Sat, 28 Mar 2026 13:39:19 -0400 Subject: [PATCH 02/10] Use decoded-time accessor functions * doc/org-manual.org (Computed tag filtering): * lisp/org-agenda.el (org-agenda-check-clock-gap, org-agenda-date-later) (org-agenda-todo-yesterday): * lisp/org-capture.el (org-capture-fill-template): * lisp/org-clock.el (org-clock-get-sum-start, org-clock-special-range): * lisp/org-colview.el (org-colview-construct-allowed-dates): * lisp/org-datetree.el (org-datetree-cleanup): * lisp/org-element.el (org-element-timestamp-parser): * lisp/org.el (org-current-time, org-current-effective-time, org-todo-yesterday) (org-read-date, org-read-date-analyze, org-read-date-get-relative) (org-display-custom-time, org-closest-date, org-date-to-gregorian) (org-get-cursor-date): * lisp/ox-icalendar.el (org-icalendar--vtodo): * testing/lisp/test-org-clock.el (org-test-clock-create-timestamp): * testing/org-test.el (org--compile-when): Use decoded-time accessor functions. --- doc/org-manual.org | 2 +- lisp/org-agenda.el | 10 +-- lisp/org-capture.el | 6 +- lisp/org-clock.el | 20 +++--- lisp/org-colview.el | 12 ++-- lisp/org-datetree.el | 2 +- lisp/org-element.el | 20 +++--- lisp/org.el | 112 +++++++++++++++++---------------- lisp/ox-icalendar.el | 10 +-- testing/lisp/test-org-clock.el | 10 +-- testing/org-test.el | 2 +- 11 files changed, 107 insertions(+), 99 deletions(-) diff --git a/doc/org-manual.org b/doc/org-manual.org index 904e1270d..15b58390f 100644 --- a/doc/org-manual.org +++ b/doc/org-manual.org @@ -9957,7 +9957,7 @@ **** Computed tag filtering (/= 0 (call-process "/sbin/ping" nil nil nil "-c1" "-q" "-t1" "mail.gnu.org"))) ((member tag '("errand" "call")) - (let ((hr (nth 2 (decode-time)))) + (let ((hr (decoded-time-hour (decode-time)))) (or (< hr 8) (> hr 21))))) (concat "-" tag))) diff --git a/lisp/org-agenda.el b/lisp/org-agenda.el index a63063d12..bc4e9471c 100644 --- a/lisp/org-agenda.el +++ b/lisp/org-agenda.el @@ -6306,8 +6306,8 @@ org-agenda-check-clock-gap (let* ((t1dec (decode-time t1)) (t2dec (decode-time t2)) ;; compute the minute on the day - (min1 (+ (nth 1 t1dec) (* 60 (nth 2 t1dec)))) - (min2 (+ (nth 1 t2dec) (* 60 (nth 2 t2dec))))) + (min1 (+ (decoded-time-minute t1dec) (* 60 (decoded-time-hour t1dec)))) + (min2 (+ (decoded-time-minute t2dec) (* 60 (decoded-time-hour t2dec))))) (when (< min2 min1) ;; if min2 is smaller than min1, this means it is on the next day. ;; Wrap it to after midnight. @@ -10114,7 +10114,9 @@ org-agenda-date-later (not (save-match-data (org-at-date-range-p)))) (setq cdate (org-parse-time-string (match-string 0) 'nodefault) cdate (calendar-absolute-from-gregorian - (list (nth 4 cdate) (nth 3 cdate) (nth 5 cdate))) + (list (decoded-time-month cdate) + (decoded-time-day cdate) + (decoded-time-year cdate))) today (org-today)) (when (> today cdate) ;; immediately shift to today @@ -11234,7 +11236,7 @@ org-agenda-todo-yesterday "Like `org-agenda-todo' but the time of change will be 23:59 of yesterday." (interactive "P") (let* ((org-use-effective-time t) - (hour (nth 2 (decode-time (org-current-time)))) + (hour (decoded-time-hour (decode-time (org-current-time)))) (org-extend-today-until (1+ hour))) (org-agenda-todo arg))) diff --git a/lisp/org-capture.el b/lisp/org-capture.el index 705fdc902..f1303cecc 100644 --- a/lisp/org-capture.el +++ b/lisp/org-capture.el @@ -1756,8 +1756,10 @@ org-capture-fill-template (file (buffer-file-name (or (buffer-base-buffer buffer) buffer))) (time (let* ((c (or (org-capture-get :default-time) (current-time))) (d (decode-time c))) - (if (< (nth 2 d) org-extend-today-until) - (org-encode-time 0 59 23 (1- (nth 3 d)) (nth 4 d) (nth 5 d)) + (if (< (decoded-time-hour d) org-extend-today-until) + (org-encode-time 0 59 23 (1- (decoded-time-day d)) + (decoded-time-month d) + (decoded-time-year d)) c))) (v-t (format-time-string (org-time-stamp-format nil) time)) (v-T (format-time-string (org-time-stamp-format t) time)) diff --git a/lisp/org-clock.el b/lisp/org-clock.el index f96d10285..8a891969d 100644 --- a/lisp/org-clock.el +++ b/lisp/org-clock.el @@ -1676,10 +1676,10 @@ org-clock-get-sum-start ((equal cmt "today") (setq org--msg-extra "showing today's task time.") (let* ((dt (decode-time)) - (hour (nth 2 dt)) - (day (nth 3 dt))) - (if (< hour org-extend-today-until) (setf (nth 3 dt) (1- day))) - (setf (nth 2 dt) org-extend-today-until) + (hour (decoded-time-hour dt)) + (day (decoded-time-day dt))) + (if (< hour org-extend-today-until) (setf (decoded-time-day dt) (1- day))) + (setf (decoded-time-hour dt) org-extend-today-until) (org-encode-time (apply #'list 0 0 (nthcdr 2 dt))))) ((or (equal cmt "all") (and (or (not cmt) (equal cmt "auto")) @@ -2443,12 +2443,12 @@ org-clock-special-range month). If you can combine both, the month starting day will have priority." (let* ((tm (decode-time time)) - (m (nth 1 tm)) - (h (nth 2 tm)) - (d (nth 3 tm)) - (month (nth 4 tm)) - (y (nth 5 tm)) - (dow (nth 6 tm)) + (m (decoded-time-minute tm)) + (h (decoded-time-hour tm)) + (d (decoded-time-day tm)) + (month (decoded-time-month tm)) + (y (decoded-time-year tm)) + (dow (decoded-time-weekday tm)) (skey (format "%s" key)) (shift 0) (q (cond ((>= month 10) 4) diff --git a/lisp/org-colview.el b/lisp/org-colview.el index 5df884199..0b2cde30f 100644 --- a/lisp/org-colview.el +++ b/lisp/org-colview.el @@ -823,15 +823,15 @@ org-colview-construct-allowed-dates (when (and s (string-match (concat "^" org-ts-regexp3 "$") s)) (let* ((time (org-parse-time-string s 'nodefaults)) (active (equal (string-to-char s) ?<)) - (fmt (org-time-stamp-format (nth 1 time) (not active))) + (fmt (org-time-stamp-format (decoded-time-minute time) (not active))) time-before time-after) - (setf (car time) (or (car time) 0)) - (setf (nth 1 time) (or (nth 1 time) 0)) - (setf (nth 2 time) (or (nth 2 time) 0)) + (setf (decoded-time-second time) (or (decoded-time-second time) 0)) + (setf (decoded-time-minute time) (or (decoded-time-minute time) 0)) + (setf (decoded-time-hour time) (or (decoded-time-hour time) 0)) (setq time-before (copy-sequence time)) (setq time-after (copy-sequence time)) - (setf (nth 3 time-before) (1- (nth 3 time))) - (setf (nth 3 time-after) (1+ (nth 3 time))) + (setf (decoded-time-day time-before) (1- (decoded-time-day time))) + (setf (decoded-time-day time-after) (1+ (decoded-time-day time))) (mapcar (lambda (x) (format-time-string fmt (org-encode-time x))) (list time-before time time-after))))) diff --git a/lisp/org-datetree.el b/lisp/org-datetree.el index ed261a514..86fdd0192 100644 --- a/lisp/org-datetree.el +++ b/lisp/org-datetree.el @@ -342,7 +342,7 @@ org-datetree-cleanup (string-match sre tmp)) (throw 'next nil)) (let* ((dct (decode-time (org-time-string-to-time (match-string 0)))) - (date (list (nth 4 dct) (nth 3 dct) (nth 5 dct))) + (date (list (decoded-time-month dct) (decoded-time-day dct) (decoded-time-year dct))) (year (calendar-extract-year date)) (month (calendar-extract-month date)) (day (calendar-extract-day date)) diff --git a/lisp/org-element.el b/lisp/org-element.el index 7078ce1f8..80868b096 100644 --- a/lisp/org-element.el +++ b/lisp/org-element.el @@ -4481,21 +4481,21 @@ org-element-timestamp-parser ;; Parse date-start. (unless diaryp (let ((date (org-parse-time-string date-start t))) - (setq year-start (nth 5 date) - month-start (nth 4 date) - day-start (nth 3 date) - hour-start (nth 2 date) - minute-start (nth 1 date)))) + (setq year-start (decoded-time-year date) + month-start (decoded-time-month date) + day-start (decoded-time-day date) + hour-start (decoded-time-hour date) + minute-start (decoded-time-minute date)))) ;; Compute date-end. It can be provided directly in timestamp, ;; or extracted from time range. Otherwise, it defaults to the ;; same values as date-start. (unless diaryp (let ((date (and date-end (org-parse-time-string date-end t)))) - (setq year-end (or (nth 5 date) year-start) - month-end (or (nth 4 date) month-start) - day-end (or (nth 3 date) day-start) - hour-end (or (nth 2 date) (car time-range) hour-start) - minute-end (or (nth 1 date) (cdr time-range) minute-start)))) + (setq year-end (or (decoded-time-year date) year-start) + month-end (or (decoded-time-month date) month-start) + day-end (or (decoded-time-day date) day-start) + hour-end (or (decoded-time-hour date) (car time-range) hour-start) + minute-end (or (decoded-time-minute date) (cdr time-range) minute-start)))) ;; Diary timestamp with time. (when (and diaryp (string-match "\\([012]?[0-9]\\):\\([0-5][0-9]\\)\\(-\\([012]?[0-9]\\):\\([0-5][0-9]\\)\\)?" date-start)) diff --git a/lisp/org.el b/lisp/org.el index 211195bca..41ec50166 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -5385,7 +5385,7 @@ org-current-time (let* ((time (decode-time now)) (res (org-encode-time (apply #'list - 0 (* r (round (nth 1 time) r)) + 0 (* r (round (decoded-time-minute time) r)) (nthcdr 2 time))))) (if (or (not past) (time-less-p res now)) res @@ -9608,8 +9608,8 @@ org-current-effective-time (cond (org-use-last-clock-out-time-as-effective-time (or (org-clock-get-last-clock-out-time) ct)) - ((and org-use-effective-time (< (nth 2 dct) org-extend-today-until)) - (org-encode-time 0 59 23 (1- (nth 3 dct)) (nth 4 dct) (nth 5 dct))) + ((and org-use-effective-time (< (decoded-time-hour dct) org-extend-today-until)) + (org-encode-time 0 59 23 (1- (decoded-time-day dct)) (decoded-time-month dct) (decoded-time-year dct))) (t ct)))) ct1)) @@ -9619,7 +9619,7 @@ org-todo-yesterday (if (eq major-mode 'org-agenda-mode) (org-agenda-todo-yesterday arg) (let* ((org-use-effective-time t) - (hour (nth 2 (decode-time (org-current-time)))) + (hour (decoded-time-hour (decode-time (org-current-time)))) (org-extend-today-until (1+ hour))) (org-todo arg)))) @@ -14368,9 +14368,9 @@ org-read-date ;; time is not given. (when (and (not default-time) (not org-overriding-default-time) - (< (nth 2 org-defdecode) org-extend-today-until)) - (setf (nth 2 org-defdecode) -1) - (setf (nth 1 org-defdecode) 59) + (< (decoded-time-hour org-defdecode) org-extend-today-until)) + (setf (decoded-time-hour org-defdecode) -1) + (setf (decoded-time-minute org-defdecode) 59) (setq org-def (org-encode-time org-defdecode)) (setq org-defdecode (decode-time org-def))) (let* ((timestr (format-time-string @@ -14456,9 +14456,11 @@ org-read-date (setq final (decode-time final)) (if (and (boundp 'org-time-was-given) org-time-was-given) (format "%04d-%02d-%02d %02d:%02d" - (nth 5 final) (nth 4 final) (nth 3 final) - (nth 2 final) (nth 1 final)) - (format "%04d-%02d-%02d" (nth 5 final) (nth 4 final) (nth 3 final)))))) + (decoded-time-year final) (decoded-time-month final) + (decoded-time-day final) (decoded-time-hour final) + (decoded-time-minute final)) + (format "%04d-%02d-%02d" (decoded-time-year final) + (decoded-time-month final) (decoded-time-day final)))))) (defun org-read-date-display () "Display the current date prompt interpretation in the minibuffer." @@ -14618,51 +14620,51 @@ org-read-date-analyze (substring ans (match-end 7)))))) (setq tl (parse-time-string ans) - day (or (nth 3 tl) (nth 3 org-defdecode)) + day (or (decoded-time-day tl) (decoded-time-day org-defdecode)) month - (cond ((nth 4 tl)) - ((not org-read-date-prefer-future) (nth 4 org-defdecode)) + (cond ((decoded-time-month tl)) + ((not org-read-date-prefer-future) (decoded-time-month org-defdecode)) ;; Day was specified. Make sure DAY+MONTH ;; combination happens in the future. - ((nth 3 tl) + ((decoded-time-day tl) (setq futurep t) - (if (< day (nth 3 nowdecode)) (1+ (nth 4 nowdecode)) - (nth 4 nowdecode))) - (t (nth 4 org-defdecode))) + (if (< day (decoded-time-day nowdecode)) (1+ (decoded-time-month nowdecode)) + (decoded-time-month nowdecode))) + (t (decoded-time-month org-defdecode))) year - (cond ((and (not kill-year) (nth 5 tl))) - ((not org-read-date-prefer-future) (nth 5 org-defdecode)) + (cond ((and (not kill-year) (decoded-time-year tl))) + ((not org-read-date-prefer-future) (decoded-time-year org-defdecode)) ;; Month was guessed in the future and is at least ;; equal to NOWDECODE's. Fix year accordingly. (futurep - (if (or (> month (nth 4 nowdecode)) - (>= day (nth 3 nowdecode))) - (nth 5 nowdecode) - (1+ (nth 5 nowdecode)))) + (if (or (> month (decoded-time-month nowdecode)) + (>= day (decoded-time-day nowdecode))) + (decoded-time-year nowdecode) + (1+ (decoded-time-year nowdecode)))) ;; Month was specified. Make sure MONTH+YEAR ;; combination happens in the future. - ((nth 4 tl) + ((decoded-time-month tl) (setq futurep t) - (cond ((> month (nth 4 nowdecode)) (nth 5 nowdecode)) - ((< month (nth 4 nowdecode)) (1+ (nth 5 nowdecode))) - ((< day (nth 3 nowdecode)) (1+ (nth 5 nowdecode))) - (t (nth 5 nowdecode)))) - (t (nth 5 org-defdecode))) - hour (or (nth 2 tl) (nth 2 org-defdecode)) - minute (or (nth 1 tl) (nth 1 org-defdecode)) - second (or (nth 0 tl) 0) - wday (nth 6 tl)) + (cond ((> month (decoded-time-month nowdecode)) (decoded-time-year nowdecode)) + ((< month (decoded-time-month nowdecode)) (1+ (decoded-time-year nowdecode))) + ((< day (decoded-time-day nowdecode)) (1+ (decoded-time-year nowdecode))) + (t (decoded-time-year nowdecode)))) + (t (decoded-time-year org-defdecode))) + hour (or (decoded-time-hour tl) (decoded-time-hour org-defdecode)) + minute (or (decoded-time-minute tl) (decoded-time-minute org-defdecode)) + second (or (decoded-time-second tl) 0) + wday (decoded-time-weekday tl)) (when (and (eq org-read-date-prefer-future 'time) - (not (nth 3 tl)) (not (nth 4 tl)) (not (nth 5 tl)) - (equal day (nth 3 nowdecode)) - (equal month (nth 4 nowdecode)) - (equal year (nth 5 nowdecode)) - (nth 2 tl) - (or (< (nth 2 tl) (nth 2 nowdecode)) - (and (= (nth 2 tl) (nth 2 nowdecode)) - (nth 1 tl) - (< (nth 1 tl) (nth 1 nowdecode))))) + (not (decoded-time-day tl)) (not (decoded-time-month tl)) (not (decoded-time-year tl)) + (equal day (decoded-time-day nowdecode)) + (equal month (decoded-time-month nowdecode)) + (equal year (decoded-time-year nowdecode)) + (decoded-time-hour tl) + (or (< (decoded-time-hour tl) (decoded-time-hour nowdecode)) + (and (= (decoded-time-hour tl) (decoded-time-hour nowdecode)) + (decoded-time-minute tl) + (< (decoded-time-minute tl) (decoded-time-minute nowdecode))))) (setq day (1+ day) futurep t)) @@ -14694,7 +14696,9 @@ org-read-date-analyze (setq futurep nil) (unless deltadef (let ((now (decode-time))) - (setq day (nth 3 now) month (nth 4 now) year (nth 5 now)))) + (setq day (decoded-time-day now) + month (decoded-time-month now) + year (decoded-time-year now)))) ;; FIXME: Duplicated value in ‘cond’: "" (cond ((member deltaw '("h" "")) (when (boundp 'org-time-was-given) @@ -14704,14 +14708,14 @@ org-read-date-analyze ((equal deltaw "w") (setq day (+ day (* 7 deltan)))) ((equal deltaw "m") (setq month (+ month deltan))) ((equal deltaw "y") (setq year (+ year deltan))))) - ((and wday (not (nth 3 tl))) + ((and wday (not (decoded-time-day tl))) ;; Weekday was given, but no day, so pick that day in the week ;; on or after the derived date. - (setq wday1 (nth 6 (decode-time (org-encode-time 0 0 0 day month year)))) + (setq wday1 (decoded-time-weekday (decode-time (org-encode-time 0 0 0 day month year)))) (unless (equal wday wday1) (setq day (+ day (% (- wday wday1 -7) 7)))))) (when (and (boundp 'org-time-was-given) - (nth 2 tl)) + (decoded-time-hour tl)) (setq org-time-was-given t)) (when (< year 100) (setq year (+ 2000 year))) ;; Check of the date is representable @@ -14724,7 +14728,7 @@ org-read-date-analyze (condition-case nil (ignore (org-encode-time second minute hour day month year)) (error - (setq year (nth 5 org-defdecode)) + (setq year (decoded-time-year org-defdecode)) (setq org-read-date-analyze-forced-year t)))) (setq org-read-date-analyze-futurep futurep) (list second minute hour day month year nil -1 nil))) @@ -14757,7 +14761,7 @@ org-read-date-get-relative (what (if (match-end 3) (match-string 3 s) "d")) (wday1 (cdr (assoc (downcase what) parse-time-weekdays))) (date (if rel default today)) - (wday (nth 6 (decode-time date))) + (wday (decoded-time-weekday (decode-time date))) delta) (if wday1 (progn @@ -14966,7 +14970,7 @@ org-display-custom-time (when (string-match "\\(-[0-9]+:[0-9]+\\)?\\( [.+]?\\+[0-9]+[hdwmy]\\(/[0-9]+[hdwmy]\\)?\\)?\\'" ts) (setq off (- (match-end 0) (match-beginning 0))))) (setq end (- end off)) - (setq with-hm (and (nth 1 t1) (nth 2 t1)) + (setq with-hm (and (decoded-time-minute t1) (decoded-time-hour t1)) tf (org-time-stamp-format with-hm 'no-brackets 'custom) time (org-fix-decoded-time t1) str (org-add-props @@ -15405,7 +15409,7 @@ org-closest-date ("h" (let ((missing-hours (mod (+ (- (* 24 (- cday sday)) - (nth 2 (org-parse-time-string start))) + (decoded-time-hour (org-parse-time-string start))) org-extend-today-until) value))) (setf n1 (if (= missing-hours 0) cday @@ -15471,8 +15475,8 @@ org-date-to-gregorian ((and (listp d) (= (length d) 3)) d) ((stringp d) (let ((d (org-parse-time-string d))) - (list (nth 4 d) (nth 3 d) (nth 5 d)))) - ((listp d) (list (nth 4 d) (nth 3 d) (nth 5 d))))) + (list (decoded-time-month d) (decoded-time-day d) (decoded-time-year d)))) + ((listp d) (list (decoded-time-month d) (decoded-time-day d) (decoded-time-year d))))) (defun org-timestamp-up (&optional arg) "Increase the date item at the cursor by one. @@ -19478,8 +19482,8 @@ org-get-cursor-date (setq hod (string-to-number (match-string 1 tp)) mod (string-to-number (match-string 2 tp)))) (or tp (let ((now (decode-time))) - (setq hod (nth 2 now) - mod (nth 1 now))))) + (setq hod (decoded-time-hour now) + mod (decoded-time-minute now))))) (cond ((eq major-mode 'calendar-mode) (setq date (calendar-cursor-to-date) diff --git a/lisp/ox-icalendar.el b/lisp/ox-icalendar.el index 9fc1e54cb..9414bf5cf 100644 --- a/lisp/ox-icalendar.el +++ b/lisp/ox-icalendar.el @@ -900,11 +900,11 @@ org-icalendar--vtodo (let ((now (decode-time))) (list 'timestamp (list :type 'active - :minute-start (nth 1 now) - :hour-start (nth 2 now) - :day-start (nth 3 now) - :month-start (nth 4 now) - :year-start (nth 5 now))))) + :minute-start (decoded-time-minute now) + :hour-start (decoded-time-hour now) + :day-start (decoded-time-day now) + :month-start (decoded-time-month now) + :year-start (decoded-time-year now))))) ((or (and (eq org-icalendar-todo-unscheduled-start 'deadline-warning) dl) diff --git a/testing/lisp/test-org-clock.el b/testing/lisp/test-org-clock.el index 034446548..9554ac4ec 100644 --- a/testing/lisp/test-org-clock.el +++ b/testing/lisp/test-org-clock.el @@ -35,11 +35,11 @@ org-test-clock-create-timestamp input nil (decode-time (current-time)))))))) (list 'timestamp (list :type (if inactive 'inactive 'active) - :minute-start (and with-time (nth 1 time)) - :hour-start (and with-time (nth 2 time)) - :day-start (nth 3 time) - :month-start (nth 4 time) - :year-start (nth 5 time)))))) + :minute-start (and with-time (decoded-time-minute time)) + :hour-start (and with-time (decoded-time-hour time)) + :day-start (decoded-time-day time) + :month-start (decoded-time-month time) + :year-start (decoded-time-year time)))))) (defun org-test-clock-create-clock (input1 &optional input2) "Create a clock line out of two date/time prompts. diff --git a/testing/org-test.el b/testing/org-test.el index c21c42835..ea2c0c8fb 100644 --- a/testing/org-test.el +++ b/testing/org-test.el @@ -351,7 +351,7 @@ org--compile-when (find-file full-path) (insert ";;; " file-name "\n\n" - ";; Copyright (c) " (nth 5 (decode-time (current-time))) + ";; Copyright (c) " (decoded-time-year (decode-time (current-time))) " " user-full-name "\n" ";; Authors: " user-full-name "\n\n" ";; Released under the GNU General Public License version 3\n" -- 2.52.0
>From ac9322b7b8967b1bbbcd4322e2194f7c69df5e21 Mon Sep 17 00:00:00 2001 From: Morgan Smith <[email protected]> Date: Sat, 28 Mar 2026 10:54:50 -0400 Subject: [PATCH 03/10] org-parse-time-string: Use `make-decoded-time' * lisp/org-macs.el (org-parse-time-string): Use `make-decoded-time' to ensure the return value is a proper decoded-time structure instead of relying on the specific list structure. --- lisp/org-macs.el | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lisp/org-macs.el b/lisp/org-macs.el index d769fffa0..a7ed0cbb7 100644 --- a/lisp/org-macs.el +++ b/lisp/org-macs.el @@ -1616,6 +1616,8 @@ org-encode-time (_ (error "`org-encode-time' may be called with 1, 6, or 9 arguments but %d given" (length time))))))) +(declare-function make-decoded-time "time-date" (&rest args)) + (defun org-parse-time-string (s &optional nodefault) "Parse Org time string S. @@ -1629,17 +1631,17 @@ org-parse-time-string This should be a lot faster than the `parse-time-string'." (unless (string-match org-ts-regexp0 s) (error "Not an Org time string: %s" s)) - (list 0 - (cond ((match-beginning 8) (string-to-number (match-string 8 s))) - (nodefault nil) - (t 0)) - (cond ((match-beginning 7) (string-to-number (match-string 7 s))) - (nodefault nil) - (t 0)) - (string-to-number (match-string 4 s)) - (string-to-number (match-string 3 s)) - (string-to-number (match-string 2 s)) - nil -1 nil)) + (make-decoded-time + :second 0 + :minute (cond ((match-beginning 8) (string-to-number (match-string 8 s))) + (nodefault nil) + (t 0)) + :hour (cond ((match-beginning 7) (string-to-number (match-string 7 s))) + (nodefault nil) + (t 0)) + :day (string-to-number (match-string 4 s)) + :month (string-to-number (match-string 3 s)) + :year (string-to-number (match-string 2 s)))) (defun org-matcher-time (s) "Interpret a time comparison value S as a floating point time. -- 2.52.0
>From 4f420c3a1e1c701ca5658d45fdbb4e11ce343508 Mon Sep 17 00:00:00 2001 From: Morgan Smith <[email protected]> Date: Sun, 15 Mar 2026 12:47:59 -0400 Subject: [PATCH 04/10] Add a compatibility shim for the function decoded-time-add * lisp/org-compat.el (org-decoded-time-add): New function to make the fixed version of `decoded-time-add' available. --- lisp/org-compat.el | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lisp/org-compat.el b/lisp/org-compat.el index 0473148b5..55a4e481e 100644 --- a/lisp/org-compat.el +++ b/lisp/org-compat.el @@ -86,6 +86,7 @@ ;; `org-string-equal-ignore-case' is in _this_ file but isn't at the ;; top-level. (declare-function org-string-equal-ignore-case "org-compat" (string1 string2)) +(declare-function decoded-time-add "time-date" (time delta)) (defvar calendar-mode-map) (defvar org-complex-heading-regexp) @@ -111,6 +112,21 @@ org-fold-core-style `(metadata . ,metadata) (complete-with-action action table string pred))))) +;; Broken for negative months before emacs commit bc33b70b280 +(if (version< "31" emacs-version) + (defalias 'org-decoded-time-add #'decoded-time-add) + (defun org-decoded-time-add (time delta) + "See doctring for `decoded-time-add'." + (if (decoded-time-month delta) + (let ((time (copy-sequence time)) + (delta (copy-sequence delta))) + (let ((new (+ (1- (decoded-time-month time)) (decoded-time-month delta)))) + (setf (decoded-time-month time) (1+ (mod new 12))) + (cl-incf (decoded-time-year time) (floor new 12))) + (setf (decoded-time-month delta) nil) + (decoded-time-add time delta)) + (decoded-time-add time delta)))) + ;;; Emacs < 29 compatibility -- 2.52.0
>From ed842b605c33516dc3e44bcff1c00504a761950a Mon Sep 17 00:00:00 2001 From: Morgan Smith <[email protected]> Date: Sat, 14 Mar 2026 16:14:17 -0400 Subject: [PATCH 05/10] org-timestamp-change: Use proper time structure accessor functions * lisp/org.el (org-timestamp-change): Use proper accessors for decoded-time and calendar time structures instead of list accessors. --- lisp/org.el | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/lisp/org.el b/lisp/org.el index 41ec50166..1ee72443d 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -15677,10 +15677,11 @@ org-timestamp-change (not current-prefix-arg)) ;; This looks like s-up and s-down. Change by one rounding step. (progn - (setq increment (* dm (cond ((> n 0) 1) ((< n 0) -1) (t 0)))) - (unless (= 0 (setq rem (% (nth 1 time0) dm))) - (setcar (cdr time0) (+ (nth 1 time0) - (if (> n 0) (- rem) (- dm rem)))))) + (setq increment (* dm (cond ((> n 0) 1) ((< n 0) -1) (t 0)))) + (unless (= 0 (setq rem (% (decoded-time-minute time0) dm))) + (setf (decoded-time-minute time0) + (+ (decoded-time-minute time0) + (if (> n 0) (- rem) (- dm rem)))))) ;; Do not round anything in `org-modify-ts-extra' when prefix ;; argument is supplied - just use whatever is provided by the ;; prefix argument. @@ -15712,13 +15713,15 @@ org-timestamp-change (setq extra (org-modify-ts-extra extra timestamp? n dm))) (when (eq what 'calendar) (let ((cal-date (org-get-date-from-calendar))) - (setcar (nthcdr 4 time0) (nth 0 cal-date)) ; month - (setcar (nthcdr 3 time0) (nth 1 cal-date)) ; day - (setcar (nthcdr 5 time0) (nth 2 cal-date)) ; year - (setcar time0 (or (car time0) 0)) - (setcar (nthcdr 1 time0) (or (nth 1 time0) 0)) - (setcar (nthcdr 2 time0) (or (nth 2 time0) 0)) - (setq time (org-encode-time time0)))) + (setq time (org-encode-time + (decoded-time-set-defaults + (make-decoded-time + :month (calendar-extract-month cal-date) + :day (calendar-extract-day cal-date) + :year (calendar-extract-year cal-date) + :second (decoded-time-second time0) + :minute (decoded-time-minute time0) + :hour (decoded-time-hour time0))))))) ;; Insert the new timestamp, and ensure point stays in the same ;; category as before (i.e. not after the last position in that ;; category). -- 2.52.0
>From 5e9ebf91594c3c7f06b342d1f5923ba0da31b0f8 Mon Sep 17 00:00:00 2001 From: Morgan Smith <[email protected]> Date: Sun, 15 Mar 2026 13:10:46 -0400 Subject: [PATCH 06/10] org-timestamp-change: Fix time addition Previously, trying to add a month to "<2026-01-31 Sat>" resulted in "<2026-03-03 Tue>". Now we get the correct result of "<2026-02-28 Sat>". * lisp/org.el (org-timestamp-change): Use `org-decoded-time-add' to add the time instead of relying of `org-encode-time' to figure it out. * testing/lisp/test-org.el (test-org/org-timestamp-change): Add regression tests. --- lisp/org.el | 18 ++++++++++-------- testing/lisp/test-org.el | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/lisp/org.el b/lisp/org.el index 1ee72443d..54008a571 100644 --- a/lisp/org.el +++ b/lisp/org.el @@ -15688,14 +15688,16 @@ org-timestamp-change (setq dm 1)) (setq time (org-encode-time - (apply #'list - (or (car time0) 0) - (+ (if (eq timestamp? 'minute) increment 0) (nth 1 time0)) - (+ (if (eq timestamp? 'hour) increment 0) (nth 2 time0)) - (+ (if (eq timestamp? 'day) increment 0) (nth 3 time0)) - (+ (if (eq timestamp? 'month) increment 0) (nth 4 time0)) - (+ (if (eq timestamp? 'year) increment 0) (nth 5 time0)) - (nthcdr 6 time0))))) + (org-decoded-time-add + time0 + (make-decoded-time + (cl-ecase timestamp? + (minute :minute) + (hour :hour) + (day :day) + (month :month) + (year :year)) + increment))))) (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 b31e0e5c1..ab3954510 100644 --- a/testing/lisp/test-org.el +++ b/testing/lisp/test-org.el @@ -9498,7 +9498,25 @@ test-org/org-timestamp-change (should (string= (buffer-substring (point-min) (point-max)) now-ts)) - (forward-char 1)))))))) + (forward-char 1))))))) + ;; Corner cases + (let ((org-timestamp-formats + (cons (replace-regexp-in-string + " %a" "" (car org-timestamp-formats)) + (replace-regexp-in-string + " %a" "" (cdr org-timestamp-formats))))) + (cl-flet ((test-org-timestamp-change (text n what expected) + (org-test-with-temp-text text + (org-timestamp-change n what) + (should (equal expected (buffer-string)))))) + ;; Changing between months with different number of days + (test-org-timestamp-change "<2026-01-31>" 1 'month "<2026-02-28>") + (test-org-timestamp-change "<2026-02-28>" 1 'month "<2026-03-28>") + (test-org-timestamp-change "<2026-03-31>" -1 'month "<2026-02-28>") + ;; Year boundry + (test-org-timestamp-change "<2025-12-31>" 1 'month "<2026-01-31>") + (test-org-timestamp-change "<2025-12-31>" 2 'month "<2026-02-28>") + (test-org-timestamp-change "<2026-02-28>" -2 'month "<2025-12-28>")))) (ert-deftest test-org/timestamp () "Test `org-timestamp' specifications." -- 2.52.0
>From 1fe8a74d494ab1b4b723c3c1b82cfd2e654c1ef8 Mon Sep 17 00:00:00 2001 From: Morgan Smith <[email protected]> Date: Fri, 5 Dec 2025 18:50:31 -0500 Subject: [PATCH 07/10] test-org/org-timestamp-change: Relax test requirements This had been fixed previously in commit c81c844d1 by hard coding the date. Relaxing the requirements allows for more testing. * testing/lisp/test-org.el (test-org/org-timestamp-change): Run the test using at most the 28th day of the month. --- testing/lisp/test-org.el | 79 +++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el index ab3954510..7bdb0c5c9 100644 --- a/testing/lisp/test-org.el +++ b/testing/lisp/test-org.el @@ -9462,43 +9462,48 @@ test-org/at-timestamp-p (ert-deftest test-org/org-timestamp-change () "Test `org-timestamp-change' specifications." - (org-test-at-time "2026-01-15" - (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))))))) + (let ((now (decode-time)) now-ts point) + ;; Decrementing a month from March 31st yields February + ;; 28th. This particular test is easier to write if the + ;; days don't change when modifying the month + (setf (decoded-time-day now) + (min (decoded-time-day now) 28)) + (setq now (encode-time now)) + (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)))))) ;; Corner cases (let ((org-timestamp-formats (cons (replace-regexp-in-string -- 2.52.0
>From 67b6878a7a476e769847ccd08d90b607d83ed41c Mon Sep 17 00:00:00 2001 From: Morgan Smith <[email protected]> Date: Sun, 15 Mar 2026 13:32:48 -0400 Subject: [PATCH 08/10] test-org/org-timestamp-change: Test minute rounding * testing/lisp/test-org.el (test-org/org-timestamp-change): Add tests for rounding the minutes according to `org-time-stamp-rounding-minutes'. --- testing/lisp/test-org.el | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el index 7bdb0c5c9..82beb9d58 100644 --- a/testing/lisp/test-org.el +++ b/testing/lisp/test-org.el @@ -9521,7 +9521,20 @@ test-org/org-timestamp-change ;; Year boundry (test-org-timestamp-change "<2025-12-31>" 1 'month "<2026-01-31>") (test-org-timestamp-change "<2025-12-31>" 2 'month "<2026-02-28>") - (test-org-timestamp-change "<2026-02-28>" -2 'month "<2025-12-28>")))) + (test-org-timestamp-change "<2026-02-28>" -2 'month "<2025-12-28>"))) + ;; Rounding from `org-time-stamp-rounding-minutes' + (cl-flet ((test-time-stamp-rounding (text rounding amount expected) + (let ((org-time-stamp-rounding-minutes (list 0 rounding))) + (org-test-with-temp-text text + (org-timestamp-change amount 'minute 'updown) + (re-search-forward org-ts-regexp1) + (should (equal expected (string-trim (match-string 6)))))))) + (dotimes (i 9) + (test-time-stamp-rounding "<2026-03-14 12:00>" (1+ i) 1 + (concat "12:0" (number-to-string (1+ i))))) + (dotimes (i 9) + (test-time-stamp-rounding "<2026-03-14 12:00>" (1+ i) -1 + (concat "11:5" (number-to-string (- 9 i))))))) (ert-deftest test-org/timestamp () "Test `org-timestamp' specifications." -- 2.52.0
>From 9f5b6d693ed44a05b892dc94c3274e74d475e5a3 Mon Sep 17 00:00:00 2001 From: Morgan Smith <[email protected]> Date: Sat, 14 Mar 2026 16:15:19 -0400 Subject: [PATCH 09/10] org-clock-timestamps-change: check location in timestamp Previously running this function with point on the brackets of a timestamp or with the point just after a timestamp would result in a puzzling error. Now the error shouldn't occur, and if it does it will be more descriptive. * lisp/org-clock.el (org-clock-timestamps-change): Guard against certain timestamp locations and try not to divide by zero. --- lisp/org-clock.el | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lisp/org-clock.el b/lisp/org-clock.el index 8a891969d..995ee0a4d 100644 --- a/lisp/org-clock.el +++ b/lisp/org-clock.el @@ -1936,7 +1936,7 @@ org-clock-timestamps-change 'org-timestamp-down)) (timestamp? (org-at-timestamp-p 'lax)) ts1 begts1 ts2 begts2 updatets1 tdiff) - (when timestamp? + (when (not (memq timestamp? '(nil bracket after))) (save-excursion (move-beginning-of-line 1) (re-search-forward org-ts-regexp3 nil t) @@ -1967,7 +1967,7 @@ org-clock-timestamps-change (goto-char begts) (org-timestamp-change (round (/ (float-time tdiff) - (pcase timestamp? + (pcase-exhaustive timestamp? (`minute 60) (`hour 3600) (`day (* 24 3600)) -- 2.52.0
>From 9e57fb36a77fbd512e7bb584ba25e9db0aa7207e Mon Sep 17 00:00:00 2001 From: Morgan Smith <[email protected]> Date: Sat, 14 Mar 2026 16:21:05 -0400 Subject: [PATCH 10/10] test-org-clock/org-clock-timestamps-change: Add more test coverage * testing/lisp/test-org-clock.el (test-org-clock/org-clock-timestamps-change): Add more test coverage inspired by the test `test-org/org-timestamp-change'. --- testing/lisp/test-org-clock.el | 45 +++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/testing/lisp/test-org-clock.el b/testing/lisp/test-org-clock.el index 9554ac4ec..4d5cb055e 100644 --- a/testing/lisp/test-org-clock.el +++ b/testing/lisp/test-org-clock.el @@ -120,7 +120,50 @@ test-org-clock/org-clock-timestamps-change (org-test-with-temp-text "CLOCK: [2023-02-19 Sun 2<point>3:30]--[2023-02-20 Mon 00:35] => 1:05" (org-clock-timestamps-change 'up 1) - (buffer-string)))))) + (buffer-string))))) + (let ((org-time-stamp-rounding-minutes '(1 1)) ;; No rounding! + (now (decode-time)) test-text point) + ;; Decrementing a month from March 31st yields February + ;; 28th. This particular test is easier to write if the + ;; days don't change when modifying the month. + (setf (decoded-time-day now) + (min (decoded-time-day now) 27)) + (setq now (encode-time 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))))) + (setq test-text + (concat "CLOCK: [" + (format-time-string (cdr org-timestamp-formats) now) + "]--[" + (format-time-string (cdr org-timestamp-formats) (time-add now (* 60 60))) + "] => 1:00")) + (org-test-with-temp-text test-text + (while (not (eolp)) + ;; 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-clock-timestamps-down) + (let ((current-prefix-arg '(2))) + ;; We supply the prefix argument 2 to increment the + ;; minutes by 2. We supply the function argument 2 for + ;; everything else. Is this a bug? Probably. FIXME. + (org-clock-timestamps-up 2)) + (org-clock-timestamps-down) + (goto-char point) + (should (string= + (buffer-substring (point-min) (point-max)) + test-text)) + (forward-char 1)))))) (ert-deftest test-org-clock/org-clock-update-time-maybe () "Test `org-clock-update-time-maybe' specifications." -- 2.52.0
