The acad_strlsort list sorting function supplied with AutoLISP was limited to sorting strings. There were no other procedures to sort lists. Sorting a list efficiently is not a trivial matter. Sorting algorithms have been studied by specialists since a long time ago and are well documented5. Visual LISP
introduces two new list sorting functions that can be used with lists that contain not only strings, but also integers or real numbers and even other lists. These functions are vl-sort and vl-sort-i.
Both functions accept as input a list and a function that compares two arguments and that will serve to set the sorting order. This function may be a system
primitive, a user-defined function or a lambda expression. The difference between them lies in the values returned. With vl-sort we obtain a sorted list containing the original list elements. The preservation of all the original list elements is not guaranteed. If the list contains duplicates in some cases only one of them will be retained. This happens, for example, in the case of strings or
integer values. However, in real number lists the returned list will contain all of the elements.
In a list mixing real and integer values, duplicates will only appear if they are real. In the case of strings, the behavior will be similar to the case with integers.
_$ (vl-sort '(28 9.0 11 44 28 15.0 28.0 9.0 44 2 1 7 88) '<) (1 2 7 9.0 9.0 11 15.0 28 28.0 44 88)
_$
In case of using vl-sort-i the list returned will contain not the original values but the indices that indicate their position in the original list.
_$ (vl-sort-i '(28 9.0 11 44 28 15.0 28.0 9.0 44 2 1 7 88) '<) (10 9 11 7 1 2 5 6 4 0 8 3 12)
_$
These indices can be used to retrieve the original values by using the nth
function. The sort-list function uses this to return instead of the index list, the sorted list with its original elements.
(defun sort-list (lst func)
(mapcar '(lambda (x) (nth x lst)) (vl-sort-i lst func)))
Listing 5.8. List sorting function.
With this user-defined function we can directly obtain the sorted list with the original values:
_$ (sort-list '(28 9.0 11 44 28 15.0 28.0 9.0 44 2 1 7 88) '<) (1 2 7 9.0 9.0 11 15.0 28.0 28 28 44 44 88)
_$
Being able to define the comparison function used as sorting criterion permits making complex orderings. For example, if we want to sort a list of points according to the value of its X coordinate:
_$ (setq lst '((25.2 19.6 0.0) (24.1 16.7 0.0) (34.6 13.7 0.0))) ((25.2 19.6 0.0) (24.1 16.7 0.0) (34.6 13.7 0.0))
_$ (sort-list lst '(lambda (a b)(< (car a)(car b)))) ((24.1 16.7 0.0) (25.2 19.6 0.0) (34.6 13.7 0.0)) _$
In fact, we must frequently sort a list of points according to one of its
coordinates. To do this we could define a function that already incorporates the
condition that was proposed in the previous example. In this case the argument
coord will be an integer, 0 for the x, 1 for the Y and 2 for the Z. The reason is that to extract the value to compare we will use nth.
(defun sort-points (point-list coord) (mapcar '(lambda (x) (nth x point-list)) (vl-sort-i point-list
'(lambda (x y)
(< (nth coord x) (nth coord y)))))) Listing 5.9. Sorting a list of points by one of its coordinates.
Although we have no functions for sorting strings, it’s not difficult to design one knowing that we can count with a function capable of converting strings into lists.
The vl-string-list function returns a list of ASCII codes (integer values) corresponding to the characters in the string. Once sorted, they may be
converted back to characters and concatenated, as shown in the sort-string
function from Listing 5.10.
(defun sort-string (string func) (apply 'strcat
(mapcar 'chr
(sort-list (vl-string->list string) func)))) Listing 5.10. Function that sorts strings.
The limit to what is possible is only the programmer’s imagination. Another example: If you wish to order not letters, but words, we can design a function as
sort-phrase (Listing 5.11):
(defun sort-phrase (phrase func)
(sort-list (mapcar 'vl-princ-to-string
(read (strcat "(" phrase ")"))) func))
Listing 5.11. Function that sorts the words in a sentence.
Using which we will get a result like:
_$ (sort-phrase "character string ordering function" '>) ("STRING" "ORDERING" "FUNCTION" "CHARACTER")
_$
Note that the function read has converted strings into symbols and for that reason they are converted back to strings as uppercase instead of lowercase.
Should we wish to preserve these objects as strings, quoting every word would suffice. To do this we should replace each space with pairs of double quotes.
There is a string manipulation function that does just that, replacing some characters with others: vl-string-susbst, whose syntax is:
(vl-string-subst new-str pattern string [start-pos])
Trying it out in the console we obtain this output:
_$ (vl-string-subst "\" \"" " " "character string ordering function")
"character\" \"string ordering function"
_$
Note that to include a double quote character within a string without it acting as a string delimiter we must precede the character with a backslash. In addition we see that only the first occurrence of the character pattern is replaced. In this case we will be using vl-string-subst instead of a very similar function called vl-string-translate. This function also replaces characters in a string. Its syntax is:
(vl-string-translate source-set destination-set string)
But in this case each character in destination-set will replace the character that occupies the same position in source-set. The difference can be seen by evaluating the following expressions:
_$ (vl-string-subst "abc" "xyz" "Point coordinates are abc")
"Point coordinates are abc"
_$ (vl-string-translate "abc" "xyz" "Point coordinates are abc")
"Point zoordinxtes xre xyz"
So using it would not give us the correct string replacement:
_$ (vl-string-translate " " "\" \"" "character string ordering function")
"character\"string\"ordering\"function"
One advantage of vl-string-translate is that it will replace all the
occurrences of the specified characters, while with vl-string-subst only the first occurrence of the character pattern is replaced. To replace them all we must implement a while loop using as its test expression other string
manipulation function, vl-string-search that searches for the occurrence of a substring returning its position, or nil if it is not found. This enables us to
implement the replace function (See Listing 5.12) that would allow us to
replace all of the occurrences of a substring. To consider cases in which the new string includes the same characters as the old one, as when trying to replace
"x" with "xx", we would have to use the optional start-pos argument, adding to its previous value the new string’s length in each loop. Initially the value of pos value will be nil, so vl-string-search will use the default position 0 for the first loop. Evaluating string when the while loop ends will return its value.
(defun replace (new old string / pos len) (setq pos 0
len (strlen new))
(while (setq pos (vl-string-search old string pos)) (setq string (vl-string-subst new old string pos) pos (+ pos len)))
string)
Listing 5.12. Replacement of characters in a string.
And so we can propose a last sorting function, this time applied to words in sentences, preserving the original characters (see Listing 5.13).
(defun sort-phrases-as-strings (phrase / search-list) (acad_strlsort
(setq search-list
(read (strcat "(\""
(replace "\"\"" " " phrase) "\")")))))
Listing 5.13. Function for ordering words in sentences.
It should be noted that the sorting obtained with the comparison functions is not really alphabetical, but derived from the ASCII codes. So the existence of upper and lowercase characters can result in incorrect orderings from the alphabetical point of view. For this reason, in this case we use instead of vl-sort or vl-sort-i, the acad_strlsort function, which is specifically designed to sort strings.
_$ (sort-phrases-as-strings "This is a test PHRASE to sort") ("a" "is" "PHRASE" "sort" "test" "This" "to")
_$
In short, the new sorting features included in Visual LISP fill a void that until now required us to develop our own routines, not always very effective, to meet this goal.
5.5 Recursion
The definition of new functions is basically the combination of calls to other functions that are appropriate for the intended purposes. A particular case arises when a function calls itself. These functions are usually called recursive
functions. In certain cases the solution of a problem requires the repetition of certain processes. A recursive function implements loops by calling itself. For each repetition there is a new call to the original function6. Most of the
recursive functions are implemented that way because of the recursive nature of the data on which they operate.
The classic example of a recursive function is the calculation of a number’s factorial. The factorial of a number n is the product
(n · n-1 · n-2 … n-n)
i.e., the product of all positive integers less than or equal to n. For example, the factorial of 4 (which is symbolized as 4!) Is equal to:
(* 4 3 2 1)
The recursive nature of this definition leads naturally to its implementation as a recursive function. Knowing that the factorial of zero is 1:
(defun factorial (n) (cond ((zerop n) 1)
(t (* n (factorial (- n 1))))))
Listing 5.14. Factorial of a number.
It analyzes two conditions:
That argument received is 0. In this case 1 is returned.
Otherwise, the argument is multiplied by the result returned by reapplying the user-defined function factorial to the argument n minus 1.
The evaluation of (factorial 4) must wait for the outcome of a new call to the factorial function, this time with 3 as its argument. As in this new
invocation the argument is not yet equal to or less than 1 a result will not be yet obtained, so we will have now to evaluate (factorial 2). And this new call will also remain in waiting for another call, this time to (factorial 1). This time we will have a value, since the argument is now equal to 1. Thereafter each of the functions that remained in waiting can complete their part of the
calculation up to the original invocation which will return the final result. This
behavior can be seen clearly in the Visual LISP Trace window7. This will require evaluating the (trace factorial) expression in the Console so as to enable tracing of the factorial function. To turn tracing off, use the untrace
function.
Figure 5.2. Trace of the factorial function.
As in recursive processes function calls accumulate, there is always the danger of running out of memory if the number of repetitions is very high. Not having analyzed all of the alternatives can prevent the function from finding a terminal condition so it will run indefinitely. This would produce a Stack overflow error. Working in the VLISP IDE this error is avoided because the system will interrupt the process simulating an error after a certain number of recursions, reporting in the AutoCAD command line:
Command: Hard error occurred ***
internal stack limit reached (simulated)
In some cases, as in the case of nested lists, it is difficult to operate on them if not recursively. In other cases, as in dealing with single level lists or strings, the use of specific operators to implement iterative loops tend to be more effective.