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. .