branch: externals/denote-sequence
commit e413fe12df0d670245ff26fbdfaf3f995c8b2cb1
Merge: f3e85f1cbd df885077d7
Author: Protesilaos Stavrou <[email protected]>
Commit: Protesilaos Stavrou <[email protected]>

    Merge branch 'alphanumeric-delimited-scheme'
---
 README.org                    |  41 +++---
 denote-sequence.el            | 289 +++++++++++++++++++++++++++++++++---------
 tests/denote-sequence-test.el | 184 +++++++++++++++++++++++----
 3 files changed, 418 insertions(+), 96 deletions(-)

diff --git a/README.org b/README.org
index cab0b07df5..5bd0805699 100644
--- a/README.org
+++ b/README.org
@@ -134,23 +134,32 @@ scheme, which is =1a2= for the same sequence 
([[#h:convert-from-one-sequencing-s
 :CUSTOM_ID: h:convert-from-one-sequencing-scheme-to-another
 :END:
 
-The decision on the desired ~denote-sequence-scheme~ wil affect new
-notes long-term 
([[#h:select-a-sequencing-scheme-for-denote-sequence-scheme][Select a 
sequencing scheme for ~denote-sequence-scheme~]]).
-It thus is important to think through your needs and proceed accordingly.
-
 #+findex: denote-sequence-convert
-Still, one cannot be sure which scheme they prefer until they
-experiment with it. It then is inconvenient to manually revert to the
-alternative scheme. To this end, we provide the command
-~denote-sequence-convert~. It convers one or more files from their
-current scheme to its counterpart.
-
-When called from inside a Denote file, it converts that file. When
-called from a Dired buffer, it operates on the marked files. If no
-files are marked, it works with the Dired file at point.
-
-Note that ~denote-sequence-convert~ DOES NOT REPARENT OR ANYHOW CHECK
-THE RESULTING SEQUENCES FOR DUPLICATES 
([[#h:re-parent-a-file-to-extend-a-given-sequence][Re-parent a file to extend a 
given sequence]]).
+The command ~denote-sequence-convert~ makes it easy to switch from one
+~denote-sequence-scheme~ to another 
([[#h:select-a-sequencing-scheme-for-denote-sequence-scheme][Select a 
sequencing scheme for ~denote-sequence-scheme~]]).
+
+This command has a "do what I mean behaviour" with regard to which
+file or files it should operate on:
+
+- When called from inside a file with a Denote sequence, it operates
+  on the current file.
+
+- When called from a Dired buffer, it operates on all the marked
+  files.
+
+- When there are no marked files in the Dired buffer, it operates on
+  the file at point.
+
+The target sequence scheme for the conversion is whatever is assigned
+to the user option ~denote-sequence-scheme~. If, however, the command
+~denote-sequence-convert~ is called with a prefix argument (=C-u= by
+default), then it will prompt for the target sequence scheme. [ This
+optional prompt is part of {{{development-version}}}. ]
+
+[ This command is for users who once used a ~denote-sequence-scheme~
+  and have since decided to switch to another. IT DOES NOT REPARENT OR
+  ANYHOW CHECK THE RESULTING SEQUENCES FOR DUPLICATES: it simply
+  performs the conversion from one scheme to another 
([[#h:re-parent-a-file-to-extend-a-given-sequence][Re-parent a file to extend a 
given sequence]]). ]
 
 ** Create parent, child, or sibling sequence notes
 :PROPERTIES:
diff --git a/denote-sequence.el b/denote-sequence.el
index a2852bb202..7144e9a7d6 100644
--- a/denote-sequence.el
+++ b/denote-sequence.el
@@ -63,9 +63,17 @@
   :link '(url-link :tag "Denote homepage" 
"https://protesilaos.com/emacs/denote";)
   :link '(url-link :tag "Denote Sequence homepage" 
"https://protesilaos.com/emacs/denote-sequence";))
 
+(defconst denote-sequence-schemes '(numeric alphanumeric 
alphanumeric-delimited)
+  "The sequence scheme symbols supported by `denote-sequence-scheme'.")
+
+;; TODO 2026-03-24: The `alphanumeric-delimited' is not supporting partial 
sequences.
+;; This will probably be a problem for `denote-sequence-convert'.
 (defcustom denote-sequence-scheme 'numeric
   "Sequencing scheme to establish file hierarchies.
-The value is the symbol `numeric' or `alphanumeric'.
+The value is a symbol among `numeric', `alphanumeric', and
+`alphanumeric-delimited'.  Users can change the applicable scheme for
+one file or those marked in Dired by calling the command
+`denote-sequence-convert'.
 
 Numeric sequences (the default) are the easier to understand but also
 are the longest.  Each level of depth in the hierarchy is delimited by
@@ -79,10 +87,21 @@ that 1a2 refers to the second child of the first child of 
parent 1.
 Because they alternate between numbers and letters, they do not use the
 equals sign.  When a number cannot be represented by a single letter,
 two or more are used instead, such as the number 51 corresponding to
-zx (z is 26 and x is 25)."
+zx (z is 26 and x is 25).
+
+Alphanumeric delimited sequences combine elements of the aforementioned.
+Levels of depth are expressed as alternating numbers and letters, like
+with the `alphanumeric' scheme, while they also get the = as a separator
+as a visual aid for long sequences.  The sepator is inserted after the
+first level of depth and then after every third level of depth, like
+1=a2b=a1c.  Note that these are levels of depth, not triplets of letters
+and numbers.  As such, 1=zx1zza=1 is valid because zx is one level of
+depth as is zza, as noted above."
   :group 'denote-sequence
+  :package-version '(denote . "0.3.0")
   :type '(choice (const :tag "Numeric like 1=1=2" numeric)
-                 (const :tag "Alphanumeric like 1a2" alphanumeric)))
+                 (const :tag "Alphanumeric like 1a2" alphanumeric)
+                 (const :tag "Alphanumeric delimited like 1=a2b=a1c" 
alphanumeric-delimited)))
 
 (defconst denote-sequence-numeric-regexp "=?[0-9]+"
   "Pattern of a numeric sequence.")
@@ -107,18 +126,113 @@ zx (z is 26 and x is 25)."
              (not (string-match-p "=" sequence)))
     sequence))
 
+(defun denote-sequence--alphanumeric-delimited-split (sequence)
+  "Split SEQUENCE to test for the alphanumeric delimited scheme."
+  (let ((start 0)
+        (strings nil))
+    (while (string-match "[0-9]+\\|[[:alpha:]]+\\|=" sequence start)
+      (push (match-string 0 sequence) strings)
+      (setq start (match-end 0)))
+    (nreverse strings)))
+
+(defun denote-sequence--alphanumeric-delimited-check-alternation 
(split-sequence)
+  "Return non-nil if SPLIT-SEQUENCE alternates between numbers and letters.
+
+SPLIT-SEQUENCE is an alphanumeric delimited sequence that is split into
+separate strings at each level of depth, like this:
+
+    (list \"1\" \"=\" \"a\" \"1\" \"b\" \"=\" \"2\" \"a\" \"1\")"
+  (catch 'error
+    (let ((last-type nil)
+          (current-type nil))
+      (dolist (string split-sequence)
+        (cond
+         ((string-match-p "\\`[0-9]+\\'" string)
+          (setq current-type 'numeric))
+         ((string-match-p "\\`[[:alpha:]]+\\'" string)
+          (setq current-type 'alpha)))
+        (unless (string= "=" string)
+          (when (eq current-type last-type)
+            (throw 'error nil))
+          (setq last-type current-type))))
+    t))
+
+(defun denote-sequence--alphanumeric-delimited-check-depths (split-sequence)
+  "Return non-nil if SPLIT-SEQUENCE is correctly delimited.
+More specifically, return non-nil if there is 1 level of depth before
+the first delimiter and then up to 3 for every subsequent delimiter.
+
+SPLIT-SEQUENCE is an alphanumeric delimited sequence that is split into
+separate strings at each level of depth, like this:
+
+    (list \"1\" \"=\" \"a\" \"1\" \"b\" \"=\" \"2\" \"a\" \"1\")"
+  (let ((levels-of-depth nil)
+        (current-depth 0))
+    (dolist (string split-sequence)
+      (if (string= string "=")
+          (progn
+            (push current-depth levels-of-depth)
+            (setq current-depth 0))
+        (setq current-depth (+ current-depth 1))))
+    (push current-depth levels-of-depth)
+    (setq levels-of-depth (nreverse levels-of-depth))
+    (catch 'error
+      (let ((first-level t))
+        (dolist (level levels-of-depth)
+          (if first-level
+              (progn
+                (setq first-level nil)
+                (unless (= level 1)
+                  (throw 'error nil)))
+            (unless (<= level 3)
+              (throw 'error nil)))))
+      (cond
+       ((and (length> levels-of-depth 2)
+             (= (car levels-of-depth) 1)
+             (seq-every-p
+              (lambda (level)
+                (= level 3))
+              (butlast (cdr levels-of-depth))))
+        levels-of-depth)
+       ((or (length= levels-of-depth 1)
+            (length= levels-of-depth 2))
+        levels-of-depth)
+       (t
+        nil)))))
+
+(defun denote-sequence-alphanumeric-delimited-p (sequence)
+  "Return SEQUENCE if it is an alphanumeric and delimited.
+Refer to the `denote-sequence-scheme' for the details."
+  (cond
+   ((string-match-p "\\`[0-9]+\\'" sequence)
+    sequence)
+   (t
+    (when (and (string-match-p "=" sequence)
+               ;; TODO 2026-03-24: Probably this should not be here
+               ;; due to how we end up with this check.  See, for
+               ;; example, `denote-sequence-p' which already checks
+               ;; for the numeric before it reaches this one.
+               (not (denote-sequence-numeric-p sequence)))
+      (let ((strings (denote-sequence--alphanumeric-delimited-split sequence)))
+        (when (and (denote-sequence--alphanumeric-delimited-check-alternation 
strings)
+                   (denote-sequence--alphanumeric-delimited-check-depths 
strings))
+          sequence))))))
+
 (defun denote-sequence-user-selected-scheme-p (sequence)
   "Return SEQUENCE if it is consistent with `denote-sequence-scheme'.
 Also see `denote-sequence-alphanumeric-p' and `denote-sequence-numeric-p'."
   (pcase denote-sequence-scheme
     ('numeric (denote-sequence-numeric-p sequence))
-    ('alphanumeric (denote-sequence-alphanumeric-p sequence))))
+    ('alphanumeric (denote-sequence-alphanumeric-p sequence))
+    ('alphanumeric-delimited (denote-sequence-alphanumeric-delimited-p 
sequence))
+    (_ (error "The sequence `%s' does not have a known scheme among 
`denote-sequence-schemes'" sequence))))
 
 (defun denote-sequence-p (sequence)
   "Return SEQUENCE string is of a supported scheme.
 Also see `denote-sequence-numeric-p' and `denote-sequence-alphanumeric-p'."
   (when (or (denote-sequence-numeric-p sequence)
-            (denote-sequence-alphanumeric-p sequence))
+            (denote-sequence-alphanumeric-p sequence)
+            (denote-sequence-alphanumeric-delimited-p sequence))
     sequence))
 
 (defun denote-sequence-with-error-p (sequence)
@@ -136,6 +250,11 @@ Also see `denote-sequence-numeric-p' and 
`denote-sequence-alphanumeric-p'."
   (and (string-match-p "[[:alpha:]]+" string)
        (not (string-match-p "[0-9[:punct:]]+" string))))
 
+(defun denote-sequence--alphanumeric-delimited-partial-p (string)
+  "Return non-nil if STRING likely is part of an alphanumeric delimited 
sequence."
+  (or (denote-sequence--numeric-partial-p string)
+      (denote-sequence--alphanumeric-partial-p string)))
+
 (defun denote-sequence-and-scheme-p (sequence &optional partial)
   "Return the sequencing scheme of SEQUENCE, per `denote-sequence-scheme'.
 Return a cons cell of the form (sequence . scheme), where the `car' is
@@ -148,6 +267,8 @@ of numbers or letters.
 
 Produce an error if the sequencing scheme cannot be established."
   (cond
+   ((and (not partial) (string-match-p "\\`[0-9]+\\'" sequence))
+    (cons sequence denote-sequence-scheme))
    ((and (not partial)
          (not (string-match-p "[[:alpha:]]" sequence))
          (eq denote-sequence-scheme 'numeric))
@@ -158,13 +279,22 @@ Produce an error if the sequencing scheme cannot be 
established."
    ((or (and partial (denote-sequence--numeric-partial-p sequence))
         (denote-sequence-numeric-p sequence))
     (cons sequence 'numeric))
+   ((or (and partial (denote-sequence--alphanumeric-delimited-partial-p 
sequence))
+        (denote-sequence-alphanumeric-delimited-p sequence))
+    (cons sequence 'alphanumeric-delimited))
    (t (error "The sequence `%s' does not pass `denote-sequence-and-scheme-p'" 
sequence))))
 
+;; FIXME 2026-03-24: This is technically incorrect because it assumes
+;; homogeneity of sequencing schemes.  But we never enforce as much.
 (defun denote-sequence--scheme-of-strings (strings)
   "Return the sequencing scheme of STRINGS, per `denote-sequence-scheme'."
-  (if (seq-find (lambda (string) (string-match-p "[[:alpha:]]" string)) 
strings)
-      'alphanumeric
-    'numeric))
+  (cond
+   ((seq-every-p #'denote-sequence-numeric-p strings)
+    'numeric)
+   ((seq-every-p #'denote-sequence-alphanumeric-p strings)
+    'alphanumeric)
+   ((seq-every-p #'denote-sequence-alphanumeric-delimited-p strings)
+    'alphanumeric-delimited)))
 
 (defun denote-sequence-file-p (file)
   "Return the sequence if Denote signature of FILE is a sequence.
@@ -178,9 +308,21 @@ SCHEME is a symbol among those mentioned in 
`denote-sequence-scheme'.
 Return resulting sequence if it conforms with `denote-sequence-p'."
   (pcase scheme
     ('numeric (mapconcat #'identity strings "="))
-    ('alphanumeric (apply #'concat strings))))
-
-;; FIXME 2026-03-23: I think this does not actually work with all sort of 
partial sequences.
+    ('alphanumeric (apply #'concat strings))
+    ('alphanumeric-delimited
+     (let* ((result nil)
+            (count 0))
+       (while strings
+         (push (car strings) result)
+         (when (and (or (length= result 1)
+                        (= (% count 3) 0))
+                    (cdr strings))
+           (push "=" result))
+         (setq count (+ count 1))
+         (setq strings (cdr strings)))
+       (string-join (nreverse result))))))
+
+;; FIXME 2026-03-23: I think this does not actually work with all sorts of 
partial sequences.
 (defun denote-sequence-split (sequence &optional partial)
   "Split the SEQUENCE string into a list.
 SEQUENCE conforms with `denote-sequence-p'.  If PARTIAL is non-nil, it
@@ -189,18 +331,19 @@ has the same meaning as in 
`denote-sequence-and-scheme-p'."
     (pcase scheme
       ('numeric
        (split-string sequence "=" t))
-      ('alphanumeric
+      ((or 'alphanumeric 'alphanumeric-delimited)
        (let ((strings nil)
-             (start 0))
-         (while (string-match denote-sequence-alphanumeric-regexp sequence 
start)
-           (push (match-string 1 sequence) strings)
-           (when-let* ((two (match-string 2 sequence)))
+             (start 0)
+             (sequence-no-delimiters (replace-regexp-in-string "=" "" 
sequence)))
+         (while (string-match denote-sequence-alphanumeric-regexp 
sequence-no-delimiters start)
+           (push (match-string 1 sequence-no-delimiters) strings)
+           (when-let* ((two (match-string 2 sequence-no-delimiters)))
              (push two strings)
              (setq start (match-end 2)))
            (setq start (match-end 1)))
          (if strings
              (nreverse strings)
-           (split-string sequence "" :omit-nulls)))))))
+           (split-string sequence-no-delimiters "" :omit-nulls)))))))
 
 (defun denote-sequence--alpha-to-number (string)
   "Convert STRING of alphabetic characters to its numeric equivalent."
@@ -239,7 +382,8 @@ has the same meaning as in `denote-sequence-and-scheme-p'."
           (make-string times ?z)))))))
 
 (defun denote-sequence--alpha-to-number-complete (sequence)
-  "Like `denote-sequence--alpha-to-number' but for the complete SEQUENCE."
+  "Like `denote-sequence--alpha-to-number' but for the complete SEQUENCE.
+If SEQUENCE conforms with `denote-sequence-numeric-p', return it as-is."
   (if (denote-sequence-numeric-p sequence)
       sequence
     (let* ((parts (denote-sequence-split sequence))
@@ -251,8 +395,13 @@ has the same meaning as in `denote-sequence-and-scheme-p'."
                              parts)))
       (denote-sequence-join converted-parts 'numeric))))
 
-(defun denote-sequence--number-to-alpha-complete (sequence)
-  "Like `denote-sequence--number-to-alpha' but for the complete SEQUENCE."
+(defun denote-sequence--number-to-alpha-complete (sequence target-scheme)
+  "Like `denote-sequence--number-to-alpha' but for the complete SEQUENCE.
+TARGET-SCHEME is either `alphanumeric' or `alphanumeric-delimited'.
+
+If SEQUENCE conforms with `denote-sequence-alphanumeric-p', return it as-is."
+  (unless (memq target-scheme '(alphanumeric alphanumeric-delimited))
+    (error "The TARGET-SCHEME can only be `alphanumeric' or 
`alphanumeric-delimited'"))
   (if (denote-sequence-alphanumeric-p sequence)
       sequence
     (let* ((parts (denote-sequence-split sequence))
@@ -268,25 +417,22 @@ has the same meaning as in 
`denote-sequence-and-scheme-p'."
                                 (t
                                  (denote-sequence--number-to-alpha string))))
                              parts)))
-      (denote-sequence-join converted-parts 'alphanumeric))))
+      (denote-sequence-join converted-parts target-scheme))))
 
-(defun denote-sequence-make-conversion (string &optional string-is-sequence)
-  "Convert STRING to its counterpart sequencing scheme.
-If STRING-IS-SEQUENCE then assume STRING to be a complete sequence, in
-which case convert the entirety of it.  Also see `denote-sequence-scheme'."
+(defun denote-sequence-make-conversion (string target-scheme &optional 
string-is-partial-sequence)
+  "Convert STRING to the given sequencing TARGET-SCHEME.
+With optional STRING-IS-PARTIAL-SEQUENCE interpret STRING accordingly."
+  (unless (memq target-scheme denote-sequence-schemes)
+    (error "The TARGET-SCHEME can only be one among the 
`denote-sequence-schemes'"))
   (cond
-   ((and string-is-sequence (denote-sequence-alphanumeric-p string))
+   (string-is-partial-sequence
+    (if (eq target-scheme 'numeric)
+        (denote-sequence--alpha-to-number string)
+      (denote-sequence--number-to-alpha string)))
+   ((eq target-scheme 'numeric)
     (denote-sequence--alpha-to-number-complete string))
-   ((and string-is-sequence (denote-sequence-numeric-p string))
-    (denote-sequence--number-to-alpha-complete string))
-   ((denote-sequence--alphanumeric-partial-p string)
-    (denote-sequence--alpha-to-number string))
-   ((denote-sequence--numeric-partial-p string)
-    (denote-sequence--number-to-alpha string))
    (t
-    (if string-is-sequence
-        (error "String `%s' did not pass `denote-sequence-p'" string)
-      (error "The `%s' must not contain both numbers and letters" string)))))
+    (denote-sequence--number-to-alpha-complete string target-scheme))))
 
 (define-obsolete-function-alias
   'denote-sequence-increment
@@ -528,7 +674,9 @@ With optional SEQUENCES operate on those, else use the 
return value of
     (delete-dups
      (mapcar
       (lambda (strings)
-        (denote-sequence-join (seq-take strings depth) 
(denote-sequence--scheme-of-strings strings)))
+        (denote-sequence-join
+         (seq-take strings depth)
+         denote-sequence-scheme))
       lists))))
 
 (defun denote-sequence--pad (sequence type)
@@ -591,7 +739,7 @@ Also see `denote-sequence-sort-sequences'."
 
 (defun denote-sequence--string-length-sans-delimiter (string)
   "Return length of STRING without the equals sign."
-  (if (eq denote-sequence-scheme 'numeric)
+  (if (memq denote-sequence-scheme '(numeric alphanumeric-delimited))
       (length (replace-regexp-in-string "=" "" string))
     (length string)))
 
@@ -621,15 +769,14 @@ TYPE is a symbol among `denote-sequence-types'."
           largest))
     (denote-sequence--get-largest-by-order sequences type)))
 
-(defun denote-sequence--get-start (&optional sequence prepend-delimiter)
+(defun denote-sequence--get-start (&optional sequence)
   "Return the start of a new sequence.
 With optional SEQUENCE, do so based on the final level of depth therein.
-This is usefule only for the alphanumeric `denote-sequence-scheme'.  If
-optional PREPEND-DELIMITER is non-nil, prepend the equals sign to the
-number if `denote-sequence-scheme' is numeric."
+This is usefule only for the alphanumeric `denote-sequence-scheme'."
+  ;; TODO 2026-04-03: Rewrite this for clarity.
   (pcase denote-sequence-scheme
-    ('numeric (if prepend-delimiter "=1" "1"))
-    ('alphanumeric
+    ('numeric "1")
+    ((or 'alphanumeric 'alphanumeric-delimited)
      (cond
       ((null sequence) "1")
       ((and sequence (denote-sequence--alphanumeric-partial-p (substring 
sequence -1))) "1")
@@ -650,7 +797,8 @@ return value of `denote-sequence-get-all-sequences'."
   "Return list of SEQUENCES that are `denote-sequence-scheme' or SCHEME."
   (let ((predicate (pcase (or scheme denote-sequence-scheme)
                      ('alphanumeric #'denote-sequence-alphanumeric-p)
-                     ('numeric #'denote-sequence-numeric-p))))
+                     ('numeric #'denote-sequence-numeric-p)
+                     ('alphanumeric-delimited 
#'denote-sequence-alphanumeric-delimited-p))))
     (seq-filter predicate sequences)))
 
 (defun denote-sequence--get-new-child (sequence &optional sequences)
@@ -659,9 +807,9 @@ Optional SEQUENCES has the same meaning as that specified 
in the
 function `denote-sequence-get-all-sequences-with-prefix'."
   (if-let* ((depth (+ (denote-sequence-depth sequence) 1))
             (all-unfiltered (denote-sequence-get-all-sequences-with-prefix 
sequence sequences))
-            (start-child (denote-sequence--get-start sequence 
:prepend-delimiter)))
+            (start-child (denote-sequence--get-start sequence)))
       (if (= (length all-unfiltered) 1)
-          (format "%s%s" (car all-unfiltered) start-child)
+          (denote-sequence-join (append (denote-sequence-split sequence) (list 
start-child)) denote-sequence-scheme)
         (if-let* ((all-schemeless (cond
                                    
((denote-sequence-get-all-sequences-with-max-depth depth all-unfiltered))
                                    (t all-unfiltered)))
@@ -678,8 +826,8 @@ function `denote-sequence-get-all-sequences-with-prefix'."
                        (append butlast (list new-number))
                      (list largest new-number))
                    scheme))
-              (format "%s%s" largest start-child))
-          (format "%s%s" sequence start-child)))
+              (denote-sequence-join (append (denote-sequence-split largest) 
(list start-child)) denote-sequence-scheme))
+          (denote-sequence-join (append (denote-sequence-split sequence) (list 
start-child)) denote-sequence-scheme)))
     (error "Cannot find sequences given sequence `%s' using scheme `%s'" 
sequence denote-sequence-scheme)))
 
 (defun denote-sequence--get-prefix-for-siblings (sequence)
@@ -712,7 +860,7 @@ function `denote-sequence-get-all-sequences-with-prefix'."
                          (last-component (car (nreverse components)))
                          (new-number (denote-sequence-increment-partial 
last-component)))
               (denote-sequence-join (append butlast (list new-number)) scheme))
-          (number-to-string (+ (string-to-number largest) 1)))
+          (denote-sequence-join (list (number-to-string (+ (string-to-number 
largest) 1))) denote-sequence-scheme))
       (error "Cannot find sequences given sequence `%s' using scheme `%s'" 
sequence denote-sequence-scheme))))
 
 (defun denote-sequence-get-new (type &optional sequence sequences)
@@ -1306,31 +1454,56 @@ file."
   (let ((new-sequence (denote-sequence--get-new-parent)))
     (denote-rename-file current-file 'keep-current 'keep-current new-sequence 
'keep-current 'keep-current)))
 
+(defvar denote-sequence-scheme-prompt-history nil
+  "Minibuffer history for `denote-sequence-scheme-prompt'.")
+
+(defun denote-sequence-scheme-prompt (&optional prompt-text)
+  "Prompt for one among the supported `denote-sequence-scheme' symbols.
+With optional PROMPT-TEXT, use it for the prompt message.  Else fall
+back to a generic prompt message."
+  (let ((default (car denote-sequence-scheme-prompt-history)))
+    (intern
+     (completing-read
+      (format-prompt (or prompt-text "Select sequence scheme") default)
+      denote-sequence-schemes nil t nil 'denote-sequence-scheme-prompt-history 
default))))
+
 ;;;###autoload
-(defun denote-sequence-convert (files)
+(defun denote-sequence-convert (files &optional target-scheme)
   "Convert the sequence scheme of FILES to match `denote-sequence-scheme'.
-When called from inside a Denote file, FILES is just the current file.
-When called from a Dired buffer, FILES are the marked files.  If no
-files are marked, then the one at point is considered.
+
+With optional TARGET-SCHEME as a prefix argument, prompt for a scheme
+among those supported by `denote-sequence-scheme'.  Otherwise, fall back
+to the current value of `denote-sequence-scheme'.
+
+When called from inside a Denote file, interpret FILES as just the
+current file.
+
+When called from a Dired buffer, FILES are the marked files.
+
+If no files are marked in the Dired buffer, then consider the one at
+point.
 
 Do not make any changes if the file among the FILES has no sequence or
 if it already matches the value of `denote-sequence-scheme'.  A file has
 a sequence when it conforms with `denote-sequence-file-p'.
 
-This command is for users who once used a `denote-sequence-scheme' and
-have since decided to switch to another.  IT DOES NOT REPARENT OR ANYHOW
-CHECK THE RESULTING SEQUENCES FOR DUPLICATES."
+[ This command is for users who once used a `denote-sequence-scheme' and
+  have since decided to switch to another.  IT DOES NOT REPARENT OR
+  ANYHOW CHECK THE RESULTING SEQUENCES FOR DUPLICATES: it simply
+  performs the conversion from one scheme to another.  ]"
   (interactive
    (list
     (if (derived-mode-p 'dired-mode)
         (dired-get-marked-files)
-      buffer-file-name))
+      buffer-file-name)
+    (when current-prefix-arg
+      (denote-sequence-scheme-prompt "Select target SCHEME")))
    dired-mode)
   (unless (listp files)
     (setq files (list files)))
   (dolist (file files)
     (when-let* ((old-sequence (denote-sequence-file-p file))
-                (new-sequence (denote-sequence-make-conversion old-sequence 
:is-complete-sequence)))
+                (new-sequence (denote-sequence-make-conversion old-sequence 
(or target-scheme denote-sequence-scheme))))
       (denote-rename-file file 'keep-current 'keep-current new-sequence 
'keep-current 'keep-current)))
   (denote-update-dired-buffers))
 
diff --git a/tests/denote-sequence-test.el b/tests/denote-sequence-test.el
index c9a66f872c..54df93a2fd 100644
--- a/tests/denote-sequence-test.el
+++ b/tests/denote-sequence-test.el
@@ -60,6 +60,76 @@
   (should (string= (denote-sequence-alphanumeric-p "1") "1"))
   (should (string= (denote-sequence-alphanumeric-p "1a") "1a")))
 
+(ert-deftest dst-denote-sequence--alphanumeric-delimited-check-alternation ()
+  "Test that `denote-sequence--alphanumeric-delimited-check-alternation' does 
the right thing.
+This helper function only checks that we alternate between numbers and
+letters.  It is not responsible to validate the levels of depth."
+  (should-not (denote-sequence--alphanumeric-delimited-check-alternation '("1" 
"=" "a" "=" "a")))
+  (should (denote-sequence--alphanumeric-delimited-check-alternation '("1" "=" 
"a" "=" "1"))))
+
+(ert-deftest dst-denote-sequence--alphanumeric-delimited-check-depths ()
+  "Test that `denote-sequence--alphanumeric-delimited-check-depths' does the 
right thing.
+This helper function is not responsible for checking whether the
+sequence alternates between numbers and letters.  It only checks the
+levels of depth between delimiters."
+  (should-not (denote-sequence--alphanumeric-delimited-check-depths '("1" "=" 
"a" "=" "1")))
+  (should-not (denote-sequence--alphanumeric-delimited-check-depths '("1" "=" 
"a" "=" "1" "a")))
+  (should-not (denote-sequence--alphanumeric-delimited-check-depths '("1" "a" 
"=" "1" "a" "=" "1" "a")))
+  (should (denote-sequence--alphanumeric-delimited-check-depths '("1")))
+  (should (denote-sequence--alphanumeric-delimited-check-depths '("1" "=" 
"1")))
+  (should (denote-sequence--alphanumeric-delimited-check-depths '("1" "=" "1" 
"a")))
+  (should (denote-sequence--alphanumeric-delimited-check-depths '("1" "=" "1" 
"a" "1")))
+  (should (denote-sequence--alphanumeric-delimited-check-depths '("1" "=" "1" 
"a" "1" "=" "1")))
+  (should (denote-sequence--alphanumeric-delimited-check-depths '("1" "=" "1" 
"a" "1" "=" "1" "a")))
+  (should (denote-sequence--alphanumeric-delimited-check-depths '("1" "=" "1" 
"a" "1" "=" "1" "a" "1"))))
+
+(ert-deftest dst-denote-sequence-alphanumeric-delimited-p ()
+  "Test that `denote-sequence-alphanumeric-delimited-p' does what it is 
supposed to."
+  (should-not (denote-sequence-alphanumeric-delimited-p "1a"))
+  (should-not (denote-sequence-alphanumeric-delimited-p "1=1"))
+  (should-not (denote-sequence-alphanumeric-delimited-p "1=a=a"))
+  (should-not (denote-sequence-alphanumeric-delimited-p "1=a=1")) ; check 
FIXME in the source
+  (should-not (denote-sequence-alphanumeric-delimited-p "hello"))
+  (should (string= (denote-sequence-alphanumeric-delimited-p "1") "1"))
+  (should (string= (denote-sequence-alphanumeric-delimited-p "1=a") "1=a"))
+  (should (string= (denote-sequence-alphanumeric-delimited-p "1=a1b") "1=a1b"))
+  (should (string= (denote-sequence-alphanumeric-delimited-p "1=a1b=2a1") 
"1=a1b=2a1"))
+  (should (string= (denote-sequence-alphanumeric-delimited-p "1=zza1zb=2za1") 
"1=zza1zb=2za1")))
+
+(ert-deftest dst-denote-sequence-and-scheme-p ()
+  "Test that `denote-sequence-and-scheme-p' covers all cases."
+  (should-error (denote-sequence-and-scheme-p "a"))
+  (should (equal (denote-sequence-and-scheme-p "1=1") (cons "1=1" 'numeric)))
+  (should (equal (denote-sequence-and-scheme-p "1a") (cons "1a" 
'alphanumeric)))
+  (should (equal (denote-sequence-and-scheme-p "1=a") (cons "1=a" 
'alphanumeric-delimited)))
+  (should
+   (let ((denote-sequence-scheme 'numeric))
+     (equal (denote-sequence-and-scheme-p "1") (cons "1" 'numeric))))
+  (should
+   (let ((denote-sequence-scheme 'alphanumeric))
+     (equal (denote-sequence-and-scheme-p "1") (cons "1" 'alphanumeric))))
+  (should
+   (let ((denote-sequence-scheme 'alphanumeric-delimited))
+     (equal (denote-sequence-and-scheme-p "1") (cons "1" 
'alphanumeric-delimited)))))
+
+(ert-deftest dst-denote-sequence-join ()
+  "Test that `denote-sequence-join' works as intended.
+The `denote-sequence-join' is not responsible for checking if the
+STRINGS passed to it conform with the given SCHEME."
+  (should (string= (denote-sequence-join '("1" "1" "1" "1") 'numeric) 
"1=1=1=1"))
+  (should (string= (denote-sequence-join '("1" "a" "1" "a") 'alphanumeric) 
"1a1a"))
+  (should (string= (denote-sequence-join '("1" "a") 'alphanumeric-delimited) 
"1=a"))
+  (should (string= (denote-sequence-join '("1" "a" "1") 
'alphanumeric-delimited) "1=a1"))
+  (should (string= (denote-sequence-join '("1" "a" "1" "a") 
'alphanumeric-delimited) "1=a1a"))
+  (should (string= (denote-sequence-join '("1" "a" "1" "a" "1" "a" "1" "a" "1" 
"a") 'alphanumeric-delimited) "1=a1a=1a1=a1a")))
+
+(ert-deftest dst-denote-sequence--number-to-alpha-complete ()
+  "Test that `denote-sequence--number-to-alpha-complete' does the right thing."
+  (should (string= (denote-sequence--number-to-alpha-complete "1=1=1=1=1=1=1" 
'alphanumeric) "1a1a1a1"))
+  (should (string= (denote-sequence--number-to-alpha-complete "1=1=1=1=1=1=1" 
'alphanumeric-delimited) "1=a1a=1a1"))
+  (should-error (denote-sequence--number-to-alpha-complete "1=1=1=1=1=1=1" 
'numeric))
+  (should-error (denote-sequence--number-to-alpha-complete "1=1=1=1=1=1=1" 
'numericdkjdldk)))
+
 (ert-deftest dst-denote-sequence--get-new-exhaustive ()
   "Test if we get the correct parent, child, sibling, or relatives of a 
sequence.
 Use the function `denote-sequence-get-new' for child and sibling with
@@ -210,36 +280,106 @@ function `denote-sequence-get-relative'."
                             "20241230T075023==10b--test__testing.txt"))
     (should (dst-relative-p "1a" 'all-children
                             "20241230T075023==1a1--test__testing.txt"
-                            "20241230T075023==1a2--test__testing.txt"))))
+                            "20241230T075023==1a2--test__testing.txt")))
+
+  (let* ((denote-sequence-scheme 'alphanumeric-delimited)
+         (denote-directory (expand-file-name "denote-sequence-test" 
temporary-file-directory))
+         (files
+          (mapcar
+           (lambda (file)
+             (let ((path (expand-file-name file (denote-directory))))
+               (if (file-exists-p path)
+                   path
+                 (with-current-buffer (find-file-noselect path)
+                   (save-buffer)
+                   (kill-buffer (current-buffer)))
+                 path)))
+           '("20241230T075023==1--test__testing.txt"
+             "20241230T075023==1=a--test__testing.txt"
+             "20241230T075023==1=a1--test__testing.txt"
+             "20241230T075023==1=a2--test__testing.txt"
+             "20241230T075023==1=b--test__testing.txt"
+             "20241230T075023==1=b1--test__testing.txt"
+             "20241230T075023==1=b1a--test__testing.txt"
+             "20241230T075023==2--test__testing.txt"
+             "20241230T075023==10--test__testing.txt"
+             "20241230T075023==10=a--test__testing.txt"
+             "20241230T075023==10=b--test__testing.txt")))
+         (sequences (denote-sequence-get-all-sequences files)))
+    (should (string= (denote-sequence-get-new 'parent) "11"))
+
+    (should (string= (denote-sequence-get-new 'child "1" sequences) "1=c"))
+    (should (string= (denote-sequence-get-new 'child "1=a" sequences) "1=a3"))
+    (should (string= (denote-sequence-get-new 'child "1=a2" sequences) 
"1=a2a"))
+    (should (string= (denote-sequence-get-new 'child "1=b" sequences) "1=b2"))
+    (should (string= (denote-sequence-get-new 'child "1=b1" sequences) 
"1=b1b"))
+    (should (string= (denote-sequence-get-new 'child "2" sequences) "2=a"))
+    (should-error (denote-sequence-get-new 'child "11" sequences))
+
+    (should (string= (denote-sequence-get-new 'sibling "1" sequences) "11"))
+    (should (string= (denote-sequence-get-new 'sibling "1=a" sequences) "1=c"))
+    (should (string= (denote-sequence-get-new 'sibling "1=a1" sequences) 
"1=a3"))
+    (should (string= (denote-sequence-get-new 'sibling "1=a2" sequences) 
"1=a3"))
+    (should (string= (denote-sequence-get-new 'sibling "1=b" sequences) "1=c"))
+    (should (string= (denote-sequence-get-new 'sibling "1=b1" sequences) 
"1=b2"))
+    (should (string= (denote-sequence-get-new 'sibling "2" sequences) "11"))
+    (should-error (denote-sequence-get-new 'sibling "12" sequences))
+
+    (should (string= (denote-sequence-get-relative "1=b1a" 'parent files)
+                     (expand-file-name 
"20241230T075023==1=b1--test__testing.txt" denote-directory)))
+    (should (string= (denote-sequence-get-relative "10=a" 'parent files)
+                     (expand-file-name 
"20241230T075023==10--test__testing.txt" denote-directory)))
+    (should (dst-relative-p "1=b1a" 'all-parents
+                            "20241230T075023==1--test__testing.txt"
+                            "20241230T075023==1=b--test__testing.txt"
+                            "20241230T075023==1=b1--test__testing.txt"))
+    (should (dst-relative-p "1=a" 'siblings
+                            "20241230T075023==1=a--test__testing.txt"
+                            "20241230T075023==1=b--test__testing.txt"))
+    (should (dst-relative-p "10=a" 'siblings
+                            "20241230T075023==10=a--test__testing.txt"
+                            "20241230T075023==10=b--test__testing.txt"))
+    (should (dst-relative-p "1" 'children
+                            "20241230T075023==1=a--test__testing.txt"
+                            "20241230T075023==1=b--test__testing.txt"))
+    (should (dst-relative-p "10" 'children
+                            "20241230T075023==10=a--test__testing.txt"
+                            "20241230T075023==10=b--test__testing.txt"))
+    (should (dst-relative-p "1=a" 'all-children
+                            "20241230T075023==1=a1--test__testing.txt"
+                            "20241230T075023==1=a2--test__testing.txt"))))
 
 (ert-deftest dst-denote-sequence-split ()
   "Test that `denote-sequence-split' splits a sequence correctly."
   (should (equal (denote-sequence-split "1") '("1")))
   (should (equal (denote-sequence-split "1=1=2") '("1" "1" "2")))
-  (should (equal (denote-sequence-split "1za5zx") '("1" "za" "5" "zx"))))
+  (should (equal (denote-sequence-split "1za5zx") '("1" "za" "5" "zx")))
+  (should (equal (denote-sequence-split "1=za5zx") '("1" "za" "5" "zx")))
+  (should (equal (denote-sequence-split "1=a2b") '("1" "a" "2" "b"))))
+  (should (equal (denote-sequence-split "1=a2b=1c3") '("1" "a" "2" "b" "1" "c" 
"3")))
 
 (ert-deftest dst-denote-sequence-make-conversion ()
   "Test that `denote-sequence-make-conversion' converts from alpha to numeric 
and vice versa."
-  (should (string= (denote-sequence-make-conversion "3") "c"))
-  (should (string= (denote-sequence-make-conversion "18") "r"))
-  (should (string= (denote-sequence-make-conversion "26") "z"))
-  (should (string= (denote-sequence-make-conversion "27") "za"))
-  (should (string= (denote-sequence-make-conversion "130") "zzzzz"))
-  (should (string= (denote-sequence-make-conversion "131") "zzzzza"))
-  (should (string= (denote-sequence-make-conversion "c") "3"))
-  (should (string= (denote-sequence-make-conversion "r") "18"))
-  (should (string= (denote-sequence-make-conversion "z") "26"))
-  (should (string= (denote-sequence-make-conversion "za") "27"))
-  (should (string= (denote-sequence-make-conversion "zzzzz") "130"))
-  (should (string= (denote-sequence-make-conversion "zzzzza") "131"))
-  (should (string= (denote-sequence-make-conversion "1=1=2" 
:string-is-sequence) "1a2"))
-  (should (string= (denote-sequence-make-conversion "1a2" :string-is-sequence) 
"1=1=2"))
-  (should (string= (denote-sequence-make-conversion "1=27=2=55" 
:string-is-sequence) "1za2zzc"))
-  (should (string= (denote-sequence-make-conversion "1za2zzc" 
:string-is-sequence) "1=27=2=55"))
-  (should (string= (denote-sequence-make-conversion "1=1=2=2=4=1" 
:string-is-sequence) "1a2b4a"))
-  (should (string= (denote-sequence-make-conversion "1a2b4a" 
:string-is-sequence) "1=1=2=2=4=1"))
-  (should-error (denote-sequence-make-conversion "111=a" :string-is-sequence))
-  (should-error (denote-sequence-make-conversion "a1" :string-is-sequence)))
+  (should (string= (denote-sequence-make-conversion "3" 'alphanumeric 
:string-is-partial-sequence) "c"))
+  (should (string= (denote-sequence-make-conversion "18" 'alphanumeric 
:string-is-partial-sequence) "r"))
+  (should (string= (denote-sequence-make-conversion "26" 'alphanumeric 
:string-is-partial-sequence) "z"))
+  (should (string= (denote-sequence-make-conversion "27" 'alphanumeric 
:string-is-partial-sequence) "za"))
+  (should (string= (denote-sequence-make-conversion "130" 'alphanumeric 
:string-is-partial-sequence) "zzzzz"))
+  (should (string= (denote-sequence-make-conversion "131" 'alphanumeric 
:string-is-partial-sequence) "zzzzza"))
+  (should (string= (denote-sequence-make-conversion "c" 'numeric 
:string-is-partial-sequence) "3"))
+  (should (string= (denote-sequence-make-conversion "r" 'numeric 
:string-is-partial-sequence) "18"))
+  (should (string= (denote-sequence-make-conversion "z" 'numeric 
:string-is-partial-sequence) "26"))
+  (should (string= (denote-sequence-make-conversion "za" 'numeric 
:string-is-partial-sequence) "27"))
+  (should (string= (denote-sequence-make-conversion "zzzzz" 'numeric 
:string-is-partial-sequence) "130"))
+  (should (string= (denote-sequence-make-conversion "zzzzza" 'numeric 
:string-is-partial-sequence) "131"))
+  (should (string= (denote-sequence-make-conversion "1=1=2" 'alphanumeric) 
"1a2"))
+  (should (string= (denote-sequence-make-conversion "1a2" 'numeric) "1=1=2"))
+  (should (string= (denote-sequence-make-conversion "1=27=2=55" 'alphanumeric) 
"1za2zzc"))
+  (should (string= (denote-sequence-make-conversion "1=27=2=55" 
'alphanumeric-delimited) "1=za2zzc"))
+  (should (string= (denote-sequence-make-conversion "1za2zzc" 'numeric) 
"1=27=2=55"))
+  (should (string= (denote-sequence-make-conversion "1=1=2=2=4=1" 
'alphanumeric) "1a2b4a"))
+  (should (string= (denote-sequence-make-conversion "1=1=2=2=4=1" 
'alphanumeric-delimited) "1=a2b=4a"))
+  (should (string= (denote-sequence-make-conversion "1a2b4a" 'numeric) 
"1=1=2=2=4=1")))
 
 (ert-deftest dst-denote-sequence-increment-partial ()
   "Test that `denote-sequence-increment-partial' does the right thing."


Reply via email to