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/#etiquetteFrom 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