TYPE() to StringType()--Bridging a Gap 

This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.

TYPE() to StringType()—Bridging a Gap

Pradip Acharya

Intuitively, you'd think that the string "123" represents a number and the string "ABC" is of type character. Right? Not according to the standard TYPE() function in VFP. If ABC happens to be a date variable, for example, the type of "ABC" will be returned as D. Yet, in many situations, we need to know the data type of the contents of an unknown string. In this article, Pradip Acharya looks at the problems associated with determining the data type of a string and describes the creation of a new function, StringType(), suited for this purpose.

One of my customers called and said, "When I type an X in your program, I get an error message, and nothing happens. Am I doing something wrong?" I rushed over and discovered that, as always, the customer was right, and I needed to create a new function to get the customer going again.

A generic text input box was presented to the user for entering a search parameter for scanning a specific column in a table for the presence of such a value. For example, if the user needs to look at the invoices created on a certain date, a date will be typed in. To look for all five HP motors, one would enter a numeric value of "5" and so on. I had validation code that ensured that if the user entered data that clearly wasn't of the same type as the field to be scanned, he or she would be notified and asked to re-enter the value. For a date type field, the user incorrectly entered an X. My validation, for some reason, failed to alert the user, and an unhandled data type mismatch error led to the previously noted disruption of work.

Dual use of the TYPE() function

Simply put, the TYPE() function in VFP accepts as input a character string and determines whether a memory variable or a field exists by that name and, if so, what its data type is. However, this description is deceptive and limiting. More strictly, TYPE() resolves the string as an expression and checks to see whether the result makes any sense.

To make sure that the slate is clean:

  CLOSE DATBASES ALL
RELEASE ALL EXTENDED 

Now let's define a memory variable:

  m.xyz = {01/01/2001}

?TYPE("XYZ") will display D. In this mode, the TYPE() function determined the data type of a prevailing memory variable (in scope). Next, if I do ?TYPE("123"), I'll get N. Obviously, in this second instance, my intention has been to use the TYPE() function to check the nature of a string and not the existence of a variable, field, object, or property.

In trying to use the TYPE() function for the two unrelated purposes, we fall into a trap. In the case study presented earlier, if the user types 123, I'll correctly determine that the type of the value entered is numeric. If, however, the user types XYZ instead of the desired data type C, I'll incorrectly determine the data type to be D because a date variable already exists by the name of XYZ. This was the reason behind the failure of my validation code that then led to a data type mismatch error. The target table field was of type date; the user typed a character instead of a date, and I incorrectly inferred that a date had been typed in because a date variable by that name existed and TYPE() fooled me.

To make matters worse, if the user enters a perfectly valid date such as 07/25/2001 (assuming that the date format is set to MDY), I'll incorrectly determine that the data type of the string is N and not D. Why N? Because 07/25/2001 evaluates to 0.0001399 as an expression, having interpreted each "/" as a division operator—unintuitive indeed! What this proves is that using the TYPE() function to determine the data type of the content of a string is a misapplication of the TYPE() function. I created a new function, StringType(), to determine the type of data contained within a string, based on a string parsing strategy, and at the same time to internally take advantage of the TYPE() function.

Function StringType()

The purpose of this function is to determine the type of data contained in a text string that's passed as the only argument. Case is immaterial, and leading and trailing white space, not just spaces, are ignored (see Table 1). White space is defined in Listing 1. (Note that there's no U for undefined category.)

****

Table 1. The function returns a single-character uppercase letter.

L

Logical

N

Numeric

I

Integer

Y

Currency (for example, $53.27)

D

Date, either strict or conforming to the prevailing date format, but independent of SET CENTURY

C

Not one of the above, character, default

****

Listing 1. Code for function StringType().

  #DEFINE WHITESPACE CHR(32)+CHR(0)+ ;
                   CHR(9)+CHR(10)+CHR(11)+ ;
                   CHR(12)+CHR(13)+CHR(26)
**      horizontal tab      9
**      linefeed            10
**      vertical tab        11
**      formfeed            12
**      enter (CR)          13
**      ctrl+Z              26

FUNCTION StringType
LPARAMETERS m.p1cString
    PRIVATE ALL
    if .NOT.(TYPE("m.p1cString") == "C")
**most unlikey, but just in case**
        return TYPE("m.p1cString")
    endif
    LOCAL m.lcTrimmed, m.lcVarType, ;
          m.liI, m.liJ, m.luTemp
**Unequivocally, eliminate leading and**
**trailing white space**
    m.liI = 0
    for m.liJ = 1 to LEN(m.p1cString)
        if .NOT.(SUBST(m.p1cString, m.liJ, 1) ;
                 $ WHITESPACE)
            m.liI = m.liJ
            EXIT
        endif
    endfor
    if m.liI = 0    && nothing in the string
        return "C"
    endif
    for m.liJ = LEN(m.p1cString) to m.liI STEP -1
        if .NOT.(SUBST(m.p1cString, m.liJ, 1) ;
                 $ WHITESPACE)
            EXIT
        endif
    endfor
    m.lcTrimmed = UPPER(SUBST(m.p1cString, ;
                        m.liI, m.liJ-m.liI+1))
**If it's a name of something,**
**it must be of type character**
    m.luTemp = LEFT(m.lcTrimmed, 1)
    if BETWEEN(m.luTemp, "A", "Z") or ;
              (m.luTemp == "_")
        return "C"
    endif
**Money ?**
    if m.luTemp == "$"
        if TYPE(STUFF(m.lcTrimmed,1,1,"")) == "N"
            return "Y"
        endif
        return "C"
    endif
**Logical ?**
    if (m.luTemp == ".").AND. ;
       (RIGHT(m.lcTrimmed, 1) == ".")
        if (m.lcTrimmed == ".T.") or ;
           (m.lcTrimmed == ".F.") or ;
           (m.lcTrimmed == ".TRUE.") or ;
           (m.lcTrimmed == ".FALSE.")
            return "L"
        endif
        return "C"
    endif
**If enclosed in {},**
**it's type D (valid or not)**
    m.liI = .F.
    m.liJ = .F.
    m.lcVarType = TYPE(m.lcTrimmed)
    if m.lcVarType == "D"
        return "D"
    endif
    m.liJ = 0
    if empty(EVALUATE("{"+m.lcTrimmed+"}"))
**Looks like empty date -**
**accept only // or {}**
        for m.liI = 1 to LEN(m.lcTrimmed)
            m.luTemp = SUBST(m.lcTrimmed, m.liI, 1)
            if m.luTemp $ WHITESPACE
                LOOP
            endif
            if not (m.luTemp == "/")
                liJ = 0
                EXIT
            endif
            m.liJ = m.liJ + 1
            if m.liJ > 2
                EXIT
            endif
        endfor
    else && Looks like a date -      **
**&& check for proper format**
        if LEFT(m.lcTrimmed, 1) == "^"
            return "D"
        endif
        for m.liI = 1 to LEN(m.lcTrimmed)
            if BETWEEN(SUBST(m.lcTrimmed, ;
                       m.liI, 1), "0", "9")
                LOOP
            endif
            m.liJ = m.liJ + 1
            DO CASE
            case m.liJ = 1
                m.luTemp = SUBST(m.lcTrimmed, ;
                                 m.liI, 1)
            case m.liJ = 2
                if .NOT.(m.luTemp == ;
                    SUBST(m.lcTrimmed, m.liI, 1))
                    m.liJ = 0
                    EXIT
                endif
            other
                EXIT
            ENDCASE
        endfor
    endif
    if m.liJ = 2
        return "D"
    endif
**Undefined means Character**
    if m.lcVarType $ "UMG"
        return "C"
    endif
**If numeric, is it an integer?**
    if (m.lcVarType == "N")
        m.luTemp = EVALUATE(m.lcTrimmed)
        if m.luTemp = ROUND(m.luTemp, 0)
            return "I"
        endif
    endif
return m.lcVarType

This function can be useful in any situation where an unknown string is encountered and one needs to determine what kind data it contains prior to taking further action. In addition to Listing 1, the file STRTYPE.PRG is available in the Download file. Table 2 presents a comparison of the output.

****

Table 2. Comparison of output.

String

TYPE()

StringType()

".T."

L

L

".False."

U

L

"123.52"

N

N

"123"

N

I

"$53.68"

U

Y

"01/01/01"

N

D

"//"

U

D

"123abc"

U

C

"m.aVarName"

Depends

C

An empty string

An empty string or a string made up of white space isn't interpreted. The function returns "C." You may reserve a word BLANK, for example, and ask the user to enter BLANK if a distinction is to be made between no value entered and an intended empty value. Then test for "BLANK" on return. This isn't done inside the StringType() as supplied, although you might wish to incorporate such a test inside the code yourself and assign a special return character—perhaps E.

Data type logical

The function will return L if the input string is one of the following:

.T.

.F.

.True.

.False.

The inclusion of the last two is an extension.

Data types numeric and integer

Normally, the function will return "N" if the string contains a number. As an extension, it will return "I" if the value entered is truly an integer. "123" and "123." will both return "I". The distinction between "N" and "I" might be useful, for example, prior to SEEKing an integer field of Invoice numbers. Under special circumstances, strings containing character data might be incorrectly identified as numeric in the presence of embedded operators. See the "Limitation" section later in the article.

Data type currency

Since parsing is involved, we might as well make a special case out of numeric data when the leading character is a $ sign. For example, "$ 52.68" will return Y instead of N. Probably no one will use this option.

Data type date

Correctly determining a string to be of type date is a vexatious problem. The problem is split into two parts. What's an empty date? And what's a valid date? If the input string is enclosed in curly brackets—that is, {…}—the result returned is always "D" regardless of the validity of what lies inside the brackets. In keeping with VFP convention, an input string like "{abc}" will return a value of "D."

Only two character representations are recognized by StringType() as a blank date:

  • • //
  • • {}

In-between white space is ignored. Therefore, /ss/ or { ss} will also return "D".

As for valid dates, internally, the TYPE() function is put to use. The problem is that VFP is highly forgiving in interpreting dates. For example, ?{4.7} will print as 04/07/2001, whereas, for our purposes, we'd like to interpret 4.7 as a numeric value and certainly not a date. Accordingly, reasonable parsing constraints have been introduced in StringType() before a string can be declared to be a date. For example, there must be two and only two identical separator characters, and the rest must be digits. Dates entered in the strict date format will also be correctly identified as date.

Limitation—no check for operators

In this version of StringType(), no attempt has been made to isolate an individual data item from an expression with embedded operators. "53.86" and "2+7" both will return N (or I). Should we interpret a string such as 27*3 as "C" or "N"? I don't know. Furthermore, StringType() normally doesn't depend on which work areas are open. Not checking for operators leaves a loophole in this regard. An input string such as "53 * Invoice.Total" will produce unpredictable output depending on whether Invoice.Total is a visible field or not. If you code a version that checks for operators and expressions and closes this loophole, I'll be happy to get a copy.

A wish

TYPE() as it stands identifies a valid property of an object. If TYPE("m.oMyObject.Size") returns "U" or "O," it's not a valid property. Otherwise, the property exists. As a logical and consistent extension to this interpretation, it makes sense if TYPE() also identifies an unprotected method and returns, for example, "H" if a method by this name exists for the object. I believe this generalization will be useful.

Conclusion

The standard TYPE() function will return the data type of a variable but isn't suitable for determining the data type of the contents of a string. The new function StringType() has been designed specifically for this purpose, with a few limitations. In a future article, I'll present a utility for generic output formatting of any type of value into a printable character string.

To find out more about FoxTalk and Pinnacle Publishing, visit their website at http://www.pinpub.com/html/main.isx?sub=57

Note: This is not a Microsoft Corporation website. Microsoft is not responsible for its content.

This article is reproduced from the September 2001 issue of FoxTalk. Copyright 2001, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. FoxTalk is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call 1-800-493-4867 x4209.

© Microsoft Corporation. All rights reserved.