On 2026-01-12 15:14, Ihor Radchenko wrote:
> I think it should better go into org-macs.el.
> For now, there is compiler warning because there is no (require
> 'org) in org-list.el. And org-list.el cannot (require 'org) because
> org.el requires org-list.

Oops, fixed in v3, I’ll pay closer attention to the warnings!  IMO
it’s better if there’s zero so that anything new sticks out.  FWIW
I’ve also attached a patch that renames a node in the Org guide to
avoid a Texinfo warning:

  @node name should not contain `,': Capture, Refile, Archive

Thanks,

-- 
Jacob S. Gordon
[email protected]
Please don’t send me HTML emails or MS Office/Apple iWork documents.
https://useplaintext.email/#etiquette
https://www.fsf.org/campaigns/opendocument
From f8d0793f37778f9c642fb96429757d11287e5dc6 Mon Sep 17 00:00:00 2001
From: "Jacob S. Gordon" <[email protected]>
Date: Mon, 12 Jan 2026 16:20:00 -0500
Subject: [PATCH v3 1/2] Ensure a non-zero percent cookie when anything is
 complete

With a large number of tasks (N > 100) the percent cookie can show 0%
when a non-zero amount have been completed, e.g. 1/101.  In those
cases adjust the percent to show 1% instead.

* lisp/org-macs.el (org-format-percent-cookie): Add function.
* lisp/org.el (org-update-parent-todo-statistics):
* lisp/org-list.el (org-update-checkbox-count):
* lisp/org-colview.el (org-columns--summary-checkbox-percent): Use
function.
* testing/lisp/test-org-list.el (test-org-list/update-checkbox-count):
* testing/lisp/test-org.el (test-org/update-todo-statistics-cookies):
Add tests.
* etc/ORG-NEWS (Important announcements and breaking changes):
Announce changes.
(New functions and changes in function arguments): Announce function.
---
 etc/ORG-NEWS                  | 12 ++++++++
 lisp/org-colview.el           |  7 ++---
 lisp/org-list.el              |  3 +-
 lisp/org-macs.el              |  7 +++++
 lisp/org.el                   |  5 ++-
 testing/lisp/test-org-list.el | 20 +++++++++++-
 testing/lisp/test-org.el      | 57 +++++++++++++++++++++++++++++++++++
 7 files changed, 101 insertions(+), 10 deletions(-)

diff --git a/etc/ORG-NEWS b/etc/ORG-NEWS
index 2e1f79f8c..9443548f1 100644
--- a/etc/ORG-NEWS
+++ b/etc/ORG-NEWS
@@ -167,6 +167,13 @@ The =show= parameter for the =org-priority= function was deprecated in
 Org 9.2 (released in 2017). Sufficient time has passed and it is being
 removed as part of refactoring for numeric priorities.
 
+*** The percent cookie is now non-zero when any sub-task is complete
+
+With a large number of tasks (N > 100) the percent cookie previously
+showed =0%= when a non-zero amount have been completed, e.g. 1/101.
+Those cases now show =1%= instead.  The same is true for the checkbox
+status summary type =X%= when defining columns for column view.
+
 ** New features
 
 # We list the most important features, and the features that may
@@ -622,6 +629,11 @@ For performance, if the invisibility spec has been constructed, it can
 be passed in as ~invisibility-spec~ instead of having it be
 constructed again.
 
+*** New function ~org-format-percent-cookie~
+
+Given the completed and total number of tasks, format the percent
+cookie =[N%]=.
+
 ** Removed or renamed functions and variables
 
 *** ~org-edit-src-content-indentation~ is renamed to ~org-src-content-indentation~
diff --git a/lisp/org-colview.el b/lisp/org-colview.el
index c295cea1d..4c65f061f 100644
--- a/lisp/org-colview.el
+++ b/lisp/org-colview.el
@@ -1393,10 +1393,9 @@ (defun org-columns--summary-checkbox-count (check-boxes _)
 
 (defun org-columns--summary-checkbox-percent (check-boxes _)
   "Summarize CHECK-BOXES with a check-box percent."
-  (format "[%d%%]"
-	  (round (* 100.0 (cl-count-if (lambda (b) (member b '("[X]" "[100%]")))
-				       check-boxes))
-		 (length check-boxes))))
+  (org-format-percent-cookie (cl-count-if (lambda (b) (member b '("[X]" "[100%]")))
+                                          check-boxes)
+                             (length check-boxes)))
 
 (defun org-columns--summary-min (values fmt)
   "Compute the minimum of VALUES.
diff --git a/lisp/org-list.el b/lisp/org-list.el
index 9b3394402..70e7f5a1f 100644
--- a/lisp/org-list.el
+++ b/lisp/org-list.el
@@ -2656,8 +2656,7 @@ (defun org-update-checkbox-count (&optional all)
 	    (goto-char beg)
             (org-fold-core-ignore-modifications
 	      (insert-and-inherit
-	       (if percent (format "[%d%%]" (floor (* 100.0 checked)
-					           (max 1 total)))
+	       (if percent (org-format-percent-cookie checked total)
 	         (format "[%d/%d]" checked total)))
 	      (delete-region (point) (+ (point) (- end beg))))
 	    (when org-auto-align-tags (org-fix-tags-on-the-fly))))))))
diff --git a/lisp/org-macs.el b/lisp/org-macs.el
index c5335ab09..2a14e1e8c 100644
--- a/lisp/org-macs.el
+++ b/lisp/org-macs.el
@@ -1862,6 +1862,13 @@ (defun org-base-buffer-file-name (&optional buffer)
       (buffer-file-name base-buffer)
     (buffer-file-name buffer)))
 
+(defun org-format-percent-cookie (completed total)
+  "Format the percent cookie `[N%]' for COMPLETED/TOTAL tasks.
+The percent is rounded down to the nearest integer, except for small
+percentages 0% < p < 1% which are rounded up to 1%."
+  (let ((p (floor (* 100.0 completed) (max 1 total))))
+    (format "[%d%%]" (if (and (= p 0) (> completed 0)) 1 p))))
+
 (provide 'org-macs)
 
 ;; Local variables:
diff --git a/lisp/org.el b/lisp/org.el
index a8c7ef9d2..9e24e0f68 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -10143,9 +10143,8 @@ (defun org-update-parent-todo-statistics ()
                   (outline-next-heading)))
 	      (setq new
                     (if is-percent
-                        (format "[%d%%]" (floor (* 100.0 cnt-done)
-					        (max 1 cnt-all)))
-                      (format "[%d/%d]" cnt-done cnt-all))
+                        (org-format-percent-cookie cnt-done cnt-all)
+		      (format "[%d/%d]" cnt-done cnt-all))
                     ndel (- (match-end 0) checkbox-beg))
               (goto-char (match-end 0))
               (unless (string-equal new (buffer-substring checkbox-beg (match-end 0)))
diff --git a/testing/lisp/test-org-list.el b/testing/lisp/test-org-list.el
index a3a526ba6..5f069dee0 100644
--- a/testing/lisp/test-org-list.el
+++ b/testing/lisp/test-org-list.el
@@ -1206,7 +1206,25 @@ (ert-deftest test-org-list/update-checkbox-count ()
   - [X] item2"
 		   (let ((org-checkbox-hierarchical-statistics nil))
 		     (org-update-checkbox-count))
-		   (buffer-string)))))
+		   (buffer-string))))
+  (let ((checklist (concat "- [%]\n"          ; 0/101 = 0%
+                           (mapconcat #'identity
+                                      (make-list 101 "  - [ ]") "\n"))))
+    (should (string-match "\\[0%\\]" (org-test-with-temp-text checklist
+                                       (org-update-checkbox-count)
+                                       (buffer-string)))))
+  (let ((checklist (concat "- [%]\n  - [X]\n" ; 1/101 = 0.99% -> 1%
+                           (mapconcat #'identity
+                                      (make-list 100 "  - [ ]") "\n"))))
+    (should (string-match "\\[1%\\]" (org-test-with-temp-text checklist
+                                       (org-update-checkbox-count)
+                                       (buffer-string)))))
+  (let ((checklist (concat "- [%]\n  - [ ]\n" ; 200/201 = 99.5% -> 99%
+                           (mapconcat #'identity
+                                      (make-list 200 "  - [X]") "\n"))))
+    (should (string-match "\\[99%\\]" (org-test-with-temp-text checklist
+                                        (org-update-checkbox-count)
+                                        (buffer-string))))))
 
 
 ;;; API
diff --git a/testing/lisp/test-org.el b/testing/lisp/test-org.el
index 7205ab948..dc38d633d 100644
--- a/testing/lisp/test-org.el
+++ b/testing/lisp/test-org.el
@@ -4224,6 +4224,63 @@ (ert-deftest test-org/org-ctrl-c-ctrl-c ()
       (org-ctrl-c-ctrl-c))
     (should-not org-columns-overlays)))
 
+(ert-deftest test-org/update-todo-statistics-cookies ()
+  "Test updating TODO statistics cookies."
+  (let ((N 3)
+        (parent "* [/]"))
+    (dolist (n (number-sequence 0 N))
+      (let* ((match (format "\\[%d/%d\\]" n N))
+             (done (mapconcat #'(lambda (n) (format "** DONE D%d" n))
+                              (number-sequence 1 n) "\n"))
+             (todo (mapconcat #'(lambda (n) (format "** TODO T%d" n))
+                              (number-sequence 1 (- N n)) "\n"))
+             (tree (concat parent "\n" done "\n" todo)))
+        (should (string-match match
+                              (org-test-with-temp-text tree
+                                (org-update-statistics-cookies t)
+                                (buffer-string)))))))
+  (let ((N 3)
+        (pvals '(0 33 66 100))
+        (parent "* [%]"))
+    (dolist (n (number-sequence 0 N))
+      (let* ((match (format "\\[%d%%\\]" (elt pvals n)))
+             (done (mapconcat #'(lambda (n) (format "** DONE D%d" n))
+                              (number-sequence 1 n) "\n"))
+             (todo (mapconcat #'(lambda (n) (format "** TODO T%d" n))
+                              (number-sequence 1 (- N n)) "\n"))
+             (tree (concat parent "\n" done "\n" todo)))
+        (should (string-match match
+                              (org-test-with-temp-text tree
+                                (org-update-statistics-cookies t)
+                                (buffer-string)))))))
+  (let ((N 101)
+        (parent "* [%]"))
+    (let ((match "\\[0%\\]")            ; 0/101 -> 0%
+          (tree (concat parent "\n"
+                        (mapconcat #'(lambda (n) (format "** TODO T%d" n))
+                                   (number-sequence 1 N) "\n"))))
+      (should (string-match match
+                            (org-test-with-temp-text tree
+                              (org-update-statistics-cookies t)
+                              (buffer-string)))))
+    (let ((match "\\[1%\\]")            ; 1/101 -> 0.99% -> 1%
+          (tree (concat parent "\n** DONE D1\n"
+                        (mapconcat #'(lambda (n) (format "** TODO T%d" n))
+                                   (number-sequence 1 (1- N)) "\n"))))
+      (should (string-match match
+                            (org-test-with-temp-text tree
+                              (org-update-statistics-cookies t)
+                              (buffer-string))))))
+  (let ((N 201)
+        (parent "* [%]"))
+    (let ((match "\\[99%\\]")           ; 200/201 -> 99.5% -> 99%
+          (tree (concat parent "\n** TODO T1\n"
+                        (mapconcat #'(lambda (n) (format "** DONE D%d" n))
+                                   (number-sequence 1 N) "\n"))))
+      (should (string-match match
+                            (org-test-with-temp-text tree
+                              (org-update-statistics-cookies t)
+                              (buffer-string)))))))
 
 ;;; Navigation
 

base-commit: c83fbea516eeed2f9b4e7f91c2b8f4abd083c118
-- 
Jacob S. Gordon
[email protected]
Please don’t send me HTML emails or MS Office/Apple iWork documents.
https://useplaintext.email/#etiquette
https://www.fsf.org/campaigns/opendocument

From 0f6af32ec8247ab654a0a17454bc5f7272c24d1a Mon Sep 17 00:00:00 2001
From: "Jacob S. Gordon" <[email protected]>
Date: Mon, 12 Jan 2026 16:21:00 -0500
Subject: [PATCH v3 2/2] ; org-guide: Rename section to avoid Texinfo warning

* doc/org-guide.org (Capture / Refile / Archive): Use slashes instead
of commas.
---
 doc/org-guide.org | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/doc/org-guide.org b/doc/org-guide.org
index ae8fe602d..bf0e0a5ef 100644
--- a/doc/org-guide.org
+++ b/doc/org-guide.org
@@ -1327,7 +1327,7 @@ ** Clocking Work Time
 Agenda]]) to show which tasks have been worked on or closed during
 a day.
 
-* Capture, Refile, Archive
+* Capture / Refile / Archive
 :PROPERTIES:
 :DESCRIPTION: The ins and outs for projects.
 :END:
-- 
Jacob S. Gordon
[email protected]
Please don’t send me HTML emails or MS Office/Apple iWork documents.
https://useplaintext.email/#etiquette
https://www.fsf.org/campaigns/opendocument

Reply via email to