Building Association Lists: A Simple Block Counter

Introduction

This tutorial provides a step-by-step explanation of how to construct a simple block counter program in AutoLISP, which will prompt the user for a selection of block references and will output the quantities of each block in the selection to the AutoCAD command-line.

The tutorial will demonstrate how to count the blocks by building an appropriate association list to record the block names and their associated quantities - this concept may then be applied to other applications which involve recording & updating items of data.

The tutorial is aimed at those with a basic understanding of the AutoLISP language and syntax, and, unlike most other tutorials written for beginners, the end result may actually prove useful in real-world drafting work.

A Basic Framework

In the following steps, we shall first construct a basic framework for the block counter program: defining a function which may be invoked from the AutoCAD command-line, obtaining a selection of blocks to be counted, and constructing a means of iterating over the selection.

Defining a Function

As the very first step, we shall define a function c:myblockcounter which will perform all of the operations of the block counter:

Select all
(defun c:myblockcounter ( / ) ;; Define function
) ;; end defun

By prefixing the function name with c: the function may be evaluated directly at the AutoCAD command-line using the command which follows this c: prefix, i.e. myblockcounter.

A defun expression cannot be empty, so let us now include the expressions to be evaluated when the function is invoked.

Obtaining a Selection

The first operation to be performed by the function is to obtain a selection of blocks to be counted - for this we will use the ssget function.

The ssget function is a general selection function which may be used to obtain a selection of multiple objects either from a user or automatically (a full reference detailing all of the options offered by this function may be found here).

Select all
(defun c:myblockcounter ( / ) ;; Define function
    (ssget) ;; Prompt the user to make a selection and return the selection set if successful
) ;; end defun

Upon running the above program the user is prompted for a selection of objects, and, if a valid selection is made, the function will return a pointer to the selection set, otherwise, the function will return nil.

Notice that these values are returned at the command-line because the ssget expression is the last expression evaluated within the defun expression and therefore any value returned by the ssget function is then returned by the defun expression.

However, as it stands, we cannot use the selection set returned by ssget, as the value is not currently assigned to a variable and so cannot be referenced elsewhere in the program. So we will now assign the value returned by ssget to a local variable sel.

Select all
(defun c:myblockcounter ( / sel ) ;; Define function
    (setq sel ;; Assign the value returned by the following expression to the symbol 'sel'
        (ssget) ;; Prompt the user to make a selection and return the selection set if successful
    ) ;; end setq
) ;; end defun

Note that the symbol sel has also been declared as a variable local to the function c:myblockcounter within the symbol list for the defun expression - to understand why this is important, see my tutorial on Localising Variables.

The current code still allows the user to select any object, so we can refine this selection to only permit block references using an appropriate ssget filter list argument:

Select all
(defun c:myblockcounter ( / sel ) ;; Define function
    (setq sel ;; Assign the value returned by the following expression to the symbol 'sel'
        (ssget ;; Prompt the user to make a selection and return the selection set if successful
           '((0 . "INSERT")) ;; Filter the selection to block references only (INSERTs)
        ) ;; end ssget
    ) ;; end setq
) ;; end defun

Finally, before we continue to attempt to process the selection of blocks, we should first check whether the user has actually made a valid selection, or whether the user has simply dimissed the prompt without selecting any block references.

For this, we can use an if statement:

Select all
(defun c:myblockcounter ( / sel ) ;; Define function, declare local variables
    (if ;; If the following expression returns a non-nil value
        (setq sel ;; Assign the value returned by the following expression to the symbol 'sel'
            (ssget ;; Prompt the user to make a selection and return the selection set if successful
               '((0 . "INSERT")) ;; Filter the selection to block references only (INSERTs)
            ) ;; end ssget
        ) ;; end setq
        "Selection valid."   ;; Then the selection was valid
        "Selection invalid." ;; Else the selection was invalid
    ) ;; end if
) ;; end defun

Try the above program - if a valid selection is made, the function will return the result of the 'then' argument ("Selection valid"), otherwise, the 'else' argument will be returned ("Selection invalid").


Aside: you will note that, in AutoLISP, a conditional expression (such an if statement) will be validated if the test expression returns any non-nil value - that is, the test expression doesn't necessarily need to return T or True explicitly for the if statement to be validated, for example consider the following:

(setq var 1)
(if var
    (princ "Do this.")
    (princ "Else do this.")
)

Here, the test expression is simply the symbol var - this symbol is evaluated by the if function to yield any value it may have been assigned. In the above example, the symbol var is a variable which has been assigned a value of 1. Since 1 is not equal to nil, the if statement is validated and the 'then' argument will be evaluated.

Only if the test expression returns a value equivalent to nil will the 'else' expression (if supplied) be evaluated.


Processing the Selection

Now that the function has been defined and we have obtained a selection of blocks from the user, the program now needs to iterate over the selection to count the number of occurrences of each distinct block.

For this, we can use Method 2a from my Selection Set Processing tutorial, which uses a repeat expression to process each item in the set, starting with the item at the highest index.

So, let us start by incorporating the repeat function into our existing code:

Select all
(defun c:myblockcounter ( / sel ) ;; Define function, declare local variables
    (if ;; If the following expression returns a non-nil value
        (setq sel ;; Assign the value returned by the following expression to the symbol 'sel'
            (ssget ;; Prompt the user to make a selection and return the selection set if successful
               '((0 . "INSERT")) ;; Filter the selection to block references only (INSERTs)
            ) ;; end ssget
        ) ;; end setq
        (repeat ;; Repeat the enclosed expressions the following number of times:
            (sslength sel) ;; Return the number of items in the selection set
        ) ;; end repeat
    ) ;; end if
) ;; end defun

Here, the repeat expression constitutes the 'then' argument for the if function; I have omitted the 'else' argument, as it is not necessary to inform the user that no objects were selected (they already know this!).

The sslength expression will return the number of items in our selection set sel and will therefore cause the repeat expression to evaluate any enclosed expressions a number of times equal to the number of objects selected by the user, hence processing every selected block reference.


At this point it is worth noting that the sslength expression is the reason that the if statement must be used: if the variable sel was nil, then (sslength sel) would return an error (specifically, bad argument type: lselsetp nil), therefore, the if statement ensures that the expression (sslength sel) is only evaluated if the variable sel is non-nil and therefore contains a valid selection set.


Continuing with the 'Method 2a' structure, we can define an index variable idx and access the block reference entity at each index in the selection set:

Select all
(defun c:myblockcounter ( / idx sel ) ;; Define function, declare local variables
    (if ;; If the following expression returns a non-nil value
        (setq sel ;; Assign the value returned by the following expression to the symbol 'sel'
            (ssget ;; Prompt the user to make a selection and return the selection set if successful
               '((0 . "INSERT")) ;; Filter the selection to block references only (INSERTs)
            ) ;; end ssget
        ) ;; end setq
        (repeat ;; Repeat the enclosed expressions the following number of times:
            (setq idx ;; Assign the value returned by the following expression to the symbol 'idx'
                (sslength sel) ;; Return the number of items in the selection set
            ) ;; end setq
            (print ;; Print the following expression to the command-line (just for testing)
                (ssname sel ;; Retrieve the entity at the following index
                    (setq idx (1- idx)) ;; Decrement the index variable (since selection set indexes are zero-based)
                ) ;; end ssname
            ) ;; end print
        ) ;; end repeat
    ) ;; end if
) ;; end defun

To understand how this structure works, please read the Method 2a section of my Selection Set Processing tutorial.


If we run the above program, selecting a set of blocks will cause each entity name in the selection to be printed to the AutoCAD command-line, however, you will notice something curious: selecting three block references will result in four entity names being printed to the command-line, with the last entity name repeated e.g.:

Select all
Command: myblockcounter
Select objects: Specify opposite corner: 3 found

Select objects:

<Entity name: 7ffff7101f0>
<Entity name: 7ffff710200>
<Entity name: 7ffff710290> <Entity name: 7ffff710290>

Do not worry! - This isn't an error with the repeat expression.

The repeat function will evaluate the enclosed expressions a given number of times and will then return the value that is returned by the last evaluated expression. In our case, the last evaluated expression is the print expression, which itself will return the value that is supplied to it (i.e. the entity name).

The if function will return the value returned by either the 'then' or 'else' arguments, therefore, in our case, this is the value returned by the repeat function (i.e. the entity name).

Finally, the defun expression will return the value returned by the last expression evaluated within the function definition - which is of course the value returned by the if function, and hence the extra entity name.

To avoid returning this value, we simply need to ensure that the last expression evaluated within the function definition returns nothing (i.e. a null symbol). For this, we can use either a (princ) or (prin1) expression:

Select all
(defun c:myblockcounter ( / idx sel ) ;; Define function, declare local variables
    (if ;; If the following expression returns a non-nil value
        (setq sel ;; Assign the value returned by the following expression to the symbol 'sel'
            (ssget ;; Prompt the user to make a selection and return the selection set if successful
               '((0 . "INSERT")) ;; Filter the selection to block references only (INSERTs)
            ) ;; end ssget
        ) ;; end setq
        (repeat ;; Repeat the enclosed expressions the following number of times:
            (setq idx ;; Assign the value returned by the following expression to the symbol 'idx'
                (sslength sel) ;; Return the number of items in the selection set
            ) ;; end setq
            (print ;; Print the following expression to the command-line (just for testing)
                (ssname sel ;; Retrieve the entity at the following index
                    (setq idx (1- idx)) ;; Decrement the index variable (since selection set indexes are zero-based)
                ) ;; end ssname
            ) ;; end print
        ) ;; end repeat
    ) ;; end if
    (princ) ;; Suppress the return of the last evaluated expression (if)
) ;; end defun

As a final step before we construct the association list to record the occurrences of each block in the selection, we first need to obtain the name of each block.

The block name will be used as the 'key' for each item in the association list, with an associated integer value corresponding to the number of occurrences of the block name in the selection.

To obtain any property of an entity, we can query the DXF data stored in the drawing database for the entity (the entity name is simply a pointer to this DXF data) - to obtain this data, we use the entget function.

The DXF data returned by entget is itself an association list: a list containing any number of sublists, with each sublist containing two items of data: a 'key' and its associated value. Such sublists are known as 'dotted pairs'.

The block name for a block reference entity is the value associated with DXF group 2, that is, the value associated with the sublist whose first element is the integer 2.

To obtain this item from the DXF data association list, we can use the assoc function coupled with the cdr to obtain the associated value, which is then assigned to a local variable blk:

Select all
(defun c:myblockcounter ( / blk idx sel ) ;; Define function, declare local variables
    (if ;; If the following expression returns a non-nil value
        (setq sel ;; Assign the value returned by the following expression to the symbol 'sel'
            (ssget ;; Prompt the user to make a selection and return the selection set if successful
               '((0 . "INSERT")) ;; Filter the selection to block references only (INSERTs)
            ) ;; end ssget
        ) ;; end setq
        (repeat ;; Repeat the enclosed expressions the following number of times:
            (setq idx ;; Assign the value returned by the following expression to the symbol 'idx'
                (sslength sel) ;; Return the number of items in the selection set
            ) ;; end setq
            (setq blk ;; Assign the block name to the variable 'blk'
                (cdr ;; Retrieve the value associated with DXF group 2 (the block name)
                    (assoc 2 ;; Retrieve the DXF group 2 dotted pair from the following DXF data
                        (entget ;; Retrieve the list of DXF data for the following entity
                            (ssname sel ;; Retrieve the entity at the following index
                                (setq idx (1- idx)) ;; Decrement the index variable (since selection set indexes are zero-based)
                            ) ;; end ssname
                        ) ;; end entget
                    ) ;; end assoc
                ) ;; end cdr
            ) ;; end setq
            (print blk) ;; Print the block name to the command-line (just for testing)
        ) ;; end repeat
    ) ;; end if
    (princ) ;; Suppress the return of the last evaluated expression (if)
) ;; end defun

Upon testing the above, you will see the block name for each selected block reference printed to the command-line.

Note that this may return anonymous block names for dynamic block references in the selection: this is because a dynamic block reference will reference an anonymous block definition when its dynamic block parameters are altered from those defined in its original block definition. To convert these anonymous block names to the true name of the dynamic block, we could use my Effective Block Name functions, however, this is beyond the scope of this tutorial.

Some Background on Association Lists

Now that we have the basic framework for our block counter program, we will now construct the main block counting engine: the association list.

What is an Association List?

An association list is typically a list of dotted pairs: each dotted pair has a 'key' as the first element, and an associated value as the second element. Though, in a general sense, an association list in AutoLISP may be any list of lists.

Below is a simple example of an association list of dotted pairs, defined literally using an apostrophe:

(setq alist
   '(
        ("Item 1" . "Value 1")
        ("Item 2" . "Value 2")
        ("Item 3" . "Value 3")
    )
)

Note that the keys and associated values need not necessarily be strings - since AutoLISP is dynamically typed, these may be any AutoLISP data type, and furthermore, items within the association list may also contain data of varying types.

The keys within an association list also do not need to be unique (as an example, consider the vertex DXF data for an LWPOLYLINE entity), however, it is usually far more useful if the keys are unique.

Constructing a Dotted Pair

Unlike a standard AutoLISP list, a dotted pair is a special form of list which differs in the way in which is stored in memory.

Without delving into too much detail, a standard list in AutoLISP is known as a Linked List. Each item in the list requires two memory locations (registers): the first (known as the address register) stores the value of the list item; the second (known as the decrement register) stores a pointer giving the location or address of the address register of the next item in the list.

To access each register, we have the functions car (Contents of Address Register) and cdr (Contents of Decrement Register). For a standard list, the car function will therefore return the first item in the list, and the cdr function will return the remainder of the list:

_$ (setq lst '(1 2 3 4 5))
(1 2 3 4 5)
_$ (car lst)
1
_$ (cdr lst)
(2 3 4 5)

Dotted pairs differ from standard lists in that rather than occupying two memory registers for each list item, the key of the dotted pair occupies a single register, and the associated value occupies a second adjacent register. Therefore, whilst the car function will still return the first item (the key) of the dotted pair, the cdr function will now return the second item (the associated value) of the dotted pair as opposed to a list:

_$ (setq dpair '(1 . 2))
(1 . 2)
_$ (car dpair)
1
_$ (cdr dpair)
2

Similar to how a standard list is constructed, we can construct a dotted pair using the cons function. When the second argument supplied to the cons function is an atom (non-list data item), the cons function will return a dotted pair:

_$ (cons 1 2)
(1 . 2)

Constructing an Association List

In the above examples, the association lists were defined as literal expressions by a preceding apostrophe - this approach is fine for constant data, however, when the data is unknown and variable, the list must be constructed at run-time.

An association list may be constructed in the same way as a standard list or dotted pair: using the cons function. When the second argument supplied to the cons function is a list, the cons function will add (push) the first argument onto the front of the list:

_$ (cons 1 '(2 3 4 5))
(1 2 3 4 5)

In this way, we can construct an association list by repeatedly pushing dotted pairs onto a list:

_$ (setq alist (cons '(1 . 2) alist))
((1 . 2))
_$ (setq alist (cons '(3 . 4) alist))
((3 . 4) (1 . 2))
_$ (setq alist (cons '(5 . 6) alist))
((5 . 6) (3 . 4) (1 . 2))

In this example, the variable alist is initially undefined and hence the symbol alist holds no value and is null. However, since a nil value is equal to an empty list, the symbol alist is effectively a variable containing an empty list, which can be passed as an argument to the cons function and then redefined by the setq function.

Retrieving an Item from an Association List

As demonstrated earlier when retrieving the block name, to obtain an item from an association list we can use the assoc function:

_$ (assoc "Item 2" alist)
("Item 2" . "Value 2")

The assoc function will return the first item in the association list whose key matches the key provided.

Given the item returned by assoc (in this example, a dotted pair), we can obtain the value associated with the key using the cdr function:

_$ (cdr (assoc "Item 2" alist))
"Value 2"

Substituting an Item in an Association List

So far we have demonstrated how to construct a dotted pair, which will be used to record each block name and the associated quantity of the block in the selection; and how to construct an association list, which will be used to store the various dotted pairs.

Finally, it is necessary to understand how to substitute (and hence update) an item in an association list, so that the quantity associated with each block name in the association list may be incremented as the blocks are processed.

For this, we will use the subst function.

The subst function will substitute an item in place of all occurrences of another item in a given list (which does not necessary need to be an association list):

_$ (subst 10 2 '(1 2 3 4 3 2 1))
(1 10 3 4 3 10 1)

Therefore, we can use the subst function to substitute dotted pairs with incremented quantity values in place of existing dotted pairs in our association list. Since the block names will be unique within the association list, we do not need to worry about multiple items being substituted.

_$ (subst '("Block 3" . 2) '("Block 3" . 1) '(("Block 1" . 1) ("Block 2" . 5) ("Block 3" . 1)))
(("Block 1" . 1) ("Block 2" . 5) ("Block 3" . 2))

Building the Block Counter

Now, back to our block counter program -

Before we count the occurrences of each block, let us first demonstrate how we are going to construct the dotted pairs and association list which will record the block count data.

We can do this using the methods described above:

Select all
(defun c:myblockcounter ( / blk idx lst sel ) ;; Define function, declare local variables
    (if ;; If the following expression returns a non-nil value
        (setq sel ;; Assign the value returned by the following expression to the symbol 'sel'
            (ssget ;; Prompt the user to make a selection and return the selection set if successful
               '((0 . "INSERT")) ;; Filter the selection to block references only (INSERTs)
            ) ;; end ssget
        ) ;; end setq
        (repeat ;; Repeat the enclosed expressions the following number of times:
            (setq idx ;; Assign the value returned by the following expression to the symbol 'idx'
                (sslength sel) ;; Return the number of items in the selection set
            ) ;; end setq
            (setq blk ;; Assign the block name to the variable 'blk'
                (cdr ;; Retrieve the value associated with DXF group 2 (the block name)
                    (assoc 2 ;; Retrieve the DXF group 2 dotted pair from the following DXF data
                        (entget ;; Retrieve the list of DXF data for the following entity
                            (ssname sel ;; Retrieve the entity at the following index
                                (setq idx (1- idx)) ;; Decrement the index variable (since selection set indexes are zero-based)
                            ) ;; end ssname
                        ) ;; end entget
                    ) ;; end assoc
                ) ;; end cdr
            ) ;; end setq
            (setq lst ;; Redefine the 'lst' variable with the following updated list data
                (cons ;; 'Push' a new item onto the front of the list
                    (cons blk 1) ;; Construct a dotted pair whose first key is the block name and value is 1
                    lst ;; The list to which the item should be added (may be nil)
                ) ;; end cons
            ) ;; end setq
        ) ;; end repeat
    ) ;; end if
    (print lst) ;; Print the association list to the command-line (just for testing)
    (princ) ;; Suppress the return of the last evaluated expression (print)
) ;; end defun

Most importantly at this stage is to remember to declare our list variable lst as a local variable to our function c:myblockcounter - since the setq expression is redefining the lst variable with each iteration, it is vital that the value of this variable is null before the loop is evaluated, else the data will be compounded for every use of the function.

Upon testing the above program, we will receive an output similar to the following:

(("Block 1" . 1) ("Block 2" . 1) ("Block 1" . 1) ("Block 1" . 1) ("Block 2" . 1) ("Block 3" . 1))

Therefore we have now demonstrated how to construct a dotted pair for each block name, and how to build an association list containing such dotted pairs.

To construct the block counter, it only remains to test whether the association list already contains a dotted pair whose key is equal to the block name being processed, and if so, increment the associated value, else add the new dotted pair to the association list.

To achieve this, we will use the assoc and subst functions as described above:

Select all
(defun c:myblockcounter ( / blk idx itm lst sel ) ;; Define function, declare local variables
    (if ;; If the following expression returns a non-nil value
        (setq sel ;; Assign the value returned by the following expression to the symbol 'sel'
            (ssget ;; Prompt the user to make a selection and return the selection set if successful
               '((0 . "INSERT")) ;; Filter the selection to block references only (INSERTs)
            ) ;; end ssget
        ) ;; end setq
        (repeat ;; Repeat the enclosed expressions the following number of times:
            (setq idx ;; Assign the value returned by the following expression to the symbol 'idx'
                (sslength sel) ;; Return the number of items in the selection set
            ) ;; end setq
            (setq blk ;; Assign the block name to the variable 'blk'
                (cdr ;; Retrieve the value associated with DXF group 2 (the block name)
                    (assoc 2 ;; Retrieve the DXF group 2 dotted pair from the following DXF data
                        (entget ;; Retrieve the list of DXF data for the following entity
                            (ssname sel ;; Retrieve the entity at the following index
                                (setq idx (1- idx)) ;; Decrement the index variable (since selection set indexes are zero-based)
                            ) ;; end ssname
                        ) ;; end entget
                    ) ;; end assoc
                ) ;; end cdr
            ) ;; end setq
            ;; If the block is already recorded in the list:
            (if ;; If the following expression returns a non-nil value
                (setq itm ;; Assign the value returned by the following expression to the symbol 'itm'
                    (assoc blk lst) ;; Attempt to retrieve a list item whose first element is equal to the block name
                ) ;; end setq
                ;; Update the existing list entry:
                (setq lst ;; Redefine the 'lst' variable with the updated list data
                    (subst ;; Substitute the following list item in the list
                        (cons blk (1+ (cdr itm))) ;; Increment the number of occurrences recorded for this item in the list
                        itm ;; The existing item to be substituted
                        lst ;; The list in which to perform the substitution
                    ) ;; end subst
                ) ;; end setq
                ;; Else add a new entry to the list:
                (setq lst ;; Redefine the 'lst' variable with the following updated list data
                    (cons ;; 'Push' a new item onto the front of the list
                        (cons blk 1) ;; Construct a dotted pair whose first key is the block name and value is 1
                        lst ;; The list to which the item should be added (may be nil)
                    ) ;; end cons
                ) ;; end setq
            ) ;; end if
        ) ;; end repeat
    ) ;; end if
    (print lst) ;; Print the association list to the command-line (just for testing)
    (princ) ;; Suppress the return of the last evaluated expression (print)
) ;; end defun

To understand how this works, we can evaluate the expressions from the inside out for a single iteration of the loop:

Let us assume that at some point in the process the variable lst contains the following data:

(("Block 1" . 3) ("Block 2" . 1) ("Block 3" . 1))

Now let us observe what happens when the program encounters another block in the selection with block name Block 2 by evaluating the various expressions from the inside-out:

_$ (setq lst '(("Block 1" . 3) ("Block 2" . 1) ("Block 3" . 1)))
(("Block 1" . 3) ("Block 2" . 1) ("Block 3" . 1))
_$ (setq blk "Block 2")
"Block 2"
_$ (setq itm (assoc blk lst))
("Block 2" . 1)
_$ (cdr itm)
1
_$ (1+ (cdr itm))
2
_$ (cons blk (1+ (cdr itm)))
("Block 2" . 2)
_$ (subst (cons blk (1+ (cdr itm))) itm lst)
(("Block 1" . 3) ("Block 2" . 2) ("Block 3" . 1))

This is exactly how my Assoc++ function operates.


Upon testing the above program, we should now receive an association list detailing the quantities of each block in the selection:

(("Block 1" . 3) ("Block 2" . 2) ("Block 3" . 1))

Displaying the Results

As a finishing touch, rather than printing the raw association list AutoLISP data to the command-line, we can print the results to the command-line in a user-friendly format.

To achieve this, we can use a simple foreach loop to iterate over our association list and print the data held by each dotted pair:

Select all
(defun c:myblockcounter ( / blk idx itm lst sel ) ;; Define function, declare local variables
    (if ;; If the following expression returns a non-nil value
        (setq sel ;; Assign the value returned by the following expression to the symbol 'sel'
            (ssget ;; Prompt the user to make a selection and return the selection set if successful
               '((0 . "INSERT")) ;; Filter the selection to block references only (INSERTs)
            ) ;; end ssget
        ) ;; end setq
        (repeat ;; Repeat the enclosed expressions the following number of times:
            (setq idx ;; Assign the value returned by the following expression to the symbol 'idx'
                (sslength sel) ;; Return the number of items in the selection set
            ) ;; end setq
            (setq blk ;; Assign the block name to the variable 'blk'
                (cdr ;; Retrieve the value associated with DXF group 2 (the block name)
                    (assoc 2 ;; Retrieve the DXF group 2 dotted pair from the following DXF data
                        (entget ;; Retrieve the list of DXF data for the following entity
                            (ssname sel ;; Retrieve the entity at the following index
                                (setq idx (1- idx)) ;; Decrement the index variable (since selection set indexes are zero-based)
                            ) ;; end ssname
                        ) ;; end entget
                    ) ;; end assoc
                ) ;; end cdr
            ) ;; end setq
            ;; If the block is already recorded in the list:
            (if ;; If the following expression returns a non-nil value
                (setq itm ;; Assign the value returned by the following expression to the symbol 'itm'
                    (assoc blk lst) ;; Attempt to retrieve a list item whose first element is equal to the block name
                ) ;; end setq
                ;; Update the existing list entry:
                (setq lst ;; Redefine the 'lst' variable with the updated list data
                    (subst ;; Substitute the following list item in the list
                        (cons blk (1+ (cdr itm))) ;; Increment the number of occurrences recorded for this item in the list
                        itm ;; The existing item to be substituted
                        lst ;; The list in which to perform the substitution
                    ) ;; end subst
                ) ;; end setq
                ;; Else add a new entry to the list:
                (setq lst ;; Redefine the 'lst' variable with the following updated list data
                    (cons ;; 'Push' a new item onto the front of the list
                        (cons blk 1) ;; Construct a dotted pair whose first key is the block name and value is 1
                        lst ;; The list to which the item should be added (may be nil)
                    ) ;; end cons
                ) ;; end setq
            ) ;; end if
        ) ;; end repeat
    ) ;; end if
    ;; Print the results (if they exist)
    (foreach itm lst ;; For every 'itm' in the list given by 'lst'
        (princ ;; Print the following to the command-line
            (strcat ;; Concatenate the following strings
                "\n" ;; (New-line character)
                (car itm) ;; The block name
                ": " ;; An arbitrary separator for the data
                (itoa (cdr itm)) ;; The number of occurrences of the block, converted to a string
            ) ;; end strcat
        ) ;; end princ
    ) ;; end foreach
    (princ) ;; Suppress the return of the last evaluated expression (foreach)
) ;; end defun

You may have noticed that the foreach expression lies outside of the if statement, and so the list variable lst may be nil when the foreach expression is evaluated.

This is not a problem since, as noted above, a nil value may be interpreted as an empty list and so the foreach function will merely return nil without throwing an error if the list argument is empty.

The foreach expression could alternatively be included as part of the 'then' argument for the if function, which would also require the use of a progn expression so that multiple expressions may be evaluated, but still supplied as a single argument for the if function.

Testing the final program we will now receive an output similar to the following:

Block 1: 3
Block 2: 2
Block 3: 1

The Final Program

Reformatting the code and omitting the code comments from every line, we have the following final program:

Select all
(defun c:myblockcounter ( / blk idx itm lst sel )
    (if (setq sel (ssget '((0 . "INSERT"))))
        (repeat (setq idx (sslength sel))
            (setq blk (cdr (assoc 2 (entget (ssname sel (setq idx (1- idx)))))))
            (if (setq itm (assoc blk lst))
                (setq lst (subst (cons blk (1+ (cdr itm))) itm lst))
                (setq lst (cons  (cons blk 1) lst))
            )
        )
    )
    (foreach itm lst (princ (strcat "\n" (car itm) ": " (itoa (cdr itm)))))
    (princ)
)

I hope you have enjoyed following this tutorial and writing the program for yourself!

textsize

increase · reset · decrease

Designed & Created by Lee Mac © 2010