Tuesday, June 14, 2011

How shall we proceed Functions and Procedures ?

In prior versions of FoxPro, applications were limited to a single active procedure file at any time. This meant that all user-defined functions and commonly used procedures were kept in a single file. With Visual FoxPro, the ability to have multiple procedure files active at any given time provides much more flexibility. Such procedures can now be grouped logically, by functionality, into different procedure files that can be loaded incrementally as needed. The downside, of course, is that procedure files are loaded into memory by Visual FoxPro and held until explicitly released. This may not be the best use of that precious resource.

Fortunately, it is also possible to define procedure classes. The individual functions that would previously have been kept in a procedure file can now become methods of a class. This approach has the benefit that, when defined visually in the Class Designer, all the functions in the procedure can be neatly viewed under the methods tab of the property sheet. Procedure classes containing specific functionality can be dropped onto forms that require this functionality. A second major benefit is that a procedure class can be sub-classed - for those special situations when standard functionality must be augmented.

The approach that we like is a combination of these two approaches. We recommend using a procedure file for truly generic functions (i.e. those that are used, unchanged, by many different applications). For example, our procedure file contains a NewID function for generating surrogate primary keys, a SetPath function to set the path for the application and a few key functions that Visual FoxPro should have, but doesn't. Two examples of such functions are the Str2Exp and Exp2Str functions (used later in this chapter These functions, as the names imply, are used to convert character strings into the values of another specified data
type and vice versa.


Separate Procedure Classes contain application specific functionality. For example, an accounting application might require several functions to calculate tax and invoice totals. The Accounting Procedures Class can be dropped onto any form that requires these functions – or can be instantiated as an Application Level object. Clearly, such functions are not generic, nor are they even required by the entire application. By grouping them into a single procedure class, we can make this functionality available at the specific parts of the application that require it without compromising the rest of the application.

Parameters (an aside)
We have all used parameters extensively in FoxPro but in Visual FoxPro’s object oriented environment, parameters have taken on a new importance as the principle means of implementing messaging – the very lifeblood of an OO application! (We have more to say on this subject later!)

By reference, by value?
Parameters are passed either by reference or by value. When a parameter is passed to a function or procedure by reference, any changes made to its value in the called code are reflected in the original value in the calling program. Conversely when a parameter is passed by value, the called code can change that value but the value in the calling program remains unchanged. Visual FoxPro interprets the code being called by the mechanism by which parameters are passed. So when the calling syntax looks like this:
luRetVal = CallMyFunction( param1, param2 )
Visual FoxPro treats this as a Function Call and passes the parameters by value. However if the same code is called like this:
DO CallMyFunction WITH param1, param2
then Visual FoxPro treats this as a Procedure Call and passes the parameters by reference.

The old coding rule that a “Function must always return a value” is not really true in Visual FoxPro, but it does make sense when the calling syntax is considered.

You can change this default behavior in two ways. One way is to:
SET UDFPARMS TO REFERENCE or SET UDFPARMS TO VALUE


However, we do not consider this a good idea because it affects the way all functions in your entire application handle the parameters they are passed. (It is never a good idea to use a global solution to solve a local problem). In this case there is a simple solution because parameters can be passed by value explicitly just by enclosing them in parentheses. Thus:
DO CallMyFunction WITH (param1), (param2)



passes the parameters by value, even though the syntax used would normally cause them to be passed by reference. To pass parameters explicitly by reference, simply preface the parameter with the “@” symbol. (This is, by the way, the only way to pass an entire array to a procedure, function or method). So we could also make our function call and pass its parameters by reference like this:
luRetVal = CallMyFunction( @param1, @param2 )



How do I know what was passed?
There are two ways for a function to determine how many parameters were passed to it. The
PARAMETERS() function returns the number of parameters that were passed to the most recently called function or procedure. This can give unexpected results since it is reset each time a function or procedure is called. Most importantly, it is also reset by functions that are not called explicitly, such as ON KEY LABEL routines.

A better way of determining how many parameters were passed to a function is to use the PCOUNT() function. This always returns the number of parameters that were passed to the currently executing code. Save yourself a lot of grief and unnecessary hair pulling by always using PCOUNT() for determining the number of parameters passed.

How should I position my parameters?
The best advice is that if a function takes optional parameters, you should place these at the end of the parameter list. PCOUNT() can then be used in the function to determine whether or not the optional parameters were passed allowing the function to take the appropriate action.

You can take advantage of the fact that Visual FoxPro always initializes parameters as logical false. By setting up your function to expect a logical false as its default, you can invoke the function without passing it any parameters. Then, in those cases where you want an alternative behavior, just invoke the function by passing it a logical true.

How can I return multiple values from a function?
Of course, returning values is simply the reverse of passing parameters - with one gotcha!. While you can easily pass multiple parameters to a function, there is no obvious mechanism for returning multiple values! The RETURN command only allows a single value to be passed back to the calling program.

One solution is to pass multiple values as a comma-delimited string. This is a little messy, however, as you will need to convert your values into character format to build the return string and then parse out the individual values again in the receiving code.

Another possibility is to define all the values you want populated by the function as Private variables in the calling program. As such, they will be available to any function or procedure that is called subsequently and they can be populated directly. However, this is neither specific nor easy to maintain and is not really a good solution.

A better possibility is to create an array in the calling code for the return values and then pass that array by reference. The called function can then simply populate the array and the values will also be available in the calling program. This is workable, at the very least, and was probably the most common method of handling the issue prior to the introduction of Visual FoxPro.

Once again Visual FoxPro has made life a lot easier. Returning multiple values from a UDF is easy if you create, and use, a parameter class. Ours is based on the Line baseclass and is named xParameters. You can find it in the CH02.VCX class library. All it needs is one custom array property, aParameters, to hold the return values and this line of code in its INIT():
LPARAMETERS taArray
ACOPY(taArray, This.aParameters)

The user-defined function can then simply populate its own local array with the values it needs to return and create the parameter object on the fly — and populate the object’s array property with a single line of code:
RETURN CREATOBJECT( 'xParameters', @laArray )



Date and time functions
Visual FoxPro has several handy dandy, built in functions for manipulating dates. The functions below illustrate how they can be used to perform a few of the most common tasks required when dealing with dates in your applications.

Elapsed time
Simply subtracting one DateTime expression from another gives you the elapsed time – this is good. Unfortunately Visual FoxPro gives you this result as the number of seconds between the two – this is bad. This value is seldom directly useful! You can use this little set of functions, which rely on the Modulus operator (%), to calculate the components of elapsed time in days, hours, minutes and seconds.

FUNCTION GetDays( tnElapsedSeconds )
RETURN INT(tnElapsedSeconds / 86400)
FUNCTION GetHours( tnElapsedSeconds )
RETURN INT(( tnElapsedSeconds % 86400 ) / 3600 )
FUNCTION GetMinutes( tnElapsedSeconds )
RETURN INT(( tnElapsedSeconds % 3600 ) / 60 )
FUNCTION GetSeconds( tnElapsedSeconds )
RETURN INT( tnElapsedSeconds % 60 )

Of course, there is more than one way to skin the fox. You can also use a single function to return an array containing the elapsed time positionally, with days as its first element through to seconds as its fourth like so:
FUNCTION GetElapsedTime( tnElapsedSeconds )

LOCAL laTime[4]
laTime[1] = INT( tnElapsedSeconds / 86400 )
laTime[2] = INT(( tnElapsedSeconds % 86400 ) / 3600 )
laTime[3] = INT(( tnElapsedSeconds % 3600 ) / 60 )
laTime[4] = INT( tnElapsedSeconds % 60 )
RETURN CREATEOBJECT( 'xParameters', @laTime )


If you prefer named parameters to the positional variety, the following code accomplishes the task:

FUNCTION GetElapsedTime( tnElapsedSeconds )
LOCAL loObject
loObject = CREATEOBJECT( 'Line' )
WITH loObject
.AddProperty( 'nDays', INT( tnElapsedSeconds / 86400 ) )
.AddProperty( 'nHours', INT(( tnElapsedSeconds % 86400 ) / 3600 ) )
.AddProperty( 'nMins', INT(( tnElapsedSeconds % 3600 ) / 60 ) )
.AddProperty( 'nSecs', INT( tnElapsedSeconds % 60 ) )
ENDWITH
RETURN loObject

Alternatively, if you merely require a string that contains the elapsed time in words, you can just reduce it to a single line of code!

FUNCTION GetElapsedTime( tnElapsedSeconds )
RETURN PADL( INT( tnElapsedSeconds / 86400 ), 3 )+' Days ';
+ PADL( INT(( tnElapsedSeconds % 86400 ) / 3600 ), 2, '0' )+' Hrs ' ;
+ PADL( INT(( tnElapsedSeconds % 3600 ) / 60 ), 2, '0')+' Min ' ;
+ PADL( INT( tnElapsedSeconds % 60 ), 2, '0' )+' Sec '

Date in words
Converting a value from date format into text can be a tricky business, especially when you are writing international applications. Visual FoxPro makes this task a lot easier with its native MDY(), CMONTH(), CDOW(), MONTH(), DAY() and YEAR() functions, to name but a few.

Version 6.0, with its ability to use strict dates, makes this task even easier. The following function provides one example of how to use these functions.

FUNCTION DateInWords( tdDate )
RETURN CDOW( tdDate ) + ', ' + MDY( tdDate )

However, the function listed above will not attach the ordinal suffix to the day portion of the date. If your application requires these suffixes when formatting the date in words, use the longer form of the function listed below. You could even extract the portion that calculates the suffix and place it in a function called MakeOrdinal. It can then be invoked any time you need to format a given number n as nth.

FUNCTION DateInWords( tdDate )
LOCAL lnDay, lnNdx, lcSuffix[31]
*** Initialize suffix for day
lnDay = DAY( tdDate )
lnNdx = lnDay % 10
IF NOT BETWEEN( lnNdx, 1, 3 )
   lcSuffix = 'th'
ELSE
  IF INT( lnDay / 10 ) = 1
     lcSuffix = 'th'
  ELSE

     lcSuffix = SUBSTR( 'stndrd', ( 2 * lnNdx ) - 1, 2 )
  ENDIF
ENDIF
RETURN CDOW( tdDate ) + ', ' + CMONTH( tdDate ) + ;
' ' + ALLTRIM( STR( lnDay )) + lcSuffix + ;
', ' + ALLTRIM( STR( YEAR( tdDate )))


Gotcha! Strict date format and parameterized views
Visual FoxPro's StrictDate format is especially comforting with the specter of the millennium bug looming large in front of us. At least it is as we are writing this. There, is however, one small bug that you should be aware of. If you have SET STRICTDATE TO 2 and try to open a parameterized view that takes a date as its parameter, you will be in for trouble. If the view parameter is not defined or is not in scope when you open or re-query the view, the friendly little dialog box prompting for the view parameter will not accept anything you enter. It will keep saying you have entered an ambiguous date/datetime constant.

The workaround is to ensure your view parameter is defined and in scope before trying to open or re-query the view. This means that, if your view is part of a form’s data environment, its NoDataOnLoad property must be set to avoid getting the dialog as the form loads.

The other workaround, setting StrictDate to 0 and then back to 2, is not recommended. As we have already mentioned, using a global solution for a local problem is a little bit like swatting flies with a sledgehammer.

Working with numbers
Mathematical calculations have been handled fairly well since the days of Eniac and Maniac, except for the notable bug in the Pentium math co-processor. The most common problems arise because many calculations produce irrational results such as numbers that carry on for an infinite number of decimal places. Rounding errors are impossible to avoid because computing demands these numbers be represented in a finite form. The study of numerical analysis deals with how to minimize these errors by changing the order in which mathematical operations are performed as well as providing methods such as the trapezoidal method for calculating the area under the curve. A discussion of this topic is beyond the scope of this book, but we can give you some tips and gotchas to watch out for when working with numbers in your application.

Converting numbers to strings
Converting integers to strings is fairly straightforward. ALLTRIM( STR( lnSomeNumber ) ) will handle the conversion if the integer contains ten digits or less. If the integer contains more than ten digits, this function will produce a string in scientific notation format unless you specify the length of the string result as the second parameter. When converting numeric values containing decimal points or currency values, it is probably better to use another function.

Although it can be accomplished using the STR() function, it is difficult to write a generic conversion routine. In order to convert the entire number you must specify both the total length of the number (including the decimal point) and the number of digits to the right of the decimal point. Thus STR(1234.5678) will produce '1235' as its result, and to get the correct conversion you must specify STR(1234.5678, 9, 4).

In Visual FoxPro 6.0, the TRANSFORM() function has been extended so that when called without any formatting parameters, it simply returns the passed value as its equivalent character string. Thus TRANSFORM(1234.5678) will correctly return '1234.5678'.

In all versions of Visual FoxPro you can use ALLTRIM( PADL ( lnSomeNumber, 32 ) ) to get the same result (providing that the total length of lnSomeNumber is less than thirty-two digits).

1 comment: