INLIST and ASCAN—Kicking Them Up a Couple of Notches

Pradip Acharya

While VFP is primarily a case-independent language, functions such as INLIST and ASCAN are exceptions to the rule. Furthermore, the match criterion is dependent on the setting of EXACT, and datatype mismatches can arise. Foolproof use of these functions requires tedious and repetitive coding. In this article, Pradip Acharya demonstrates the striking code simplification and improved reliability that can result from using the extended versions of these routines. Also included are an extended equality checker and a more practical ADEL.

Who's afraid of INLIST? Me, me. I dreaded using INLIST and ASCAN because of the inherent pitfalls. First, INLIST is subject to datatype mismatch (while ASCAN isn't). Second, for character data, there are two uncertainties to take care of—namely, case sensitivity and the setting of EXACT. In common use, I don't care about the case just as I don't for variable or file names, and I don't care about partial match from the left. If I'm looking for one of APPLE, orange, or Peach, I shouldn't have to write 10 lines of code or keep inserting UPPER(ALLTRIM(m.MyVar)) in order to call INLIST without fear.

Take Genmenu.prg that comes with the distribution. The code suffers from precisely this affliction. I looked at my cumbersome code and concluded that there must be a better way to call INLIST, ASCAN, or ADEL without fear and with ease. The problem extends to determining the equality of two variables as well. The fact that 123 is not equal to "ABC" is intuitively obvious. Why then do I get an error?

Similarly, for practical purposes, PrePaid is equivalent to PREPAID. Time and again, I wish to remove one item from a dropdown list. I can't call ADEL because I don't know the position in the array. I need to call ASCAN first, which is a circus. After I overcome this hurdle, I'm left with a redundant .F. value at the end, which means I have to manually redimension the array.

The solution to all this lies in four extended functions—xINLIST, xASCAN, xADEL, and xEQ. I was amazed at the code simplification that resulted after I swept my application clean by substituting these new functions. But that isn't all. Greater reliability is another bonus. For example, suppose a procedure name is stored in a memo field. If there's a redundant linefeed, UPPER(ALLTRIM()) alone won't perform the desired trimming prior to checking for equality. You have no such worry if you use any of these new functions. The source code for these four functions is available in the Download file. In order to set up for testing, unzip the contents and type in:

SET PROCEDURE TO XINLIST ADDITIVE

Extended INLIST

For practical purposes, it's virtually impossible to accomplish the following with INLIST:

OnOff = xINLIST(m.MyVar, "Yes", 1, .T., "Y", "ON", ;

"T", ".t. ", "True")

With xINLIST, as you can see, it's a breeze. What more need be said?

A typical call to INLIST involves codes such as this (see Genmenu.prg):

PRIVATE SaveExact

SaveExact = SET("EXACT")

SET EXACT ON

If INLIST(UPPER(ALLTRIM(m.MyVar1)), ;

UPPER(ALLTRIM(m.ListItem1)), ;

UPPER(ALLTRIM(m.ListItem2)))

….

endif

If m.SaveExact == "OFF"

SET EXACT OFF

endif

Now suppose either m.ListItem1 or m.ListItem2 isn't of type Character; you'll need another 10 lines of code to work around the problem of datatype mismatch. You can replace the entire block of code with:

If xINLIST(m.MyVar1, m.ListItem1, m.ListItem2)

….

endif

As an added bonus, you need not worry about datatype mismatches as well as the presence of any extra linefeeds and so forth in any of the strings, which translates to greater reliability.

xINLIST is called with the same arguments as regular INLIST and is independent of case, datatype, and Set Exact setting. Character strings are automatically hyper trimmed. For a match, lengths must be equal. Apart from the candidate, you can have up to 26 list items (see Listing 1).

Listing 1. The xINLIST function.

FUNCTION xINLIST

LPARAMETERS ;

p1, p2, p3, p4, p5, p6, p7, p8, p9, ;

p10,p11,p12,p13,p14,p15,p16,p17,p18, ;

p19,p20,p21,p22,p23,p24,p25,p26,p27

local npar, i, temp, item, NullFlag, p1type

npar = PARAMETERS()

if ISNULL( m.p1 )

return NULL

endif

p1type = VARTYPE(m.p1)

if m.p1type $ "CM" & Character branch

temp = UPPER(xTRIM( m.p1 ))

for i = 2 to m.npar

item = EVAL("m.p" + ALLTRIM(STR(m.i)))

DO CASE

case ISNULL( m.item )

NullFlag = NULL

case (VARTYPE( m.item ) $ "CM") and ;

(UPPER(xTRIM( m.item )) == m.temp)

return

ENDCASE

endfor

else & Other data types

for i = 2 to m.npar

item = EVAL("m.p" + ALLTRIM(STR(m.i, 2)))

DO CASE

case ISNULL( m.item )

NullFlag = NULL

case (VARTYPE( m.item ) == m.p1type) and ;

(m.item = m.p1)

return

ENDCASE

endfor

endif

return m.NullFlag

Equality 101

If i1 and i2 are integers and c1 and c2 are character strings, then the opposite of (i1 = i2) is (i1 != i2). For Character data, the opposite of (c1 == c2) isn't (c1 != c2) but !(c1 == c2). Why? Because (c1 != c2) depends on the setting of EXACT. This pitfall alone can lead to erroneous coding. To illustrate this point, try the following:

SET EXACT OFF

?"xxx" == "xx"& displays .F.

?"xxx" != "xx"& also displays .F.

The extended equality checker xEQ circumvents these problems and doesn't generate any datatype mismatch error if the arguments are, for example, 123 and "ABC". xEQ is a special case of xINLIST with just two arguments. Note that like xINLIST, xEQ can return NULL if either argument is NULL. It's interesting that xEQ() will return .T. because the two implied arguments are both .F., and xEQ(1) will return .F. because the implied second argument is .F., which isn't equal to 1 (see Listing 2).

The power xEQ is illustrated by the following:

x="APPLES"

y="apples" + CHR(13)

?UPPER(ALLTRIM(m.x)) == ;

UPPER(ALLTRIM(m.y)) & wrong answer .F.

?xEQ(m.x, m.y) & correct answer .T.

?xEQ(123, "ABC") & correct answer .F.

Listing 2. The xEQ function.

FUNCTION xEQ & returns .T., .F. or Null

LPARAMETERS p1arg, p2arg

if ISNULL( m.p1arg ) or ISNULL( m.p2arg )

return NULL

endif

local temp

temp = VARTYPE( m.p1arg )

if not (m.temp == VARTYPE( m.p2arg ))

return .f.

endif

if m.temp $ "CM"

return (UPPER(xTRIM(m.p1arg)) == ;

UPPER(xTRIM(m.p2arg)))

endif

return (m.p1arg = m.p2arg)

Extended ASCAN

Unlike a xINLIST, xASCAN features an additional fifth argument that isn't present in the standard ASCAN. The fifth argument p5atc (see Listing 3) means that in determining a character type match, apply the ATC functionality of a case-insensitive sliding match anywhere. For example, this will determine that a match exists if PrePaid is found anywhere in one array element:

xASCAN(@m.MyArray, "PREPAID",,,"Slide")

xASCAN is also independent of datatypes, case, and the setting of EXACT. Note the difference in passing the array reference between ASCAN and xASCAN:

ASCAN(MyArray, "LookFor")

xASCAN(@m.MyArray, "LookFor")

Although we're focusing on strings, xASCAN works equally well with all types of data. There's no checking for NULL values (see Listing 3).

Listing 3. The xASCAN function.

FUNCTION xASCAN & no check for NULL values

LPARAMETERS p1array, p2what, p3i1, p4ntot, p5atc

external array p1array

local i, i1, i2, vType, LookFor, temp

i2 = ALEN( p1array )

DO CASE & set up start and end elements

case empty( m.p3i1 )

i1 = 1

case m.p3i1 > m.i2

return 0

other

i1 = m.p3i1

ENDCASE

if not empty( m.p4ntot )

temp = m.i1 + m.p4ntot - 1

if m.temp < m.i2

i2 = m.temp

endif

endif

vType = VARTYPE( m.p2what )

if m.vType $ "CM"

LookFor = UPPER(xTRIM( m.p2what ))

endif

DO CASE

case not (m.vType $ "CM")

for i = m.i1 to m.i2

if (VARTYPE(p1array( m.i )) == m.vType) and ;

(p1array( m.i ) = m.p2what)

return m.i

endif

endfor

case empty( LookFor )

for i = m.i1 to m.i2

if (VARTYPE(p1array( m.i )) $ "CM") and ;

empty(xTRIM(p1array( m.i )))

return m.i

endif

endfor

case empty( m.p5atc )

for i = m.i1 to m.i2

if (VARTYPE(p1array( m.i )) $ "CM") and ;

(UPPER(xTRIM(p1array( m.i ))) == m.LookFor)

return m.i

endif

endfor

other

for i = m.i1 to m.i2

if (VARTYPE(p1array( m.i )) $ "CM") and ;

(ATC(m.LookFor, p1array( m.i )) > 0)

return m.i

endif

endfor

ENDCASE

return 0

Pass by reference VFP bug

In passing any array to any function, you'll fall prey to a low-level addressing bug in VFP, including version 7. This applies to calling xASCAN as well. The bug is shown in Listing 4.

Listing 4. Pass by reference VFP bug.

dimension MyVar( 3 )

create table temp (MyVar L)

Proc1( @MyVar ) & GENERATES BOGUS ERROR:

** Function argument value, type or count is invalid

Proc Proc1

lpar aList

extern array aList

return

The & indirection prefix is handled correctly by VFP but not the @ reference prefix. VFP knows that & can only apply to memory variables, not to table fields. For the @ prefix, exactly the same criterion applies. Instead, VFP gets confused in the presence of a table field by the same name.

The solution is to always attach m. prefix to the name. There will be no error generated in the previous example if you do:

Proc1( @m.MyVar )

However, the problem doesn't end there. Essentially, you can't pass one single element of an array by reference without risk at any time. For example, the following syntax is rejected when it shouldn't be:

Proc1( @m.MyVar( 2 ))

The only safe workaround to passing a single element by reference is to do the following:

Local temp

Temp = MyVar( 2 )

Proc1( @m.temp)

MyVar( 2 ) = m.temp

Such low-level bugs—and this isn't the only one—lead to accidental coding and sudden, unexplained failures in distributed VFP applications.

Extended ADEL

xADEL bears little resemblance to the standard ADEL. For example, the second argument isn't the element number, but a match criterion for locating the element to delete. A character type match is independent of datatype, case, or the setting of exact. The third argument p3atc (see Listing 5) means, when determining a character type match, apply the ATC functionality of a case-insensitive sliding match anywhere. For example, this will delete the first element found to contain the string Deliver anywhere in it:

xADEL(@m.MyArray, "DELIVER", "Slide")

Note the difference in passing the array reference between ADEL and xADEL:

ADEL(MyArray, 5)

xADEL(@m.MyArray, "LookFor")

Although we're focusing on strings, xADEL works equally well with all types of data. There's no checking for NULL values. xADEL automatically eliminates the trailing .F. value after deleting, and returns .T. if a match was found and delete carried out, else returns .F. xADEL is ideally suited to deleting one item from a drop down list (see Listing 5).

Listing 5. The xADEL function.

FUNCTION xADEL & Search, destroy and adjust

lpar ap1, p2LookFor, p3atc

external array ap1

local i

i = xASCAN(@m.ap1, m.p2LookFor,,,p3atc)

if empty( m.i )

return .f.

endif

ADEL(ap1, m.i)

if ALEN( ap1 ) > 1

dime ap1(ALEN( ap1 ) - 1)

else

dime ap1( 1 )

ap1( 1 ) = .f.

endif

return

Conclusion

The extended functions xINLIST and xASCAN take away the fear and awkwardness associated with using the standard INLIST and ASCAN. Using these functions leads to striking code simplification and boosts reliability. xADEL automatically searches for the element to delete in the array, before deleting, and removes the trailing .F. after deleting. xEQ determines the equality of two variables without regard to datatype, case, the setting of EXACT, or the presence of extraneous characters. Together, these functions can reduce and streamline coding to a significant degree.

Download 09ACHASC.ZIP

Pradip Acharya is a developer and consultant based in Toronto. Prior to setting up Gencom Software Inc. in 1992, he worked for British Petroleum. An area Pradip specializes in is writing DLLs and FLLs in C to complement VFP capabilities, when required. In recent years, he delivered major systems to companies such as Steelcase Canada and Billiton Metals. .