Ragged array grab bag
Ragged arrays are innovative technologies inside Clipper that solve some intriguing problems
Data Based Advisor
December 01, 1994 Gutierrez, Dan D.
In this issue's column, I'm going to try something a bit different. We'll examine some esoteric uses of one of the most innovative
technologies inside Clipper, namely ragged arrays. In doing so, I'll present functions that solve several intriguing problems in ways you
may not have guessed. First, I'll present the problem to be solved and then the solution. This way, you can test your problem-solving
abilities using Clipper by trying to solve the problems before peeking at the answers. Have fun!
Problem 1: Concatenation recursion
Let's say you want to traverse an arbitrary array structure (i.e., me that could contain sub-arrays) in an orderly fashion. Suppose
further, that each non-array element is of a character type. You need a function that accepts one array parameter and returns a single-
character parameter containing the concatenated values of all scalar array elements (i.e., elements that are not sub-arrays themselves).
Consider the following test program and ragged array function, AConCat() to solve this problem:
#include `common.ch'
PROCEDURE Main
* Define a ragged array with strange topology
LOCAL aRagged := { `Row', { `Row'}, (`Row', ;
`Your', {`Boat', `Gently', ;
{`Down', {`the', `Stream'}}}}}
* Displayed: * "Row Row Row Your Boat Gently Down the Stream"
? AConCat( aRagged, TRUE )
* Now trim down original ragged array
aRagged := ASIZE( aRagged, 2 )
* Displayed: "Row Row"
? AConCat( aRagged, TRUE )
RETURN
/***
*
* Function: AConCat ( , <1New> ) --> cConCat
*
* Purpose: Concatenate all scalar string elements.
*
* Parameters: aArray - Ragged array of any topology,
* all scalars must be char type.
* 1New - TRUE if this is a first time
* call, FALSE or NIL otherwise
* (when the function recurses)
*
* Returns: cConCat - Concatenated string. */
FUNCTION AConCat( aArray, lNew )
STATIC cConCat := “”
// Initialized only once
IF 1New != NIL // Fresh call,
cConCat := "” // re-init static
ENDIF
/* If a sub-array is encountered, dive deeper into the recursion; otherwise just display the scalar element's value.
*/
AEVAL( aArray, { | elem | IIF( VALTYPE ( elem ) == `A' ,;
AConCat( elem ) ,;
cConCat += elem ) } )
RETURN (cConCat)
The secret to aConCat() is its use of recursion, a relatively uncommon sub-program-calling technique. Recursion is the process whereby a
subprogram calls itself. Notice that in the body of AConCat() is a call to AConCat(). This is the only way a function can traverse a
completely arbitrary array structure. The function recurses whenever another sub-array is encountered during the traversal process.
Fortunately, the debugger handles recursion. You should run the above code under the debugger with the call stack turned on. This way,
you'll be able to witness the function issuing calls to itself.
For completeness, the function should check that each scalar element is indeed a character data type, otherwise a run time error results
during an attempt to concatenate a non-character element.
Problem 2: Orthogonal extraction
Next, we'll look at the case of having an array structure consisting of sub-arrays, which could in turn contain sub-arrays. Unlike the
ragged array in Problem 1, however, this array is uniform. For example, consider the array returned by the DIRECTORY() function. It
contains a series of sub-arrays that have the file information for a group of DOS files. The goal here is to develop a function that returns
an array with only the nth specified elements from each of the sub-arrays, or similarly the mth elements of the nth elements in the case
where the primary array has two levels of sub-arrays. This process could continue indefinitely, but we'll limit it to two levels. Here's the
solution to this problem:
#include `directry.ch'
#include `common.ch'
PROCEDURE Main
LOCAL aDirectory := DIRECTORY()
LOCAL aAllsizes := {}
LOCAL aVector := {}
LOCAL nSum := 0
* Define array with 2 levels of sub-arrays
LOCAL aSecond := { { 1, {2, 9}, 3} ,;
{ 2, {3, 9}, 4} ,;
{ 3, {4, 9}, 5} }
* Resulting array contains all file sizes
aAllSizes := AOrthog(aDirectory, F_SIZE)
AEVAL (aAllSizes, { |elem| nSum += elem})
? `Total of all files: ', nSum
* Resulting array: (9, 9, 9)
aVector := AOrthog( aSecond, 2, 2)
RETURN
/*** * Function: AOrthog() --> aOrthogonal
* Purpose: Generate an orthogonal single dimension
* array based on a ragged array.
* Parameters: aArray - Ragged array of a specific
* topology, namely all sub-arrays
* must have like dimensions.
* nElem1 - Element number to extract
* from subarray.
* nElem2 - Second order element number
* to extract
* Returns: aorthogonal - Resulting orthogonal array */
FUNCTION Aorthog( aArray, nElem1, nElem2 )
LOCAL aOrthogonal : = {}
IF ISNIL (nElem2)
AEVAL (aArray, { | elem | AADD (aOrthogonal,;
elem[ nElem1 ])})
ELSE
AEVAL (aArray, { | elem | AADD (aOrthogonal, ;
elem[ nElem1, nElem2])})
ENDIF
RETURN (aOrthogonal)
The first call to AOrthog() demonstrates its use to build an array containing only sizes of files in the current directory. In this case, you're
only using single-level sub-arrays, having the manifest constant F_SIZE (found in DIRECTRY.CH), whose value is 2, point to the desired
element. It's now easy to calculate the total of all file sizes with a single AEVAL().
The second example of AOrthog() uses a two-level, ragged array called aSecond. This time, you need to specify both the second and
third parameters of AOrthog()to point to both levels.
Problem 3: Ragged comparisons
This ragged array UDF, named AComp(), solves the very important and, at first look, somewhat difficult problem of comparing two
arrays for identical structure and values. Consider these two arrays: LOCAL aOne := {1, 2, 3 } LOCAL aTwo := {1, 2, {3} }
Are these arrays equivalent? They have the same scalar values, but their structures differ slightly: The third element of aOne is a
numeric value 3 and the third element of aTwo is an array consisting of a numeric value 3. There's a difference and AComp() catches it.
To be equivalent, two ragged arrays must have identical topologies and values.
This solution is simple if you look at the problem in the right light; namely you must use recursion again:
#include `common.ch'
PROCEDURE Main
LOCAL aArray1 := {`a', {'b', (1, 2), `c'}}
LOCAL aArray2 := {`a', `b', 1, 2, `c'}
LOCAL aArray3 := {`a', {'b', 1 , 2, `c'}}
LOCAL aArray4 := {`a', `b', {1, 3}, `c'}}
LOCAL aArray5 := {`a', {`b', {1, 2}, `c'}}
LOCAL aArray6
IF AComp( aArray1, aArray2 )
? `Arrays have equivalent structure and values'
ELSE
? `Arrays are not equivalent'
ENDIF
? AComp( aArray1, aArray3 )
? AComp( aArray1, aArray4 )
? AComp( aArray1, aArray5 )
aArray6 := aArray1
? AComp( aArray1, aArray6 )
RETURN
/*** * Function: AComp( , )
* --> 1Equivalent
* Purpose: Determine whether two ragged arrays are
* equivalent both in terms of structure and
* value.
* Parameters: aArray1 - First array
* aArray2 – Second array
*
* Returns: lEquivalent - TRUE: arrays are equivalent,
* FALSE otherwise
*
* Note: We assume that both parameters are passed,
* and are arrays. Furthermore, scalar array
* elements may only be Character, Numeric,
* Date, Logical or NIL. */
FUNCTION AComp( aArray1, aArray2 )
LOCAL nSize := LEN(aArray1)
LOCAL nCnt
LOCAL 1Equivalent := TRUE
* Check for identical array references
IF ! (aArray1 == aArray2)
IF nSize == LEN(aArray2)
FOR nCnt := 1 TO nSize
* Make sure elements are of same type
IF VALTYPE (aArray1 [nCnt]) == ;
VALTYPE (aArray2 [nCnt])
IF VALTYPE (aArray1 [ncnt]) == `A'
* Check if recursion is necessary.
IF ! (lEquivalent := ;
AComp (aArray1 [nCnt],;
aArray2 [nCnt]))
EXIT
ENDIF
ELSEIF (aArray1 [nCnt] == aArray2 [ncnt])
* Different values, stop comparing.
lEquivalent := FALSE
EXIT
ENDIF
ELSE
* Data types different, stop comparing.
lEquivalent := FALSE
EXIT
ENDIF
NEXT
ELSE
* Array structures different
lEquivalent := FALSE
ENDIF
ENDIF
RETURN (lEquivalent)
The output of this code is: Arrays are not equivalent .F. .F. .T. .T.
Let's analyze this output and see how the function works. The first and second calls to AComp() using the arrays aArray1, aArray2, and
aArray3 both return a F. since the arrays are equivalent in value but not in structure. The third call, using aArray1 and aArray4, returns a
.F. also, since the arrays are identical structurally but not in value. The fourth call, using aArray1 and aArray5 returns a T. because the
arrays are equivalent in every way. Finally, the last call yields a T. because AComp() handles same-reference array comparisons. Here,
aArray1 and aArray6 point to the same area of memory so they must be equivalent.
The function begins by treating the first array parameter as a base and then, using a recursive call back to AComp(), it traverses
this array's structure using VALTYPE() to see if another sub-array was encountered requiring another depth of recursion.
Problem 4: Pure manipulation
The last example represents somewhat of a mental exercise that I encountered in a real-life programming problem. Let's say you have a
two-dimensional array containing all numeric elements and you need to replace all zero-valued elements with some other value, say a
logical .T. value. You need a general-purpose, ragged array function to perform this task. Furthermore, let's say you can only use code
blocks to solve the problem. The function AZero(), below, is one possible solution. Try to solve it on your own before looking at the code
and use the definition of the array aInt here for test data:
#include `common.ch'
PROCEDURE main
LOCAL aInt := { { 0, 1, 0, 2}, ;
{ 7, 0, 0, 0}, ;
{ 8, 3, 4, 0} }
AZero( aInt, TRUE )
RETURN
/***
*
* Function: AZero( , --> NIL
*
* Purpose: Change "zeros" of the matrix to vValue
*
* Parameters: aArray - Two dimensional array of
* numerics
* vValue - Value with which to replace
* all zeros.
* Note: Since aArray is passed By Reference (default)
* actual parameter in caller will be modified. */
FUNCTION aZero( aArray, vValue )
LOCAL nRow := 0, nCol := 0
/* The following code blocks handle the traversal of the two-dimensional array. */
LOCAL bBlock1 := {|elem_r| nRow++, nCol := 0, ;
AEVAL (elem_r, bBlock2)}
LOCAL bBlock2 := {|elem_c| nCol++, ;
IF(elem_c == 0, aArray [nRow, nCol];
:= vValue, NIL) }
* Evaluate nested code block
Aeval (aArray, bBlock1)
* Use debugger to verify array contents at this
* point.
RETURN NIL
The key to the above solution is two-nested code blocks executed by an AEVAL(). The first block, bBlock1, is EVALed and passes each
sub-array element to a second AEVAL(), which executes the second block, bBlock2. It is bBlock2 that receives the scalar array elements
and does the assignment of the new value to the elements. You need two variables, nRow and nCol, to keep track of the position within
the array that the AEVAL()s are accessing so you can reference the zero elements in the assignment statement embedded inside the
nested block.
You don't need to pass the array back as a parameter since the default technique for passing arrays in Clipper is By Reference.
Summary
I hope you've had fun figuring out the answers to these ragged array problems. If any of the solutions seem cryptic, bear in mind they're
using Clipper features that make it an elegant and robust programming language. You'll get good at coming up with this type of solution
with a bit of practice.
Contributing Editor Dan D. Gutierrez is president of AMULET Consulting, a Los Angeles-based xBase development firm. He
teaches xBase and Clipper programming at UCLA and is the author of Clipper5.2: Step By Step. He also provides on-site
Clipper and Microsoft Access training.
http://www.accessmylibrary.com/coms2/summary_0286-9297318_ITM