I ended up going down a couple of paths before settling on a general
refactor of the code to introduce some helper functions and more unit
tests. There are still several things I need to fix, but I've attached the
patches related to `org-priority' at least. If this looks reasonable I'll
start working on the other parts of the codebase that deal with priorities,
using the same helper functions.

Thanks,

Derek

On Mon, Jul 21, 2025 at 10:53 AM Ihor Radchenko <yanta...@posteo.net> wrote:

> Derek Chen-Becker <de...@chen-becker.org> writes:
>
> > Sounds good. I'm happy to update the documentation, the regexp, and
> > other mentioned locations to match one of A-Z or 0-64. Are we OK
> > restricting it to uppercase only?
>
> Yes, let's go for it.
>
> --
> Ihor Radchenko // yantar92,
> Org mode maintainer,
> Learn more about Org mode at <https://orgmode.org/>.
> Support Org development at <https://liberapay.com/org-mode>,
> or support my work at <https://liberapay.com/yantar92>
>


-- 
+---------------------------------------------------------------+
| Derek Chen-Becker                                             |
| 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 c256a75956c3ebf374b9e11366af926b56dbdb05 Mon Sep 17 00:00:00 2001
From: Derek Chen-Becker <o...@chen-becker.org>
Date: Sat, 6 Sep 2025 07:07:34 -0600
Subject: [PATCH 2/2] lisp/org.el: Add proper support for numeric priorities

* doc/org-manual.org: Update the manual to clarify the allowed values for
priorities.

* lisp/org.el (org-priority-regexp): Update the priority cookie regular
expression to fully validate numeric values, and update the documentation
to match.
(org-priority-valid-cookiep, org-priority-valid-valuep,
org-priority-value-in-rangep, org-priority-numberp): Add validation
predicates to test priority cookies and priority values.
(org-priority-to-string): Add a function to provide a consistent string
value from a given priority value.
(org-priority): Refactor to apply more validation and use the new
predicates and functions defined in this commit to simplify logic and make
things more consistent, particularly around double-digit numeric values.

* testing/lisp/test-org.el: Add unit tests for the new predicats and format
functions related to priority.
---
 doc/org-manual.org       |   8 +-
 lisp/org.el              | 165 +++++++++++++++++++++++++++------------
 testing/lisp/test-org.el |  66 +++++++++++++++-
 3 files changed, 185 insertions(+), 54 deletions(-)

diff --git a/doc/org-manual.org b/doc/org-manual.org
index 8c6962d4b..26a5cbd57 100644
--- a/doc/org-manual.org
+++ b/doc/org-manual.org
@@ -4626,7 +4626,7 @@ You can also use numeric values for priorities, such as
 
 When using numeric priorities, you need to set ~org-priority-highest~,
 ~org-priority-lowest~ and ~org-priority-default~ to integers, which
-must all be strictly inferior to 65.
+must all be a positive integer between 0 and 64, inclusive.
 
 Priorities can be attached to any heading; they do not need to be
 TODO items.
@@ -4661,8 +4661,10 @@ TODO items.
 #+vindex: org-priority-default
 You can change the range of allowed priorities by setting the
 variables ~org-priority-highest~, ~org-priority-lowest~, and
-~org-priority-default~.  For an individual buffer, you may set these
-values (highest, lowest, default) like this (please make sure that the
+~org-priority-default~.  Valid priority values are single uppercase
+Latin alphabetical characters A-Z, and positive integers in between 0
+and 64, inclusive.  For an individual buffer, you may set these values
+(highest, lowest, default) like this (please make sure that the
 highest priority is earlier in the alphabet than the lowest priority):
 
 #+cindex: @samp{PRIORITIES}, keyword
diff --git a/lisp/org.el b/lisp/org.el
index 602fea261..d2aec1b33 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -11296,14 +11296,66 @@ from the `before-change-functions' in the current buffer."
 
 ;;;; Priorities
 
-(defvar org-priority-regexp ".*?\\(\\[#\\([A-Z0-9]+\\)\\] ?\\)"
+(defvar org-priority-regexp ".*?\\(\\[#\\([A-Z]\\|[0-9]\\|[1-5][0-9]\\|6[0-4]\\)\\] ?\\)"
   "Regular expression matching the priority indicator.
 A priority indicator can be e.g. [#A] or [#1].
+The value of the priority cookie must be a capital latin
+alphabetic character, A-Z, or can be an integer value in
+the range 0-64.
 This regular expression matches these groups:
 0 : the whole match, e.g. \"TODO [#A] Hack\"
 1 : the priority cookie, e.g. \"[#A]\"
 2 : the value of the priority cookie, e.g. \"A\".")
 
+(defun org-priority-valid-cookiep (priority)
+  "Return t if the PRIORITY is a valid priority cookie, nil otherwise."
+  (interactive "P")
+  (cond
+   ((stringp priority)
+    (let ((case-fold-search nil)) ;; Force case-sensitive match
+      ;; (and ... t) to force explicit t/nil
+      (and (string-match-p org-priority-regexp priority) t)))))
+
+(defun org-priority-valid-valuep (priority)
+  "Return t if the PRIORITY is a valid priority value (0-64, A-Z), nil otherwise."
+  (interactive "P")
+  ;; Although we can have either numeric or alphabetic priorities,
+  ;; we simplify here by treating everything as integers because the ASCII
+  ;; alphabetic range also fits in an integer range.
+  (and (integerp priority)
+       (or (and (>= priority 0)
+                (<= priority 64))
+           (and (>= priority ?A)
+                (<= priority ?Z)))))
+
+(defun org-priority-value-in-rangep (priority)
+  "Return t if the PRIORITY is a valid priority value that is in the range of
+   org-priority-lowest >= PRIORITY >= org-priority-highest."
+  (interactive "P")
+  ;; Although we can have either numeric or alphabetic priorities,
+  ;; we simplify here by treating everything as integers because the ASCII
+  ;; alphabetic range also fits in an integer range.
+  (and (org-priority-valid-valuep priority)
+       (<= priority org-priority-lowest)
+       (>= priority org-priority-highest)))
+
+(defun org-priority-numberp (priority)
+  "Return t if the PRIORITY is a number between 0 and 64, inclusive, return
+   nil otherwise."
+  (interactive "P")
+  (and (integerp priority)
+       (>= priority 0)
+       (<= priority 64)))
+
+(defun org-priority-to-string (priority)
+  "Returns a string form of PRIORITY based on whether it's numeric or alphabetic."
+  (interactive "P")
+  (if (org-priority-valid-valuep priority)
+      (if (org-priority-numberp priority)
+          (number-to-string priority)
+        (format "%c" priority))
+    (user-error "Invalid priority value `%s'" priority)))
+
 (defun org-priority-up ()
   "Increase the priority of the current item."
   (interactive)
@@ -11320,57 +11372,72 @@ This regular expression matches these groups:
 When called interactively with a `\\[universal-argument]' prefix,
 show the priority in the minibuffer instead of changing it.
 
-When called programmatically, ACTION can be `set', `up', `down',
-or a character."
+When called programmatically, ACTION can be `set', `up', `down', an
+uppercase alphabetic character (A-Z), or an integer between [0,64],
+inclusive."
   (interactive "P")
   (if (equal action '(4))
       (org-priority-show)
     (unless org-priority-enable-commands
       (user-error "Priority commands are disabled"))
+    ;; validation checks on our current range of priority values
+    (unless (org-priority-valid-valuep org-priority-lowest)
+      (user-error "Invalid org-priority-lowest: %s" org-priority-lowest))
+    (unless (org-priority-valid-valuep org-priority-highest)
+      (user-error "Invalid org-priority-highest: %s" org-priority-highest))
+    (unless (or (and (org-priority-numberp org-priority-highest)
+                     (org-priority-numberp org-priority-lowest))
+                (and (not (org-priority-numberp org-priority-highest))
+                     (not (org-priority-numberp org-priority-lowest))))
+      (user-error "Priority range highest/lowest must both be numeric or both be alphabetic: %s-%s"
+                  org-priority-highest
+                  org-priority-lowest))
+    ;; If action was not provided, default to "set", which will prompt the user
     (setq action (or action 'set))
-    (let ((nump (< org-priority-lowest 65))
-	  current new news have remove)
+    (let ((is_numeric_priority (org-priority-numberp org-priority-lowest))
+	  current_value new_value new_value_string has_existing_cookie remove)
       (save-excursion
 	(org-back-to-heading t)
 	(when (looking-at org-priority-regexp)
 	  (let ((ms (match-string 2)))
-	    (setq current (org-priority-to-value ms)
-		  have t)))
+	    (setq current_value (org-priority-to-value ms)
+		  has_existing_cookie t)))
 	(cond
 	 ((eq action 'remove)
-	  (setq remove t new ?\ ))
+	  (setq remove t new_value ?\ ))
+         ;; set and a value are treated similarly, but only if the
+         ;; value is a valid priority
 	 ((or (eq action 'set)
-	      (integerp action))
+	      (org-priority-valid-valuep action))
 	  (if (not (eq action 'set))
-	      (setq new action)
+	      (setq new_value action)
 	    (setq
-	     new
-	     (if nump
-                 (let* ((msg (format "Priority %s-%s, SPC to remove: "
-                                     (number-to-string org-priority-highest)
-                                     (number-to-string org-priority-lowest)))
-                        (s (if (< 9 org-priority-lowest)
+	     new_value
+             (let* ((msg (format "Priority %s-%s, SPC to remove: "
+                                 (org-priority-to-string org-priority-highest)
+                                 (org-priority-to-string org-priority-lowest)))
+                    (s (if (and is_numeric_priority
+                                (< 9 org-priority-lowest))
                                (read-string msg)
-                             (message msg)
-                             (char-to-string (read-char-exclusive)))))
-                   (if (equal s " ") ?\s (string-to-number s)))
-	       (progn (message "Priority %c-%c, SPC to remove: "
-			       org-priority-highest org-priority-lowest)
-		      (save-match-data
-			(setq new (read-char-exclusive)))))))
-	  (when (and (= (upcase org-priority-highest) org-priority-highest)
-		     (= (upcase org-priority-lowest) org-priority-lowest))
-	    (setq new (upcase new)))
-	  (cond ((equal new ?\s) (setq remove t))
-		((or (< (upcase new) org-priority-highest) (> (upcase new) org-priority-lowest))
-		 (user-error
-		  (if nump
-		      "Priority must be between `%s' and `%s'"
-		    "Priority must be between `%c' and `%c'")
-		  org-priority-highest org-priority-lowest))))
+                             (char-to-string (read-char-exclusive msg)))))
+               (if (equal s " ") ?\s (string-to-number s)))))
+          ;; The user might have given us garbage
+          (unless (org-priority-valid-valuep new_value)
+            (user-error "Invalid priority: `%s'" new_value))
+          ;; If we're using alphabetical priorities, we will force uppercase letters
+	  (when (not is_numeric_priority)
+	    (setq new_value (upcase new_value)))
+          ;; After reading interactive input, set removal flag if needed and
+          ;; perform validation on the new value
+	  (cond
+           ((equal new_value ?\s) (setq remove t))
+	   ((not (org-priority-value-in-rangep new_value))
+	    (user-error "Priority must be between `%s' and `%s'"
+		        (org-priority-to-string org-priority-highest)
+                        (org-priority-to-string org-priority-lowest)))))
 	 ((eq action 'up)
-	  (setq new (if have
-			(1- current)  ; normal cycling
+	  (setq new_value (if has_existing_cookie
+			(1- current_value)  ; normal cycling
 		      ;; last priority was empty
 		      (if (eq last-command this-command)
 			  org-priority-lowest  ; wrap around empty to lowest
@@ -11379,8 +11446,8 @@ or a character."
 			    org-priority-default
 			  (1- org-priority-default))))))
 	 ((eq action 'down)
-	  (setq new (if have
-			(1+ current)  ; normal cycling
+	  (setq new_value (if has_existing_cookie
+			(1+ current_value)  ; normal cycling
 		      ;; last priority was empty
 		      (if (eq last-command this-command)
 			  org-priority-highest  ; wrap around empty to highest
@@ -11389,36 +11456,34 @@ or a character."
 			    org-priority-default
 			  (1+ org-priority-default))))))
 	 (t (user-error "Invalid action")))
-	(when (or (< (upcase new) org-priority-highest)
-		  (> (upcase new) org-priority-lowest))
+        ;; Check if we need to wrap
+	(when (not (org-priority-valid-valuep new_value))
 	  (if (and (memq action '(up down))
-		   (not have) (not (eq last-command this-command)))
-	      ;; `new' is from default priority
+		   (not has_existing_cookie) (not (eq last-command this-command)))
+	      ;; `new_value' is from default priority
 	      (error
 	       "The default can not be set, see `org-priority-default' why")
-	    ;; normal cycling: `new' is beyond highest/lowest priority
+	    ;; normal cycling: `new_value' is beyond highest/lowest priority
 	    ;; and is wrapped around to the empty priority
 	    (setq remove t)))
-	;; Numerical priorities are limited to 64, beyond that number,
-	;; assume the priority cookie is a character.
-	(setq news (if (> new 64) (format "%c" new) (format "%s" new)))
-	(if have
+	(setq new_value_string (org-priority-to-string new_value))
+	(if has_existing_cookie
 	    (if remove
 		(replace-match "" t t nil 1)
-	      (replace-match news t t nil 2))
+	      (replace-match new_value_string t t nil 2))
 	  (if remove
 	      (user-error "No priority cookie found in line")
 	    (let ((case-fold-search nil)) (looking-at org-todo-line-regexp))
 	    (if (match-end 2)
 		(progn
 		  (goto-char (match-end 2))
-		  (insert " [#" news "]"))
+		  (insert " [#" new_value_string "]"))
 	      (goto-char (match-beginning 3))
-	      (insert "[#" news "] "))))
+	      (insert "[#" new_value_string "] "))))
 	(when org-auto-align-tags (org-align-tags)))
       (if remove
 	  (message "Priority removed")
-	(message "Priority of current item set to %s" news)))))
+	(message "Priority of current item set to %s" new_value_string)))))
 
 (defalias 'org-show-priority 'org-priority-show)
 (defun org-priority-show ()
diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el
index 36dea35b7..0ec8fc2ff 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -1,4 +1,4 @@
-;;; test-org.el --- tests for org.el  -*- lexical-binding: t -*-
+    ;;; test-org.el --- tests for org.el  -*- lexical-binding: t -*-
 
 ;; Copyright (c)  David Maus
 ;; Authors: David Maus
@@ -9847,6 +9847,70 @@ two
             (test-org/extract-mathml-math
              (org-create-math-formula "quote\" ; |"))))))
 
+;;; Priority validation and handling
+(ert-deftest test-org/priority-validation ()
+  "Test validation of priority cookies."
+  ;; Simple bounds checks on single alphabetic characters
+  (should
+   (seq-every-p (lambda (p)
+                  (let ((cookie (format "[#%c]" p)))
+                    (org-priority-valid-cookiep cookie)))
+                (number-sequence ?A ?Z)))
+  ;; Test all valid numbers
+  (should
+   (seq-every-p (lambda (p)
+                  (let ((cookie (format "[#%d]" p)))
+                    (org-priority-valid-cookiep cookie)))
+                (number-sequence 0 64)))
+  ;; Invalid characters (not exhaustive)
+  (should
+   (not (org-priority-valid-cookiep "[#$]")))
+  ;; Don't accept lower-case
+  (should
+   (seq-every-p (lambda (p)
+                  (let ((cookie (format "[#%c]" p)))
+                    (not (org-priority-valid-cookiep cookie))))
+                (number-sequence ?a ?z)))
+  ;; Invalid numberic values (< 0 or > 64)
+  (should
+   (not (org-priority-valid-cookiep "[#-1]")))
+  (should
+   (not (org-priority-valid-cookiep "[#65]")))
+  ;; Value tests (as opposed to cookie tests)
+  ;;
+  ;; Numeric, full range
+  (should
+   (let ((org-priority-highest 0)
+         (org-priority-lowest 64))
+     (seq-every-p (lambda (pv) (and (org-priority-valid-valuep pv)
+                                    (org-priority-value-in-rangep pv)))
+                  (number-sequence 0 64))))
+  ;; Numeric, valid value, but out of range
+  (should
+   (let ((org-priority-highest 10)
+         (org-priority-lowest 20))
+     (seq-every-p (lambda (pv)
+                    (and (org-priority-valid-valuep pv)
+                         (not (org-priority-value-in-rangep pv)))
+                    )
+                  '(0 5 9 21 42 64))))
+  ;; ;; Alphabetic, full range
+  (should
+   (let ((org-priority-highest ?A)
+         (org-priority-lowest ?Z))
+     (seq-every-p (lambda (pv) (and (org-priority-valid-valuep pv)
+                                    (org-priority-value-in-rangep pv)))
+                  (number-sequence ?A ?Z))))
+  ;; Alphabetic, valid value, but out of range
+  (should
+   (let ((org-priority-highest ?C)
+         (org-priority-lowest ?K))
+     (seq-every-p (lambda (pv)
+                    (and (org-priority-valid-valuep pv)
+                         (not (org-priority-value-in-rangep pv))))
+                  '(?A ?L ?N ?Z))))
+  )
+
 (provide 'test-org)
 
 ;;; test-org.el ends here
-- 
2.43.0

From 5c6c1dd28b6447d24918412c1568a8317ac38c6c Mon Sep 17 00:00:00 2001
From: Derek Chen-Becker <o...@chen-becker.org>
Date: Tue, 29 Jul 2025 06:19:32 -0600
Subject: [PATCH 1/2] lisp/org.el: Remove deprecated show command

* org.el (org-priority): Remove the deprecated show command now that we're
several versions beyond when the deprecation was introduced.
---
 lisp/org.el | 6 +-----
 1 file changed, 1 insertion(+), 5 deletions(-)

diff --git a/lisp/org.el b/lisp/org.el
index 68ecb95f2..602fea261 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -11314,7 +11314,7 @@ This regular expression matches these groups:
   (interactive)
   (org-priority 'down))
 
-(defun org-priority (&optional action show)
+(defun org-priority (&optional action)
   "Change the priority of an item.
 
 When called interactively with a `\\[universal-argument]' prefix,
@@ -11323,10 +11323,6 @@ show the priority in the minibuffer instead of changing it.
 When called programmatically, ACTION can be `set', `up', `down',
 or a character."
   (interactive "P")
-  (when show
-    ;; Deprecation warning inserted for Org 9.2; once enough time has
-    ;; passed the SHOW argument should be removed.
-    (warn "`org-priority' called with deprecated SHOW argument"))
   (if (equal action '(4))
       (org-priority-show)
     (unless org-priority-enable-commands
-- 
2.43.0

Reply via email to