Session FT-

You Need Arrays



Tamar E. Granor, Ph.D.

Email: tamar_granor@compuserve.com





Overview



Visual FoxPro's arrays are amazingly useful and powerful. This session will examine the functions that let you manipulate arrays, the many functions that use arrays to provide information about both VFP and the broader environment in which an application is operating, and explore the many ways arrays can be used in application development and in building developer tools. It will also address the performance of arrays and look at issues in using arrays in COM development. Special attention will be paid to changes in VFP 7 that affect arrays. This session assumes familiarity with VFP fundamentals.





Array Basics

An array is an organized collection of data. It lets you call a number of variables by a single name. Each item in the array, called an element, is identified by its position. While some programming languages require all elements of an array to be of the same type, in FoxPro, there is no such rule. Every array element can be of a different type.

Individual elements of an array are referenced by following the name of the array with either square brackets or parentheses, enclosing the position of the element. (These notes use the square bracket notation.) Visual FoxPro (and FoxPro before it) offers one-dimensional (1-D) and two-dimensional (2-D) arrays. To specify an element of a 2-D array, the row and column positions are separated with a comma.

Internally, all FoxPro arrays are represented as one-dimensional, which means that you can access the elements of a 2-D array as if it were 1-D. Two-dimensional arrays are stored in row-major order. That is, all the elements from the first row are stored before the elements from the second row, and so forth. (The alternative to row-major order, used by some programming languages, is column-major order, in which all the elements of the first column are stored before all the elements of the second column.) So, when you create a 6x3 array in VFP, it can be seen as a one-dimensional array with 18 elements. The first 3 are those from row 1, then the next three are those from row 2, and so on.

Internally, a 1-D array is seen as a single row of elements, so while you can access the elements as if it were 2-D without error, it's not terribly meaningful. In fact, it can be confusing, because changing the row index doesn't affect the result. VFP only looks at the column index in this case.

Defining arrays

Several commands let you create arrays. The identical DIMENSION and DECLARE commands are designed specifically for array definition. They create the new array as a private variable (except when used from the Command Window, where they create public variables). For example:

DIMENSION aTest1D[3], aTest2D[7,5]

creates two arrays. aTest1D is a one-dimensional array with 3 elements, while aTest2D is a two-dimensional array with 7 rows and 5 columns.

To create local arrays, use the LOCAL keyword, but specify the dimensions of the array. There's an optional ARRAY keyword for those who prefer to be explicit. Without the ARRAY keyword, array and scalar (non-array) variables can be created with a single LOCAL statement. When the ARRAY keyword is specified, only arrays can be defined with that LOCAL statement. For example, the following commands are all valid:

LOCAL ARRAY aLocal[3]

LOCAL cName, aLocal2D[5,2]

LOCAL aLocal1D[25], nValue

but these are not:

LOCAL dToday, ARRAY aNoGo[17]

LOCAL ARRAY aNoGo[17], dToday

The PUBLIC command, not surprisingly, creates public variables, and like LOCAL, can be used to define arrays. It has the same optional ARRAY keyword and the same restrictions on its use. Think twice, however, about creating any public variables, arrays included. Public variables can lead to hard-to-find errors. In general, the best idea is to make all variables local unless you have a specific need for a larger scope.

Arrays are limited to 65,000 elements. The number of elements in a two-dimensional array is computed by multiplying rows by columns. When you attempt to create an array with more than 65,000 elements, you get the error message "Too many variables."

Creating array properties

When you need an array to be available to multiple methods of an object (whether it's a form, a control or some other kind of object), the best solution is to make the array a property of that object. Defining array properties is simple, once you know how.

In coded classes, to add an array property, use DIMENSION or DECLARE in the definition portion of the class. For example, this (fairly useless) class has a three-element array and a method to report the type of the specified element of the array's:

DEFINE CLASS ArrayDemo AS Custom


DIMENSION aMyArrayProperty[3]


PROCEDURE ArrayElementType

LPARAMETERS nElement


* Report the element's type

WAIT WINDOW TYPE("This.aMyArrayProperty[nElement]")

RETURN


ENDDEFINE

In the Class Designer or Form Designer, use the New Property dialog to add an array property. The secret is to specify the dimensions for the array in the dialog. Figure 1 shows the creation of an array property in the New Property dialog.

Figure 1 Adding array properties – The New Property dialog lets you add array properties like any other, but you must specify the dimensions of the array.

Redefining arrays

FoxPro allows you to redefine an array once it's been created. Use DIMENSION or DECLARE and specify the new size of the array. Any data already in the array remains in the same element. If you change a 1-D array to a 2-D array, the existing data fills the cells in row-major order. This makes sense if you remember that internally, all arrays are one-dimensional.

If you resize an array so that it's larger than it was before, the new elements are added at the end. The new elements have an initial value of .F. Similarly, if you resize an array so that it's smaller than it was before, the extra elements are removed from the end.

Here are some examples:

LOCAL aExample[ 17 ] && results in a 17-element array

DIMENSION aExample[6, 3] && adds an element at the end and makes the array 2-D

DIMENSION aExample[6, 2] && reduces the array to 12 elements. The last 6 (the

&& last two rows in the previous arrangement) are

&& removed and the remaining elements are

&& redistributed to fill 2 columns.

Assigning array data

Array elements are accessed like any other variable, except that you must specify the particular element you want. So, for example, to save "This is a test" to the third element of aTest, use:

aTest[3] = "This is a test"

It's also possible to store the same value to all elements of an array with one command. To set the entire array aTest to "This is a test", use:

aTest = "This is a test"

However, there's a trap lurking here. Setting all elements of an array with a single assignment statement works only if SET COMPATIBLE is OFF or FOXPLUS (which are identical settings). If COMPATIBLE is set to ON or DB4, a statement like the last example destroys the array and creates a new variable with the same name and scope, assigning it the specified value. (Note that this trap affects only variables, not array properties.)

While it's easy enough to avoid changing SET COMPATIBLE programmatically, there's one further trap. In the General page of the Options dialog (figure 2), there's an innocuous looking checkbox labeled "dBASE compatibility". Checking this box is the same as issuing SET COMPATIBLE ON.

Figure 2 Avoiding SET COMPATIBLE ON – The selected checkbox in this page of the VFP's Options dialog changes the setting of COMPATIBLE, which affects assignment statements to entire arrays (and a host of other things).

There's one further consideration when working with array properties. As the ArraySize method in the Creating Array Properties section shows, to refer to an array property, you need to provide the appropriate object reference. In a method of the class to which the property belongs, using THIS is sufficient.

Checking for an array

To check whether a particular variable is an array, use the TYPE() function and examine the first element. If the variable is an array, TYPE() returns the type of that element. If the variable is not an array, TYPE() returns "U" for "unknown." For example:

LOCAL aArray[4]

? TYPE("aArray[1]") && returns L because newly created elements contain .F.

? TYPE("cScalar[1]") && returns "U" because cScalar is not an array

Moving data between arrays and tables

Several commands are designed to provide quick ways of transferring data from tables to arrays and back. (In this context, "table" can usually mean a table, view or cursor.)

Starting with the most commonly used of these command, arrays are one of the possible destinations for the SQL SELECT command. If the specified array doesn't exist, it's created (with public scope if you're working in the Command Window or with private scope in a program). If the array already exists, it's redimensioned and retains its current scope. This example puts the name and birth date of all employees located in the UK into an array. (The table is drawn from Visual FoxPro's example TasTrade database.)

SELECT first_name, last_name, birth_date ;

FROM _SAMPLES+"TasTrade\Data\Employee" ;

WHERE country = "UK" ;

INTO ARRAY aUKEmps

The newly created array has 5 rows and 3 columns.

SCATTER and GATHER let you copy data from a single record to an array and back to the record. To use an array as the destination for the SCATTER command, add the TO keyword and the name of the array. Similarly, to collect data from an array, add the FROM keyword to GATHER. This example retrieves the first record from the Employee table, changes a few things, then restores the array data to the record

USE _SAMPLES+"TasTrade\Data\Employee"

SCATTER TO aOneRecord

aOneRecord[2] = "Jones-Buchanan"

aOneRecord[4] = "Sales Manager Extraordinaire"

GATHER FROM aOneRecord

USE

As the example shows, working with record data this way is complicated because you need to know which element of the array represents which field in the table. In FoxPro 2.x, SCATTER and GATHER were commonly used to provide data buffering.

The other pair of commands that move data between tables and arrays is COPY TO ARRAY and APPEND FROM ARRAY. Unlike SCATTER and GATHER, these commands can move data from more than one record at a time. Whether they do or not depends on the array definition. When the specified array is one-dimensional, a single record is transferred. When the array is two-dimensional, one record is moved for each row of the array.

Both commands (like SCATTER and GATHER) let you specify which fields are involved in the data transfer. You can provide either an explicit field list using the FIELDS clause or use the LIKE and EXCEPT clauses to specify a field skeleton. COPY TO ARRAY also lets you limit the records copied using the usual Xbase Scope, FOR and WHILE clauses.

This example performs exactly the same operation as the SQL SELECT above. It copies the name and birth date of all UK employees into an array:

USE _SAMPLES+"TasTrade\Data\Employee"

COPY TO ARRAY aUKEmps ;

FIELDS First_Name, Last_Name, Birth_Date ;

FOR Country = "UK"

Although SQL SELECT can do everything that COPY TO ARRAY can, COPY TO ARRAY is as much as an order of magnitude faster with small data sets. However, the time to execute COPY TO ARRAY appears to increase with the size of the table, as well as with the size of the result set.

There's also a problem using COPY TO ARRAY with large data sets. If the data set is larger than the 65,000-element limit for arrays, you must define the array before the COPY TO ARRAY. Otherwise, the command fails. However, when you use COPY TO ARRAY with an array that already exists, the array is not redimensioned. The command fills only the number of elements provided. So, to successfully use COPY TO ARRAY with a large data set, you must COUNT the number of matching records first and dimension the array appropriately.

There's also an important difference between COPY TO ARRAY and SELECT INTO ARRAY when you're working with buffered data. SELECT draws its results from the original tables, while COPY TO uses the buffers. When you need to create an array based on buffered data, COPY TO is the way to go.

Finally, it's worth noting that SELECTing into a cursor rather than an array is faster than either of the methods of creating an array. With large result sets (my test extracted about 20,000 records from a table containing more than a million), SELECTing into a cursor is an order of magnitude faster. So, unless an array is absolutely necessary, when dealing with large record sets, use a cursor instead.

APPEND FROM ARRAY also supports the FOR clause. Each array element is evaluated as if it had already been added to the table. If the condition is true, the record is added; if the condition is false, the record is not added. This example takes the data in aUKEmps and copies it into a cursor:

CREATE CURSOR UKEmps (cFirst C(10), cLast C(20), dBirthDate D)

APPEND FROM ARRAY aUKEmps

Of course, this example could be accomplished in a single SQL SELECT. The only difference is that the cursor created here is read-write and, by default, a cursor created by SQL SELECT is read-only. However, in VFP 7, that's no longer an issue because of the addition of a READWRITE clause to SQL SELECT.

However, the approach of creating a cursor and using APPEND FROM ARRAY is very handy when dealing with the various functions that examine the environment and put their results into an array. You can take the results and move them to a cursor, where you can then use all of VFP's processing power.

Finally, there's the array version of INSERT INTO. This command can add one or many records stored in an array to a table. If the array is one-dimensional, the command adds a single record. If it's two-dimensional, the command adds one record for each row of the array. The syntax is:

INSERT INTO Table FROM ARRAY Array

Like APPEND FROM ARRAY, this command can be combined with any of the ones that copy data from an array to move records from one table to another. Here's an example using SQL SELECT:

SELECT first_name, last_name, birth_date ;

FROM _SAMPLES+"TasTrade\Data\Employee" ;

WHERE country = "UK" ;

INTO ARRAY aUKEmps


CREATE CURSOR EmpNames (cFirst C(10), cLast C(20), dBirthdate D)

INSERT INTO EmpNames FROM ARRAY aUKEmps

As with APPEND FROM ARRAY, you're more likely to do things this way when you need to manipulate the records before moving them, or when you're copying from one table to an existing table, rather than to a newly-created cursor. INSERT INTO FROM ARRAY is also handy for moving the information in arrays created by the "A" functions discussed below into cursors or tables.

There is one big difference between APPEND FROM ARRAY and INSERT INTO FROM ARRAY. INSERT INTO can populate a memo field, while APPEND FROM cannot.

Moving data from strings to arrays

The ALINES() function, added in VFP 6, lets you move character strings into arrays, breaking them up into lines, so that each array element contains one line of the original string. In VFP 7, ALINES() has been enhanced to let you decide what characters indicate the end of a line.

The syntax for ALINES() is:

nLines = ALINES( Array, cString, [, lTrim ] [, cSeparator, … , cSeparator ] )

The function returns the number of lines in the resulting array. As is generally true for FoxPro functions that put something into an array, if the array already exists, it's resized. If the array doesn't exist, it's created.

If you pass only an array name and a string (which can be a memo field), CHR(13), CHR(10) or a combination of the two are considered to mark the end of a line. The line separator is not placed in the array.

The optional lTrimIt parameter lets you strip both leading and trailing blanks from each line.

The cSeparator parameter(s), added in VFP 7, let you expand the horizons of this function. You can list one or more characters that indicate the end of a line. Even when you specify your own line separators, the default CHR(13) and CHR(10) are still interpreted as ending a line. So the characters you specify are added to the list. As in the default case, the separator character that ends a line is not put into the array.

One handy way to use ALINES() is to break up delimited lists. For example, given a list like "red, orange, yellow, green, blue, purple", you can put each color in an array element. In VFP 7, use this code:

cColorList = "red, orange, yellow, green, blue, purple"

nColorCount = ALINES( aColors, cColorList, .T., ",")

In VFP 6, it's necessary to change the commas into one of the accepted separators before calling ALINES():

cColorList = "red, orange, yellow, green, blue, purple"

nColorCount = ALINES( aColors, STRTRAN(cColorList,",",CHR(13)), .T.)

ALINES() has one unusual feature. You can omit the lTrim parameter and begin specifying separators with the third parameter. For example, the RowSource of a combo with RowSourceType = 1 is a comma-separated list. Any blanks that occur are part of the item. To parse such a list, you can write:

nItems = ALINES( aItems, This.RowSource, "," )

Passing arrays as parameters

Arrays must be passed to functions and procedures by reference. When an array is passed by value, the routine receives only the first element.

Since the default for procedures is to receive parameters by reference, no special action is needed in that case. That is, when a routine is called using the DO command, all parameters are passed by reference, including arrays.

By default, parameters to functions are passed by value. (Although the default can be changed with SET UDFPARMS, that's not a good idea.) To pass a function parameter by reference, precede it with "@".

This program demonstrates the various options:

* Demonstrate the ways of passing an array

LOCAL aArray[3]

LOCAL cResult


WAIT WINDOW "Calling routine as procedure (using DO)"

DO PassArray WITH aArray, cResult

WAIT WINDOW cResult


WAIT WINDOW "Calling routine as function without @"

cResult = PassArray(aArray)

WAIT WINDOW cResult


WAIT WINDOW "Calling routine as function with @"

cResult = PassArray(@aArray)

WAIT WINDOW cResult


RETURN


PROCEDURE PassArray

* This procedure receives an array parameter.


LPARAMETERS aParm, cReturn


IF TYPE("aParm[1]") = "U"

cReturn = "Not an array"

ELSE

cReturn = "Array"

ENDIF


RETURN cReturn

Array properties cannot be passed as parameters, except to built-in functions. To pass an array, say from one method to another, you need to use ACOPY() (discussed in "Copying Arrays" below) to copy the data to a local array, then pass that array to the other method. If you need changes made by the other method to be restored to the array property, use ACOPY() again after the method call. Here's the structure:

PROCEDURE MyMethod


* whatever happens before the call to the other method


ACOPY( This.aArrayProperty, aPlaceHolder )

This.MyOtherMethod( @aPlaceHolder )

ACOPY( aPlaceHolder, This.aArrayProperty)


* whatever happens after the call to the other method


ENDPROC

Passing arrays to COM components raises some special issues. See the section "Arrays and COM" for the details.

Returning array values

In VFP 6 and earlier versions, there's no way to directly return an array from a function. VFP 7 adds this ability, provided the array is in scope after the function returns. In practice, this means you can return an array from a method if the array is a property of that method's object. (You can also return a non-property array if you declare it sufficiently high in the scope chain. However, doing so violates good programming practices since the function that returns the array must reference a variable that it didn't create or receive as a parameter.)

To return an array, you must precede it with "@" in the return statement, as if it were a parameter.

This code demonstrates returning an array. The RGBComp method of the class receives an RGB value as a parameter and breaks it down into its components, storing them in an array, which it returns. This function does the same thing as the RGBComp() function in FoxTools, but that function handles the need to return three values by accepting them as parameters by reference.

#DEFINE CR CHR(13)


LOCAL nColor, aRGBColor[3], oRGB AS Colors


* Get a color

nColor = GETCOLOR()


oRGB=CREATEOBJECT("Colors")

aRGBColor = oRGB.RGBComp(nColor)


MESSAGEBOX("The color is " + TRANSFORM(nColor) + "." + CR + ;

"Red: " + TRANSFORM(aRGBColor[1]) + CR + ;

"Green: " + TRANSFORM(aRGBColor[2]) + CR + ;

"Blue: " + TRANSFORM(aRGBColor[3]), ;

"Show array return value", 64)


RETURN


DEFINE CLASS Colors AS Custom

* Color handling code

DIMENSION aRGB[3]


FUNCTION RGBComp(nColor) AS Array

* RGBComp

* Returns the Red, Green and Blue Components

* of a color in an array


This.aRGB[1] = -1

This.aRGB[2] = -1

This.aRGB[3] = -1


IF VARTYPE(nColor)="N"

This.aRGB[3] = INT(nColor/(256^2))

nColor = MOD(nColor,(256^2))

This.aRGB[2] = INT(nColor/256)

This.aRGB[1] = MOD(nColor,256)

ENDIF


RETURN @This.aRGB


ENDDEFINE

In versions prior to VFP 7, you have to simulate returning an array. One solution is to create an object with an array property and return the object. Here's the same class rewritten to work in VFP 6. The additional class, ArrayReturn, has a single custom property, to hold the return value.

#DEFINE CR CHR(13)


LOCAL nColor, oRGB, oReturn


* Get a color

nColor = GETCOLOR()


oRGB=CREATEOBJECT("Colors")

oReturn = oRGB.RGBComp(nColor)


MESSAGEBOX("The color is " + TRANSFORM(nColor) + "." + CR + ;

"Red: " + TRANSFORM(oReturn.aReturnValue[1]) + CR + ;

"Green: " + TRANSFORM(oReturn.aReturnValue[2]) + CR + ;

"Blue: " + TRANSFORM(oReturn.aReturnValue[3]), ;

64, "Show array return value")


RETURN


DEFINE CLASS Colors AS Custom

* Color handling code

DIMENSION aRGB[3]


FUNCTION RGBComp(nColor) AS Array

* RGBComp

* Returns the Red, Green and Blue Components

* of a color in an array


This.aRGB[1] = -1

This.aRGB[2] = -1

This.aRGB[3] = -1


IF VARTYPE(nColor)="N"

This.aRGB[3] = INT(nColor/(256^2))

nColor = MOD(nColor,(256^2))

This.aRGB[2] = INT(nColor/256)

This.aRGB[1] = MOD(nColor,256)

ENDIF


oReturn = CreateObject("ArrayReturn")

DIMENSION oReturn.aReturnValue[3]

ACOPY(This.aRGB, oReturn.aReturnValue)


RETURN oReturn


ENDDEFINE


DEFINE CLASS ArrayReturn AS Line

* Use Line because it's light-weight


DIMENSION aReturnValue[1]


ENDDEFINE

Array-filling functions

Visual FoxPro has quite a few functions whose purpose is to fill an array with certain data. One such function, ALINES(), is discussed in "Moving data from strings to arrays" above. The others are described below.

These functions all have some behaviors in common (except for exceptions noted below). First, they either resize or create the array, so that it's the right size for the data involved. That is, if the array exists, the function resizes it; if the array doesn't exist, the function creates it and makes it the right size. In addition, these functions return the number of rows or elements in the resulting array.

Array Manipulation

Visual FoxPro has a number of functions for working with arrays. A few change the array's contents, but most of them let you explore the array.

Determining array size

The ALEN() function tells you how big an array is. Depending how you call it, it can return the total number of elements, the number of rows or the number of columns. To determine the total number of elements in an array, call ALEN() and pass only the array, like this:

LOCAL aTest1D[ 12 ], aTest2D[ 7, 5 ]

? ALEN( aTest1D ) && returns 12

? ALEN( aTest2D ) && returns 35

To find the number of rows, add a second parameter of 1. For one-dimensional arrays, the result here is the same as if you'd omitted the second parameter. This is misleading since FoxPro views a 1-D array as a single row.

? ALEN( aTest1D, 1) && returns 12

? ALEN( aTest2D, 1) && returns 7

Passing 2 for the second parameter causes the function to return the number of columns. For a 1-D array, ALEN( Array, 2) returns 0. Again, this is misleading, but it also provides a way to determine whether an array is declared as one-dimensional or two-dimensional.

? ALEN( aTest1D, 2 ) && returns 0

? ALEN( aTest2D, 2 ) && returns 5

lIsOneD = ( ALEN( aTest1D, 2) = 0 )

Converting between element notation and row, column notation

As explained in "Array Basics", all FoxPro arrays are represented internally as one-dimensional. You can reference two-dimensional arrays using either a single element number or a row and a column. A pair of functions, AELEMENT() and ASUBSCRIPT(), convert between the two notations.

AELEMENT() takes an array, a row and a column (the latter is optional) and returns the element number. If the array is one-dimensional, the function returns the original row number. Interestingly, it does the same thing if only a row is passed – in this case, most likely, the array is being seen as 1-D and the second parameter is being interpreted as an element number.

Here are some examples:

DIMENSION aTest2D[ 13, 5 ]

? AELEMENT( aTest2D, 2, 4 ) && returns 9

? AELEMENT( aTest2D, 7, 1 ) && returns 31

? AELEMENT( aTest2D, 13 ) && returns 13

ASUBSCRIPT() performs a nearly inverse operation. It converts an element number to a row or a column. Because a function can return only a single item, it takes two calls to ASUBSCRIPT() to find both the row and the column.

This function has three parameters: the array, the element number and either 1 or 2 to indicate which subscript to return. Pass 1 to get the row, 2 to get the column. ASUBSCRIPT() works only on two-dimensional arrays – if you pass a one-dimensional array, you get an error.

Here are some examples:

DIMENSION aTest2D[ 13, 5 ]

? ASUBSCRIPT( aTest2D, 9, 1 ) && returns 2

? ASUBSCRIPT( aTest2D, 9, 2 ) && returns 4

? ASUBSCRIPT( aTest2D, 20, 1) && returns 4

? ASUBSCRIPT( aTest2D, 20, 2) && returns 5

ASUBSCRIPT() has a bug in versions prior to VFP 7 – it can't handle arrays with more than 32,767 rows. Of course, the only such arrays you can create have only one column. But, in that case, when you ask for the row number of an element beyond 32767, ASUBSCRIPT() returns 32767. This bug is fixed in VFP 7.

With VFP 7's new ability to return an array, we can write a function to return both the row and the column in a single array. The function is defined as a method of a class and the array is a property of the class. Here's the code for the class:

DEFINE CLASS cusArrayFns AS Custom


DIMENSION aReturn[2]


FUNCTION ABothSubscripts

* Return both subscripts of an array

* in an array


LPARAMETERS aInArray, nElement


* First, check parameters

DO CASE

CASE PCOUNT() <> 2

* Did we get two params?

ERROR 1229

RETURN .T.

CASE VARTYPE(aInArray) = "U"

* Does the array variable exist?

ERROR 255

RETURN .F.


CASE TYPE("aInArray[1]") = "U"

* Is it an array?

ERROR 232

RETURN .F.


CASE ALEN( aInArray, 2 ) = 0

* Is it a 2-D array?

ERROR 1234

RETURN .F.

CASE VARTYPE(nElement) <> "N"

* Did we get an element number?

ERROR 11

RETURN .F.

CASE NOT BETWEEN( nElement, 1, ALEN(aInArray))

* Is the subscript valid for this array?

ERROR 1234

RETURN .F.


OTHERWISE

* So far, so good


ENDCASE


* Use the built-in functions to get the information

* and put it into the array.

This.aReturn[1] = ASUBSCRIPT( aInArray, nElement, 1 )

This.aReturn[2] = ASUBSCRIPT( aInArray, nElement, 2 )


RETURN @This.aReturn


ENDDEFINE

To use the function, we need to instantiate the class and then call the method:

LOCAL aTest[ 7, 5], aResult[ 2 ], oArrayFns


* Instantiate the class

oArrayFns = NewObject( "curArrayFns", "ArrayFns.PRG" )


* Call the method

aResult = oArrayFns.aBothSubscripts( @aTest, 15 )

Examining aResult in this case shows the row as 3 and the element as 5.

Adding and removing array data

Two functions let you manipulate the contents of an array. AINS() inserts a blank element, row or column at a specified position. ADEL() deletes the data from an element, row or column at a specified position.

Both AINS() and ADEL() adjust the data following the specified position appropriately. That is, AINS() moves the remaining elements closer to the end of the array, pushing the last one(s) out. ADEL() moves the remaining elements toward the front of the array, leaving the last one(s) empty. In most cases, you want to resize the array before calling AINS() and after calling ADEL().

While insertion and deletion combined with resizing work as you'd expect for rows, that's not so for columns. When you resize a 2-D array to add or remove columns, data is moved from one row to another. When you resize, then insert a column, data is lost. The same thing happens when you delete a column, then resize. (Of course, some data is discarded when you delete a column, but when you resize, additional data is lost.)

The syntax for AINS() is:

AINS( Array, nPos [, 2 ])

The interpretation of nPos is determined by the array and by the presence or absence of the third parameter. If the array is one-dimensional, nPos indicates an element. If the array is two-dimensional and the third parameter is omitted (or anything less than 2), nPos indicates a row. If the array is two-dimensional and the third parameter is 2, nPos indicates a column.

First, here's an example for one-dimensional arrays:

LOCAL aTest1D[ 7 ]

LOCAL nItem


FOR nItem = 1 TO ALEN( aTest1D )

* Set each element to the corresponding letter of the alphabet

aTest1D[ nItem ] = CHR(ASC("A") + nItem - 1)

ENDFOR


* Now add a new fourth item

AINS( aTest1D, 4 )


* Show result – fourth element is .F. and "G" is missing

FOR nItem = 1 TO ALEN( aTest1D )

? aTest1D[ nItem ]

ENDFOR

?


* Give the new element a value

aTest1D[ 4 ] = "New!"


* This time, enlarge the array first

DIMENSION aTest1D[ ALEN( aTest1D ) + 1]


* Now add a new second item

AINS( aTest1D, 2)


* Show result – second element is .F.

FOR nItem = 1 TO ALEN( aTest1D )

? aTest1D[ nItem ]

ENDFOR

?

This example shows insertion of rows and columns in a two-dimensional array.

LOCAL aTest2D[ 3, 7 ]

LOCAL nRow, nCol, nRowCount, nColCount


nRowCount = ALEN( aTest2D, 1)

nColCount = ALEN( aTest2D, 2)


* Fill array with element numbers

FOR nRow = 1 TO nRowCount

FOR nCol = 1 TO nColCount

aTest2D[ nRow, nCol ] = (nRow-1) * nColCount + nCol

ENDFOR

ENDFOR


* Display initial data

FOR nRow = 1 TO nRowCount

?

FOR nCol = 1 TO nColCount

?? aTest2D[ nRow, nCol ], " "

ENDFOR

ENDFOR

?


* Add a new second row

AINS( aTest2D, 2 )


* Display result - second row is .F. and values above 15 are gone

FOR nRow = 1 TO nRowCount

?

FOR nCol = 1 TO nColCount

?? aTest2D[ nRow, nCol ], " "

ENDFOR

ENDFOR

?


* Reset to initial data

FOR nRow = 1 TO nRowCount

FOR nCol = 1 TO nColCount

aTest2D[ nRow, nCol ] = (nRow-1) * nColCount + nCol

ENDFOR

ENDFOR


* This time, resize array first

DIMENSION aTest2D[ nRowCount + 1, nColCount ]

nRowCount = nRowCount + 1


* Now insert a new second row

AINS( aTest2D, 2 )


* Display result - second row is .F., but no data is lost

FOR nRow = 1 TO nRowCount

?

FOR nCol = 1 TO nColCount

?? aTest2D[ nRow, nCol ], " "

ENDFOR

ENDFOR

?


* Refill array with element numbers

FOR nRow = 1 TO nRowCount

FOR nCol = 1 TO nColCount

aTest2D[ nRow, nCol ] = (nRow-1) * nColCount + nCol

ENDFOR

ENDFOR


* Now add a column

DIMENSION aTest2D[ nRowCount, nColCount + 1 ]

nColCount = nColCount + 1


* Display result – data has been rearranged

FOR nRow = 1 TO nRowCount

?

FOR nCol = 1 TO nColCount

?? aTest2D[ nRow, nCol ], " "

ENDFOR

ENDFOR

?


* Insert a new fifth column

AINS( aTest2D, 5, 2 )


* Display result - fifth column is .F., and data has been lost

FOR nRow = 1 TO nRowCount

?

FOR nCol = 1 TO nColCount

?? aTest2D[ nRow, nCol ], " "

ENDFOR

ENDFOR

?

The last section of the code demonstrates the problem with inserting entire columns. Data is moved from one row to another to fill in empty spaces when the array is redimensioned. Then, when the column is inserted, some data falls out and is lost.

The syntax for ADEL() syntax is identical to AINS(), except for the name of the function:

ADEL( Array, nPos [, 2 ] )

Here's an example for a one-dimensional array:

LOCAL aTest1D[ 7 ]

LOCAL nItem


FOR nItem = 1 TO ALEN( aTest1D )

* Set each element to the corresponding letter of the alphabet

aTest1D[ nItem ] = CHR(ASC("A") + nItem - 1)

ENDFOR


* Now delete the fourth item

ADEL( aTest1D, 4 )


* Show result – "D" is missing and the last item is .F.

FOR nItem = 1 TO ALEN( aTest1D )

? aTest1D[ nItem ]

ENDFOR

?

Here's the two-dimensional example:

LOCAL aTest2D[ 5, 7 ]

LOCAL nRow, nCol, nRowCount, nColCount


nRowCount = ALEN( aTest2D, 1)

nColCount = ALEN( aTest2D, 2)


* Fill array with element numbers

FOR nRow = 1 TO nRowCount

FOR nCol = 1 TO nColCount

aTest2D[ nRow, nCol ] = (nRow-1) * nColCount + nCol

ENDFOR

ENDFOR


* Display initial data

FOR nRow = 1 TO nRowCount

?

FOR nCol = 1 TO nColCount

?? aTest2D[ nRow, nCol ], " "

ENDFOR

ENDFOR

?


* Delete the second row

ADEL( aTest2D, 2 )


* Display result - values 8-14 are gone and last row is .F.

FOR nRow = 1 TO nRowCount

?

FOR nCol = 1 TO nColCount

?? aTest2D[ nRow, nCol ], " "

ENDFOR

ENDFOR

?


* Now, resize the array

DIMENSION aTest2D[ nRowCount - 1, nColCount ]

nRowCount = nRowCount - 1


* Display result - values 8-14 are gone and array has only 5 rows

FOR nRow = 1 TO nRowCount

?

FOR nCol = 1 TO nColCount

?? aTest2D[ nRow, nCol ], " "

ENDFOR

ENDFOR

?


* Refill array with element numbers

FOR nRow = 1 TO nRowCount

FOR nCol = 1 TO nColCount

aTest2D[ nRow, nCol ] = (nRow-1) * nColCount + nCol

ENDFOR

ENDFOR


* Delete the fifth column

ADEL( aTest2D, 5, 2 )


* Display result - last column is .F., and 5th column data is gone

FOR nRow = 1 TO nRowCount

?

FOR nCol = 1 TO nColCount

?? aTest2D[ nRow, nCol ], " "

ENDFOR

ENDFOR

?


* Now resize the array

DIMENSION aTest2D[ nRowCount, nColCount-1 ]

nColCount = nColCount - 1


* Display result - only 6 columns, data has been rearranged

* and some items have been lost

FOR nRow = 1 TO nRowCount

?

FOR nCol = 1 TO nColCount

?? aTest2D[ nRow, nCol ], " "

ENDFOR

ENDFOR

?

Again, the final example demonstrates the problems with manipulating columns. The deletion behaves as you expect, but when you resize the array to the smaller size, data is rearranged rather than just cutting out the empty final column.

Copying arrays

The ACOPY() function provides a quick and easy way to copy data from one array to another, but it also provides far more because the source and destination arrays don't have to match in size and shape. ACOPY() even lets you copy data from one place to another in the same array. The syntax for ACOPY() is:

nNumberCopied = ACOPY( SourceArray, DestArray

[, nSourceStart [, nNumberToCopy

[, nDestStart ] ] ] )

Perhaps the key to understanding what ACOPY() does is knowing that it treats all arrays as one-dimensional and, therefore, works at the element level. It just barely knows about rows and columns.

In the simplest case, you provide just the names of the source and destination arrays and data is copied. If the destination doesn't exist, it's created to exactly match the source. (Note that it's created as a private variable.) If the array exists and has a different shape (that is, a different number of rows or columns or both), data is copied element by element. If the destination is too small, an error is generated after the data has been copied.

If you specify nSourceStart, copying starts with that element. Similarly, if you specify nNumberToCopy, only that many elements are copied. Specifying these items lets you see one really strange behavior of ACOPY(). When the destination array is created, it always has exactly the same dimensions as the source array, even if you're only copying some of the source data.

Finally, nDestStart lets you indicate where to put the results in the destination array.

Here are some examples:

LOCAL aTest2D[ 5, 7 ]

LOCAL nRow, nCol, nRowCount, nColCount


nRowCount = ALEN( aTest2D, 1)

nColCount = ALEN( aTest2D, 2)


* Fill array with element numbers

FOR nRow = 1 TO nRowCount

FOR nCol = 1 TO nColCount

aTest2D[ nRow, nCol ] = (nRow-1) * nColCount + nCol

ENDFOR

ENDFOR


* Copy everything

ACOPY( aTest2D, aCopy2D )


nRowCount = ALEN( aCopy2D, 1)

nColCount = ALEN( aCopy2D, 2)


* Display copied data

FOR nRow = 1 TO nRowCount

?

FOR nCol = 1 TO nColCount

?? aCopy2D[ nRow, nCol ], " "

ENDFOR

ENDFOR

?


* Copy two rows

ACOPY( aTest2D, aTwoRows, 8, 14 )


nRowCount = ALEN( aTwoRows, 1)

nColCount = ALEN( aTwoRows, 2)


* Display copied data - note that array is 5 x 7, not 2 x 7

FOR nRow = 1 TO nRowCount

?

FOR nCol = 1 TO nColCount

?? aTwoRows[ nRow, nCol ], " "

ENDFOR

ENDFOR

?


* Specify start in destination

* Copy first row of source to 3rd row of destination

ACOPY( aTest2D, aTwoRows, 1, 7, 15 )


* Display copied data

FOR nRow = 1 TO nRowCount

?

FOR nCol = 1 TO nColCount

?? aTwoRows[ nRow, nCol ], " "

ENDFOR

ENDFOR

?

Due to its element-oriented nature, ACOPY() has one major limit. It's unable to copy columns. To do that, you have to write code. This fu