Subject: bug#78678: [PATCH v2] world-clock: Add user option to control order of entries

From: "Jacob S. Gordon" <[email-masked]>
Date: Sun, 8 Jun 2025 23:54:06 -0400

Hello,

On 2025-06-03 08:00, Eli Zaretskii wrote:
> This requires users to be Lisp programmers.  If there are
> reasonably-useful cases where a certain order would be desirable,
> allowing users to customize a variable using symbol values makes the
> feature much more friendly to users who are not programmers.  Would
> it make sense to add a couple of such values?

I agree, and I think it does make sense. After trying a version with
symbols for common use-cases, I think the most flexible option is to
allow the user to provide a format for `format-time-string' to act as
the sorting key. This is similar in complexity to the variable
`world-clock-time-format', and some predefined values can be put in
the `:type' block of the `defcustom':

#+begin_src emacs-lisp
:type '(choice (const :tag "No sorting" nil)
               (const :tag "Chronological order" "%FT%T")
               (const :tag "Reverse chronological order" ("%FT%T" . t))
               (const :tag "Time of day order" "%T")
               (const :tag "Reverse time of day order" ("%T" . t))
               (string :tag "Format string")
               (cons :tag "Format string and reverse flag"
                     (string :tag "Format string")
                     (boolean :tag "Reverse"))
               (function :tag "Sorting function")))
#+end_src

Then, sorting can be switched based on the type in a helper function:

#+begin_quote
(defun world-clock--sort-entries (tzlist time)
  "Sort TZLIST according to `world-clock-sort-order' at a given TIME."
  (pcase world-clock-sort-order
    ((pred null) tzlist)
    ((or (and (pred stringp) format)
        `(,(and (pred stringp) format) .
          ,(and (pred booleanp) reverse)))
     (sort tzlist
           :key (lambda (entry)
                   (format-time-string format time (car entry)))
           :reverse reverse))
    ((pred functionp) (funcall world-clock-sort-order tzlist time))
    (_ (error "Invalid `world-clock-sort-order': `%s'"
              world-clock-sort-order))))
#+end_quote

In terms of coding standards:

+ I could do this without `pcase', if desired.
+ I can write unit tests if we’re okay with the idea.

> Please reformat the log message according to our conventions
> (described in CONTRIBUTE).

Done in the updated patch (I think?).

Thank you!

-- 
Jacob S. Gordon
[email-masked]

======================

Please avoid sending me HTML emails and MS Office documents.
https://useplaintext.email/#etiquette
https://www.gnu.org/philosophy/no-word-attachments.html

From db68de75d0f07727f3cd3390a73c3a9dc6b4d2ab Mon Sep 17 00:00:00 2001
From: "Jacob S. Gordon" <[email-masked]>
Date: Mon, 2 Jun 2025 21:06:37 -0400
Subject: [PATCH v2] world-clock: Add user option to control order of entries

* lisp/time.el (world-clock-sort-order): Add user option.
(world-clock--sort-entries): Add auxiliary sorting function.
(world-clock-display): Sort entries based on the new user option.
---
 etc/NEWS     | 12 +++++++++++
 lisp/time.el | 61 ++++++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 66 insertions(+), 7 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index 1f29fe5f0b4..3c43f01fc49 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2140,6 +2140,18 @@ is bound to 'C-l' in the calendar buffer.
 You can now use the mouse wheel to scroll the calendar by 3 months.
 With the shift modifier, it scrolls by one month.  With the meta
 modifier, it scrolls by year.
+
+** Time
+
+*** New user option 'world-clock-sort-order'.
+This option controls the order of timezone entries in the 'world-clock'.
+By default, no sorting is done, and entries appear in the same order as
+in 'world-clock-list'.  Any format understood by 'format-time-string'
+can be used to specify a key for the sort order, which is updated upon
+each refresh.  The sort direction can be controlled by using a cons cell
+of a format string and a boolean.  Alternatively, a sorting function can
+be provided directly.
+
 
 * New Modes and Packages in Emacs 31.1
 
diff --git a/lisp/time.el b/lisp/time.el
index fd66775c879..7f2f276be6d 100644
--- a/lisp/time.el
+++ b/lisp/time.el
@@ -536,6 +536,36 @@ world-clock-timer-second
   :type 'natnum
   :version "28.1")
 
+(defcustom world-clock-sort-order nil
+  "Sort order of entries in the `world-clock'.
+This variable can take a few different forms:
+
+- A nil value indicates no sorting, and the displayed order is the same
+  as in `world-clock-list'.  This is the default.
+
+- A string is taken as a format for `format-time-string', whose output
+  is used as the sorting key.  For example, an ISO 8601 format `\"%FT%T\"'
+  means entries are sorted in chronological order.
+
+- A cons cell of the form (STRING . BOOL) again provides a format for
+  `format-time-string'.  The boolean part controls the sort direction.
+  For example, `(\"%FT%T\" .  t)' means entries are sorted in reverse
+  chronological order.
+
+- A function that takes a list of (TIMEZONE LABEL) entries and a TIME as
+  arguments, and returns another list of entries."
+  :version "31.1"
+  :type '(choice (const :tag "No sorting" nil)
+		 (const :tag "Chronological order" "%FT%T")
+		 (const :tag "Reverse chronological order" ("%FT%T" . t))
+		 (const :tag "Time of day order" "%T")
+		 (const :tag "Reverse time of day order" ("%T" . t))
+		 (string :tag "Format string")
+		 (cons :tag "Format string and reverse flag"
+		       (string :tag "Format string")
+		       (boolean :tag "Reverse"))
+		 (function :tag "Sorting function")))
+
 (defface world-clock-label
   '((t :inherit font-lock-variable-name-face))
   "Face for time zone label in `world-clock' buffer.")
@@ -562,15 +592,32 @@ world-clock-mode
 (defvar world-clock--timer nil
   "The current world clock timer.")
 
+(defun world-clock--sort-entries (tzlist time)
+  "Sort TZLIST according to `world-clock-sort-order' at a given TIME."
+  (pcase world-clock-sort-order
+    ((pred null) tzlist)
+    ((or (and (pred stringp) format)
+	 `(,(and (pred stringp) format) .
+	   ,(and (pred booleanp) reverse)))
+     (sort tzlist
+	   :key (lambda (entry)
+		  (format-time-string format time (car entry)))
+	   :reverse reverse))
+    ((pred functionp) (funcall world-clock-sort-order tzlist time))
+    (_ (error "Invalid `world-clock-sort-order': `%s'"
+	      world-clock-sort-order))))
+
 (defun world-clock-display (alist)
-  "Replace current buffer text with times in various zones, based on ALIST."
-  (let ((inhibit-read-only t)
-	(buffer-undo-list t)
-	(now (current-time))
-	(max-width 0)
-	result fmt)
+  "Replace current buffer text with times in various zones, based on ALIST.
+The entries are ordered according to `world-clock-sort-order'."
+  (let* ((inhibit-read-only t)
+	 (buffer-undo-list t)
+	 (now (current-time))
+	 (zones (world-clock--sort-entries alist now))
+	 (max-width 0)
+	 result fmt)
     (erase-buffer)
-    (dolist (zone alist)
+    (dolist (zone zones)
       (let* ((label (cadr zone))
 	     (width (string-width label)))
 	(push (cons label

Range-diff against v1:
1:  0d293d92d81 ! 1:  db68de75d0f world-clock: Sort entries based on a user-provided function
    @@ Metadata
     Author: Jacob S. Gordon <[email-masked]>
     
      ## Commit message ##
    -    world-clock: Sort entries based on a user-provided function
    +    world-clock: Add user option to control order of entries
     
    -    The 'world-clock' displays time zone entries in the order they appear in
    -    'world-clock-list' (or a default if unset).  We introduce a custom
    -    variable 'world-clock-sort-function' to generate an order on each update
    -    of 'world-clock-display'.  The default value of 'identity' preserves the
    -    existing behavior.
    -
    -    * lisp/time.el (world-clock-sort-function, world-clock-display):
    -
    -    Add custom variable 'world-clock-sort-function', and sort time zone
    -    entries in 'world-clock-display' according to it.
    +    * lisp/time.el (world-clock-sort-order): Add user option.
    +    (world-clock--sort-entries): Add auxiliary sorting function.
    +    (world-clock-display): Sort entries based on the new user option.
     
      ## etc/NEWS ##
    -@@ etc/NEWS: You can now use the mouse wheel to scroll the calendar by 3 months.
    +@@ etc/NEWS: is bound to 'C-l' in the calendar buffer.
    + You can now use the mouse wheel to scroll the calendar by 3 months.
      With the shift modifier, it scrolls by one month.  With the meta
      modifier, it scrolls by year.
    - 
     +
     +** Time
     +
    -+*** New user option 'world-clock-sort-function'
    -+
    -+The order of time zone entries in the 'world-clock' is updated according
    -+to the 'world-clock-sort-function'.  By default, no sorting is done and
    -+entries appear according to their order in 'world-clock-list' (or a
    -+default if unset).
    ++*** New user option 'world-clock-sort-order'.
    ++This option controls the order of timezone entries in the 'world-clock'.
    ++By default, no sorting is done, and entries appear in the same order as
    ++in 'world-clock-list'.  Any format understood by 'format-time-string'
    ++can be used to specify a key for the sort order, which is updated upon
    ++each refresh.  The sort direction can be controlled by using a cons cell
    ++of a format string and a boolean.  Alternatively, a sorting function can
    ++be provided directly.
     +
    + 
      * New Modes and Packages in Emacs 31.1
      
    - ** New minor mode 'delete-trailing-whitespace-mode'.
     
      ## lisp/time.el ##
     @@ lisp/time.el: world-clock-timer-second
        :type 'natnum
        :version "28.1")
      
    -+(defcustom world-clock-sort-function 'identity
    -+  "Function to sort time zone entries upon each `world-clock-display'.
    -+It should take a list of `(TIMEZONE LABEL)' entries and return the same.
    ++(defcustom world-clock-sort-order nil
    ++  "Sort order of entries in the `world-clock'.
    ++This variable can take a few different forms:
     +
    -+This function will be called with `world-clock-list' if set, otherwise
    -+with a default."
    ++- A nil value indicates no sorting, and the displayed order is the same
    ++  as in `world-clock-list'.  This is the default.
    ++
    ++- A string is taken as a format for `format-time-string', whose output
    ++  is used as the sorting key.  For example, an ISO 8601 format `\"%FT%T\"'
    ++  means entries are sorted in chronological order.
    ++
    ++- A cons cell of the form (STRING . BOOL) again provides a format for
    ++  `format-time-string'.  The boolean part controls the sort direction.
    ++  For example, `(\"%FT%T\" .  t)' means entries are sorted in reverse
    ++  chronological order.
    ++
    ++- A function that takes a list of (TIMEZONE LABEL) entries and a TIME as
    ++  arguments, and returns another list of entries."
     +  :version "31.1"
    -+  :type '(function :tag "Sorting function"))
    ++  :type '(choice (const :tag "No sorting" nil)
    ++		 (const :tag "Chronological order" "%FT%T")
    ++		 (const :tag "Reverse chronological order" ("%FT%T" . t))
    ++		 (const :tag "Time of day order" "%T")
    ++		 (const :tag "Reverse time of day order" ("%T" . t))
    ++		 (string :tag "Format string")
    ++		 (cons :tag "Format string and reverse flag"
    ++		       (string :tag "Format string")
    ++		       (boolean :tag "Reverse"))
    ++		 (function :tag "Sorting function")))
     +
      (defface world-clock-label
        '((t :inherit font-lock-variable-name-face))
        "Face for time zone label in `world-clock' buffer.")
    -@@ lisp/time.el: world-clock--timer
    +@@ lisp/time.el: world-clock-mode
    + (defvar world-clock--timer nil
        "The current world clock timer.")
      
    ++(defun world-clock--sort-entries (tzlist time)
    ++  "Sort TZLIST according to `world-clock-sort-order' at a given TIME."
    ++  (pcase world-clock-sort-order
    ++    ((pred null) tzlist)
    ++    ((or (and (pred stringp) format)
    ++	 `(,(and (pred stringp) format) .
    ++	   ,(and (pred booleanp) reverse)))
    ++     (sort tzlist
    ++	   :key (lambda (entry)
    ++		  (format-time-string format time (car entry)))
    ++	   :reverse reverse))
    ++    ((pred functionp) (funcall world-clock-sort-order tzlist time))
    ++    (_ (error "Invalid `world-clock-sort-order': `%s'"
    ++	      world-clock-sort-order))))
    ++
      (defun world-clock-display (alist)
     -  "Replace current buffer text with times in various zones, based on ALIST."
    +-  (let ((inhibit-read-only t)
    +-	(buffer-undo-list t)
    +-	(now (current-time))
    +-	(max-width 0)
    +-	result fmt)
     +  "Replace current buffer text with times in various zones, based on ALIST.
    -+The displayed entries are sorted according to `world-clock-sort-function'."
    -   (let ((inhibit-read-only t)
    - 	(buffer-undo-list t)
    - 	(now (current-time))
    -+	(sorted-zones (funcall world-clock-sort-function alist))
    - 	(max-width 0)
    - 	result fmt)
    ++The entries are ordered according to `world-clock-sort-order'."
    ++  (let* ((inhibit-read-only t)
    ++	 (buffer-undo-list t)
    ++	 (now (current-time))
    ++	 (zones (world-clock--sort-entries alist now))
    ++	 (max-width 0)
    ++	 result fmt)
          (erase-buffer)
     -    (dolist (zone alist)
    -+    (dolist (zone sorted-zones)
    ++    (dolist (zone zones)
            (let* ((label (cadr zone))
      	     (width (string-width label)))
      	(push (cons label

base-commit: 86eb733aef49fc91b6f2cf4ecb384c05e899ffa5
-- 
Jacob S. Gordon
[email-masked]

=========================

Please avoid sending me HTML emails and MS Office documents.
https://useplaintext.email/#etiquette
https://www.gnu.org/philosophy/no-word-attachments.html