Hey everyone,

With a large number of tasks / checklist items (N > 100), the percent
completion statistic can show 0% even if some of the tasks are
complete, e.g. 1/101

- [0%]
  1. [X]
  2. [ ]
  3. [ ]
  --8<--
  101. [ ]

Personally, I find this misleading and I’d rather it show 1% (which
would make it symmetric with 200/201 showing 99% instead of 100%).
With the attached patch, small percentages 0% < p < 1% are rounded up
to 1% instead of down to 0%:

  (let* ((p (floor (* 100.0 completed) (max 1 total)))
         (p (if (and (= p 0) (> completed 0)) 1 p)))
         (format "[%d%]" p))

I’ve also added some tests.  Any thoughts on this behaviour?

-- 
Jacob S. Gordon
[email protected]
Please avoid sending me HTML emails and MS Office documents.
https://useplaintext.email/#etiquette
From d7a2ec74f0e8772398c1f937347c87e43c8a2086 Mon Sep 17 00:00:00 2001
From: "Jacob S. Gordon" <[email protected]>
Date: Wed, 7 Jan 2026 04:20:00 -0500
Subject: [RFC PATCH] Ensure a nonzero percent cookie when anything is complete

With a large number of tasks (N > 100) the percentage 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-list.el (org-update-checkbox-count): Adjust checkbox
completion percent calculation.
* lisp/org.el (org-update-parent-todo-statistics): Adjust todo
completion percent calculation.
* 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.
---
 lisp/org-list.el              |  6 ++--
 lisp/org.el                   |  7 +++--
 testing/lisp/test-org-list.el | 20 +++++++++++-
 testing/lisp/test-org.el      | 57 +++++++++++++++++++++++++++++++++++
 4 files changed, 84 insertions(+), 6 deletions(-)

diff --git a/lisp/org-list.el b/lisp/org-list.el
index 9b3394402..9ff58c75a 100644
--- a/lisp/org-list.el
+++ b/lisp/org-list.el
@@ -2656,8 +2656,10 @@ (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
+		   (let ((p (floor (* 100.0 checked) (max 1 total))))
+		     (format "[%d%%]" (if (and (= p 0) (> checked 0))
+					  1 p)))
 	         (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.el b/lisp/org.el
index 7b455a18a..69bc0c028 100644
--- a/lisp/org.el
+++ b/lisp/org.el
@@ -10143,9 +10143,10 @@ (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))
+			(let ((p (floor (* 100.0 cnt-done) (max 1 cnt-all))))
+			  (format "[%d%%]" (if (and (= p 0) (> cnt-done 0))
+					       1 p)))
+		      (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 530584e8d..7d2ebfb80 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: 1328e518db2e5876c8768fb4c74769d5b518984a
-- 
Jacob S. Gordon
[email protected]
Please avoid sending me HTML emails and MS Office documents.
https://useplaintext.email/#etiquette

Reply via email to