branch: externals/compat commit 8190769d9eb9258dd8361bd322d90228dc586770 Author: Daniel Mendler <m...@daniel-mendler.de> Commit: Daniel Mendler <m...@daniel-mendler.de>
compat-30: Add value< and sort with keyword arguments --- NEWS.org | 2 + compat-25.el | 17 ------ compat-30.el | 91 +++++++++++++++++++++++++++++ compat-tests.el | 61 +++++++++++++++++++- compat.texi | 175 +++++++++++++++++++++++++++++++++++++++++++++++++------- 5 files changed, 308 insertions(+), 38 deletions(-) diff --git a/NEWS.org b/NEWS.org index 15e5e7b576..c51195b74a 100644 --- a/NEWS.org +++ b/NEWS.org @@ -4,6 +4,8 @@ * Development +- compat-30: Add extended function =sort= with keyword arguments. +- compat-30: New function =value<=. - compat-30: Add extended =copy-tree= with support for copying records with non-nil optional second argument. - compat-30: New macro =static-if=. diff --git a/compat-25.el b/compat-25.el index 4a662fd493..9a0b13d622 100644 --- a/compat-25.el +++ b/compat-25.el @@ -40,23 +40,6 @@ usage: (bool-vector &rest OBJECTS)" i (1+ i))) vec)) -;;;; Defined in fns.c - -(compat-defun sort (seq predicate) ;; <compat-tests:sort> - "Handle vector SEQ." - :extended t - (cond - ((listp seq) - (sort seq predicate)) - ((vectorp seq) - (let* ((list (sort (append seq nil) predicate)) - (p list) (i 0)) - (while p - (aset seq i (car p)) - (setq i (1+ i) p (cdr p))) - (apply #'vector list))) - (t (signal 'wrong-type-argument (list 'list-or-vector-p seq))))) - ;;;; Defined in editfns.c (compat-defalias format-message format) ;; <compat-tests:format-message> diff --git a/compat-30.el b/compat-30.el index 4d9c5b1e74..28f9faaa1a 100644 --- a/compat-30.el +++ b/compat-30.el @@ -219,5 +219,96 @@ enclosed in a `progn' form. ELSE-FORMS may be empty." then-form (cons 'progn else-forms))) +;;;; Defined in fns.c + +(compat-defun value< (a b) ;; <compat-tests:value<> + "Return non-nil if A precedes B in standard value order. +A and B must have the same basic type. +Numbers are compared with <. +Strings and symbols are compared with string-lessp. +Lists, vectors, bool-vectors and records are compared lexicographically. +Markers are compared lexicographically by buffer and position. +Buffers and processes are compared by name. +Other types are considered unordered and the return value will be ‘nil’." + (cond + ((and (number-or-marker-p a) (number-or-marker-p b)) + (< a b)) + ((or (and (stringp a) (stringp b)) + (and (symbolp a) (symbolp b))) + (string< a b)) + ((and (listp a) (listp b)) + (while (and (consp a) (consp b) (equal (car a) (car b))) + (setq a (cdr a) b (cdr b))) + (cond + ((not b) nil) + ((not a) t) + ((and (consp a) (consp b)) (value< (car a) (car b))) + (t (value< a b)))) + ((and (vectorp a) (vectorp b)) + (let* ((na (length a)) + (nb (length b)) + (n (min na nb)) + (i 0)) + (while (and (< i n) (equal (aref a i) (aref b i))) + (cl-incf i)) + (if (< i n) (value< (aref a i) (aref b i)) (< n nb)))) + ;; TODO Add support for more types. + (t (error "value< unsupported type: %S %S" a b)))) + +(compat-defun sort (seq &optional lessp &rest rest) ;; <compat-tests:sort> + "Sort function with support for keyword arguments. +The following arguments are defined: + +:key FUNC -- FUNC is a function that takes a single element from SEQ and + returns the key value to be used in comparison. If absent or nil, + `identity' is used. + +:lessp FUNC -- FUNC is a function that takes two arguments and returns + non-nil if the first element should come before the second. + If absent or nil, `value<' is used. + +:reverse BOOL -- if BOOL is non-nil, the sorting order implied by FUNC is + reversed. This does not affect stability: equal elements still retain + their order in the input sequence. + +:in-place BOOL -- if BOOL is non-nil, SEQ is sorted in-place and returned. + Otherwise, a sorted copy of SEQ is returned and SEQ remains unmodified; + this is the default. + +For compatibility, the calling convention (sort SEQ LESSP) can also be used; +in this case, sorting is always done in-place." + :extended t + (let ((in-place t) (orig-seq seq)) + (when (or (not lessp) rest) + (setq + rest (if lessp (cons lessp rest) rest) + in-place (plist-get rest :in-place) + lessp (let ((key (plist-get rest :key)) + (reverse (plist-get rest :reverse)) + (< (or (plist-get rest :lessp) #'value<))) + (cond + ((and key reverse) + (lambda (a b) (not (funcall < (funcall key a) (funcall key b))))) + (key + (lambda (a b) (funcall < (funcall key a) (funcall key b)))) + (reverse + (lambda (a b) (not (funcall < a b)))) + (t <))) + seq (if (or (eval-when-compile (< emacs-major-version 25)) in-place) + seq + (copy-sequence seq)))) + ;; Emacs 24 does not support vectors. Convert to list. + (when (and (eval-when-compile (< emacs-major-version 25)) (vectorp seq)) + (setq seq (append seq nil))) + (setq seq (sort seq lessp)) + ;; Emacs 24: Convert back to vector. + (if (and (eval-when-compile (< emacs-major-version 25)) (vectorp orig-seq)) + (if in-place + (cl-loop for i from 0 for x in seq + do (aset orig-seq i x) + finally return orig-seq) + (apply #'vector seq)) + seq))) + (provide 'compat-30) ;;; compat-30.el ends here diff --git a/compat-tests.el b/compat-tests.el index bac29aeb4b..64e1072a10 100644 --- a/compat-tests.el +++ b/compat-tests.el @@ -1757,6 +1757,45 @@ (should-equal '(1 2 3 4) (flatten-tree '((1) nil 2 ((3 4))))) (should-equal '(1 2 3 4) (flatten-tree '(((1 nil)) 2 (((3 nil nil) 4)))))) +(defmacro compat--should-value< (x y) + "Helper for (value< X Y) test." + `(progn + (should (value< ,x ,y)) + (should-not (value< ,y ,x)))) + +(ert-deftest compat-value< () + ;; Type mismatch + (should-error (value< 'aa "aa")) + (should-error (value< 1 "aa")) + (should-error (value< 1 (cons 1 2))) + ;; Nil symbol + (compat--should-value< nil t) + (compat--should-value< nil 'nim) + (compat--should-value< nil 'nll) + (compat--should-value< 'mil nil) + ;; Atoms + (compat--should-value< 1 2) + (compat--should-value< "aa" "b") + (compat--should-value< 'aa 'b) + ;; Lists + (compat--should-value< nil '(1)) + (compat--should-value< '(1 2) '(2 3)) + (compat--should-value< '(1 2 3) '(2)) + (compat--should-value< '(0 1 2) '(0 2 3)) + (compat--should-value< '(0 1 2 3) '(0 2)) + ;; Pairs and improper lists + (compat--should-value< nil '(1 . 2)) + (compat--should-value< nil '(1 2 . 3)) + (compat--should-value< '(1 . 2) '(2 . 2)) + (compat--should-value< '(1 . 2) '(1 . 3)) + (compat--should-value< '(1 2 . 3) '(1 2 . 4)) + ;; Vectors + (compat--should-value< [] [1]) + (compat--should-value< [1 2] [2 3]) + (compat--should-value< [1 2 3] [2]) + (compat--should-value< [0 1 2] [0 2 3]) + (compat--should-value< [0 1 2 3] [0 2])) + (ert-deftest compat-sort () (should-equal (list 1 2 3) (sort (list 1 2 3) #'<)) (should-equal (list 1 2 3) (sort (list 1 3 2) #'<)) @@ -1764,14 +1803,34 @@ (should-equal (list 1 2 3) (compat-call sort (list 1 2 3) #'<)) (should-equal (list 1 2 3) (compat-call sort (list 1 3 2) #'<)) (should-equal (list 1 2 3) (compat-call sort (list 3 2 1) #'<)) + ;; Test Emacs 25 support for vectors. (should-equal [1 2 3] (compat-call sort (vector 1 2 3) #'<)) (should-equal [1 2 3] (compat-call sort (vector 1 3 2) #'<)) (should-equal [1 2 3] (compat-call sort (vector 3 2 1) #'<)) ;; Test side effect (let* ((vec (vector 4 5 8 3 1 2 3 2 3 4)) (sorted (compat-call sort vec #'>))) + (should (eq vec sorted)) (should-equal sorted [8 5 4 4 3 3 3 2 2 1]) - (should-equal vec [8 5 4 4 3 3 3 2 2 1]))) + (should-equal vec [8 5 4 4 3 3 3 2 2 1])) + ;; Test Emacs 30 keyword arguments. + (should-equal '(1 2 3) (compat-call sort '(2 3 1))) + (should-equal '(3 2 1) (compat-call sort '(2 3 1) :reverse t)) + (should-equal '((x 3) (y 2) (z 1)) (compat-call sort '((z 1) (x 3) (y 2)) :key #'car)) + (should-equal '((z 1) (y 2) (x 3)) (compat-call sort '((z 1) (x 3) (y 2)) :key #'car :reverse t)) + (should-equal '((z 1) (y 2) (x 3)) (compat-call sort '((z 1) (x 3) (y 2)) :key #'cadr)) + (should-equal '((x 3) (y 2) (z 1)) (compat-call sort '((z 1) (x 3) (y 2)) :key #'cadr :reverse t)) + (should-equal '(3 2 1) (compat-call sort '(2 3 1) :lessp #'>)) + (should-equal '(1 2 3) (compat-call sort '(2 3 1) :reverse t :lessp #'>)) + (should-equal '((30 1) (20 2) (10 3)) (compat-call sort '((30 1) (10 3) (20 2)) :key #'car :lessp #'>)) + (should-equal '((10 3) (20 2) (30 1)) (compat-call sort '((30 1) (10 3) (20 2)) :key #'car :reverse t :lessp #'>)) + (should-equal '((x 3) (y 2) (z 1)) (compat-call sort '((z 1) (x 3) (y 2)) :key #'cadr :lessp #'>)) + (should-equal '((z 1) (y 2) (x 3)) (compat-call sort '((z 1) (x 3) (y 2)) :key #'cadr :reverse t :lessp #'>)) + (let* ((vec (vector 4 5 8 3 1 2 3 2 3 4)) + (sorted (compat-call sort vec :in-place t))) + (should (eq vec sorted)) + (should-equal sorted [1 2 2 3 3 3 4 4 5 8]) + (should-equal vec [1 2 2 3 3 3 4 4 5 8]))) (ert-deftest compat-replace-string-in-region () (with-temp-buffer diff --git a/compat.texi b/compat.texi index 825f348e8e..4177b56ad0 100644 --- a/compat.texi +++ b/compat.texi @@ -532,26 +532,6 @@ arguments, @var{objects}. @xref{Bool-Vectors,,,elisp}. @end defun -@subsection Extended Definitions -These functions must be called explicitly via @code{compat-call}, -since their calling convention or behavior was extended in Emacs 25.1: - -@defun compat-call@ sort sequence predicate -This function sorts @var{sequence} stably. Note that this function -doesn't work for all sequences; it may be used only for lists and -vectors. If @var{sequence} is a list, it is modified destructively. -This functions returns the sorted @var{sequence} and compares elements -using @var{predicate}. A stable sort is one in which elements with -equal sort keys maintain their relative order before and after the -sort. Stability is important when successive sorts are used to order -elements according to different criteria. - -@xref{Sequence Functions,,,elisp}. - -The compatibility version adds support for vectors to be sorted, not -just lists. -@end defun - @subsection Missing Definitions Compat does not provide support for the following Lisp features implemented in 25.1: @@ -3350,6 +3330,63 @@ older than 30.1. Note that due to upstream changes, it might happen that there will be the need for changes, so use these functions with care. +@c copied from lispref/sequences.texi +@defun value< a b +This function returns non-@code{nil} if @var{a} comes before @var{b} +in the standard sorting order; this means that it returns @code{nil} +when @var{b} comes before @var{a}, or if they are equal or unordered. + +The arguments @var{a} and @var{b} must have the same type. +Specifically: + +@itemize @bullet +@item +Numbers are compared using @code{<}. +@item +Strings are compared using @code{string<} and symbols are compared by +comparing their names as strings. +@item +Conses, lists, vectors and records are compared lexicographically. +This means that the two sequences are compared element-wise from left +to right until they differ, and the result is then that of +@code{value<} on the first pair of differing elements. If one +sequence runs out of elements before the other, the shorter sequence +comes before the longer. +@item +Markers are compared first by buffer, then by position. +@item +Buffers and processes are compared by comparing their names as +strings. Dead buffers (whose name is @code{nil}) will compare before +any live buffer. +@item +Other types are considered unordered and the return value will be +@code{nil}. +@end itemize + +Examples: +@example +(value< -4 3.5) @result{} t +(value< "dog" "cat") @result{} nil +(value< 'yip 'yip) @result{} nil +(value< '(3 2) '(3 2 0)) @result{} t +(value< [3 2 "a"] [3 2 "b"]) @result{} t +@end example + +@noindent +Note that @code{nil} is treated as either a symbol or an empty list, +depending on what it is compared against: + +@example +(value< nil '(0)) @result{} t +(value< 'nib nil) @result{} t +@end example + +@noindent +There is no limit to the length of sequences (lists, vectors and so +on) that can be compared, but @code{value<} may fail with an error if +used to compare circular or deeply nested data structures. +@end defun + @c based on lispref/lists.texi @defun drop n list This function is an alias for @code{nthcdr}. It returns the @var{n}th @@ -3466,6 +3503,104 @@ Here is an example of its use from CC Mode, which prevents a These functions must be called explicitly via @code{compat-call}, since their calling convention or behavior was extended in Emacs 30.1: +@c copied from lisp/sequences.texi +@defun compat-call@ sort sequence &rest keyword-args +This function sorts @var{sequence}, which must be a list or vector, +and returns a sorted sequence of the same type. The sort is stable, +which means that elements with equal sort keys maintain their relative +order. It takes the following optional keyword arguments: + +@table @code +@item :key @var{keyfunc} +Use @var{keyfunc}, a function that takes a single element from +@var{sequence} and returns its key value, to generate the keys used in +comparison. If this argument is absent or if @var{keyfunc} is +@code{nil} then @code{identity} is assumed; that is, the elements +themselves are used as sorting keys. + +@item :lessp @var{predicate} +Use @var{predicate} to order the keys. @var{predicate} is a function +that takes two sort keys as arguments and returns non-@code{nil} if +the first should come before the second. If this argument is absent +or @var{predicate} is @code{nil}, then @code{value<} is used, which is +applicable to many different Lisp types and generally sorts in +ascending order. + +For consistency, any predicate must obey the following rules: +@itemize @bullet +@item +It must be @dfn{antisymmetric}: it cannot both order @var{a} before +@var{b} and @var{b} before @var{a}. +@item +It must be @dfn{transitive}: if it orders @var{a} before @var{b} and +@var{b} before @var{c}, then it must also order @var{a} before @var{c}. +@end itemize + +@item :reverse @var{flag} +If @var{flag} is non-@code{nil}, the sorting order is reversed. With +the default @code{:lessp} predicate this means sorting in descending order. + +@item :in-place @var{flag} +If @var{flag} is non-@code{nil}, then @var{sequence} is sorted +in-place (destructively) and returned. If @code{nil}, or if this +argument is not given, a sorted copy of the input is returned and +@var{sequence} itself remains unmodified. In-place sorting is +slightly faster, but the original sequence is lost. +@end table + +If the default behaviour is not suitable for your needs, it is usually +easier and faster to supply a new @code{:key} function than a +different @code{:lessp} predicate. For example, consider sorting +these strings: + +@example +@group +(setq numbers '("one" "two" "three" "four" "five" "six")) +(sort numbers) + @result{} ("five" "four" "one" "six" "three" "two") +@end group +@end example + +You can sort the strings by length instead by supplying a different key +function: + +@example +@group +(sort numbers :key #'length) + @result{} ("one" "two" "six" "four" "five" "three") +@end group +@end example + +@noindent +Note how strings of the same length keep their original order, thanks to +the sorting stability. Now suppose you want to sort by length, but use +the string contents to break ties. The easiest way is to specify a key +function that transforms an element to a value that is sorted this way. +Since @code{value<} orders compound objects (conses, lists, +vectors and records) lexicographically, you could do: + +@example +@group +(sort numbers :key (lambda (x) (cons (length x) x))) + @result{} ("one" "six" "two" "five" "four" "three") +@end group +@end example + +@noindent +because @code{(3 . "six")} is ordered before @code{(3 . "two")} and so on. + +For compatibility with previous versions of Emacs, the @code{sort} +function can also be called using the fixed two-argument form: + +@example +(@code{sort} @var{sequence} @var{predicate}) +@end example + +@noindent +where @var{predicate} is the @code{:lessp} argument. When using this +form, sorting is always done in-place. +@end defun + @c based on lisp/minibuffer.el @defun compat-call@ completion-metadata-get metadata prop Get property @var{prop} from completion @var{metadata}. If the