The Novice's Corner: Understanding Clarion Code

By David Harms

Posted December 14 1999

Printer-friendly version

In the first article in this series I discussed the basic format of Clarion source code and how the project system turns this into a working program. In this article I'll look in more detail at the language and discuss a small utility program for deleting old Windows temporary files.

But first, a few details about data types and operators. If you're familiar with these you may want to skip down to the example program, but then again if you read through you may just learn a few things that surprise you.

Data Types

In the previous article I touched on how Clarion allocates memory to variables, and how data declaration affects scope, or visibility. The following is a discussion of what form these declarations can take.

Clarion data declarations can be loosely grouped into three kinds: simple, complex (or structured), and special.

Simple data types

Clarion simple data types include BYTE, SHORT, USHORT, LONG, ULONG, SIGNED, UNSIGNED, SREAL, REAL, BFLOAT4, BFLOAT8, DECIMAL, PCDECIMAL, STRING, CSTRING, PSTRING, DATE and TIME. And there are a couple of new types coming in Clarion 5.5 including ASTRING and BSTRING.

Click here for a list of Clarion data types and their restrictions.

Some example variable declarations:

bVar   BYTE
lVar    LONG
sVar    STRING(20)

It's vital to know the limitations of the data types you're using. For instance, suppose you're using a BYTE variable called X as a loop counter, and you have code something like this:

LOOP X = 1 to 300
   ! do something
END

This code will run forever because whenever X reaches 255, the next increment will take it back to zero, and the value of 300 will never be achieved.

Another place incorrect data types can cause problems is on Windows API calls where an unsigned variable is expected and a signed variable is used. Windows will load up the specified number of bytes (i.e. two bytes for a USHORT) with value, but your Clarion program will interpret those bytes as a SHORT resulting in a quite different number than the one Windows stored.

You also need to be aware of when an integer variable is required, and when a floating point or decimal variable is required. If you attempt to store a floating point or decimal value in an integer variable, the decimal portion will be truncated. You don't have to explicitly tell Clarion when you're changing data types - you can just say (assuming you have a LONG variable called LongVar and a REAL variable called RealVar):

LongVar = RealVar

and Clarion will handle the data type conversion for you. It'll do numerics to strings and the other way around (if the string contains numbers, that is). But because Clarion does the type conversion you have to be extra careful that you're using data types that won't cause a loss of data (unless that's what you want).

Many of the data types in Clarion are there to support either particular types of databases or Windows API functions. SIGNED and UNSIGNED are useful with the latter but aren't actually data types. They're just assigned (using EQUATE) to the appropriate data type depending on whether the code is 16 or 32 bit.

Some of the more commonly used simple data types are BYTE, SHORT, LONG, REAL and DECIMAL. For floating point calculations, REALs are much faster than DECIMALs, but they lack some precision (this is a result of how the hardware does floating point calculations, not a shortcoming in Clarion). Where precision is essential you should use DECIMALs. When DECIMALs are used instead of REALs in a calculation Clarion invokes its internal binary coded decimal (BCD) library, which is accurate to within 31 decimal places on both sides of the decimal point. There are several rules governing when the BCD library is invoked. See the online help for BCD for more information.

PDECIMALs are mainly provided for Btrieve compatibility and are not otherwise much used.

Numeric variables can be declared with initial values, as in:

MyVar LONG(447)

In this example MyVar will be initialized to 447.

Among string data types the two most-used are STRING and CSTRING. STRING is the string type that has existed in Clarion since its origin, and CSTRING is the C language equivalent. The difference between the two is that STRINGs are padded with spaces, but CSTRINGs terminate with a null character. Say you have two STRING variables of 20 characters, and the first is set to 'John' and the second to 'Smith' and you want to combine them into a third STRING variable, which is 30 characters long.

FirstName  STRING(20)
SecondName  STRING(20)
FullName    STRING(30)
   CODE
   FirstName = 'John'
   SecondName = 'Smith'
   FullName = FirstName & ' ' & SecondName

The problem is that FullName will end up looking like this:

John Smith

In a STRING spaces are valid characters, and have to be stripped using the CLIP() function:

FullName = CLIP(FirstName) & ' ' & CLIP(SecondName)

If, on the other hand, you use CSTRINGs instead of STRINGs you don't have to do the CLIP at all. In the code:

FirstName = 'John'
SecondName = 'Smith'

FirstName will have a null character after the letter n and SecondName will have a null character after the letter h. This is done automatically whenever a value is assigned to a CSTRING; the null specifies where the string ends.

So why would anyone bother with STRINGs? For some of us, it's just habit. And with CSTRINGs you always have to allocate one more character than you would normally, because the last byte will always be a null value. So the declaration would look like this:

FirstName  CSTRING(21)
SecondName  CSTRING(21)
FullName    CSTRING(31)

That may not seem like a bit deal (what's an extra byte) but just remember it when you're dealing with strings of just a few characters!

Dates And Times

DATE and TIME are four byte values for dates and times, and are standard data types in SQL databases. Clarion applications traditionally have used LONG fields for dates and times, in the standard Clarion format. Clarion standard dates are stored as the number of days since Dec 28, 1800, and standard times are stored as the number of hundreds of a second since midnight, plus one. (Because of that extra hundredth of a second, time calculations can be tricky. Click here for more. Clarion standard dates and times (look for that phrase in the online help) stored in LONGs are fine if you're strictly using Clarion to access your data, since Clarion knows how to format the data. But if you'll be using any other products, be warned that most people will not be able look at the raw file data and know, for instance, that 72350 translates to February 8, 1999. (In fact it doesn't translate to that date at all. Get the point?)

Arrays

If you want more than one copy of a particular variable, you may find it easiest to use arrays. When you declare a variable use the DIM attribute to specify how many elements you want in the array. For an array of 20 name fields, you could use something like the following:

NameField STRING(30),DIM(20)

To reference the individual elements of the array you need to specify the array index in square brackets. To set the fourth NameField in the array you'd use the following:

NameField[4] = 'Some Value'

Arrays are a powerful addition to variable declaration, and begin to blur the line between simple and complex data types. In Clarion arrays always begin with the index 1 rather than zero. If you do attempt to access the zero element of an array you'll be attempting to use memory that doesn't belong to the array, and that can have all kinds of bad results.

Clarion will only tell you that you have an out of range array index if you have debug on and Array Index checked in your debug settings. In 16 bit code you'll get a compiler error if you're using an out of range constant to check the array index, and a runtime error in all other cases.

In 32 bit code you will get these errors if the subscript is greater than the array size, i.e. you attempt to access element 6 of a DIM(5) variable. However in 32 bit standalone apps there is NO check, runtime or compile time, on accessing the zero element of an array! So be very careful if you're coming from a language that uses zero-based arrays..

Complex Data Types: GROUPs

Complex data types are (generally speaking) combinations of simple data types. One of the most common is the GROUP. Here a small example:

MyGroup    GROUP
Var1          STRING(20)
Var2           STRING(20)
           END

GROUPs are a handy way of associating data types so you can move, copy, or otherwise deal with them as a unit. The most-used variation on this theme is the file RECORD structure which is really just a grouping of the fields in a file.

QUEUEs

Another oft-used data type is the QUEUE. A QUEUE is something like an array, but you don't specify ahead of time how many elements, or records, a QUEUE can have.

MyQueue   QUEUE
Var1         STRING(20)
Var2         STRING(20)
          END

One of the great conveniences of Clarion over the years is its ability to clean up after itself. In lower level languages you the programmer have to explicitly allocate memory for variables. Clarion always takes care of allocating memory for, and cleaning up, simple data types (except for those you create yourself using NEW, but that's another subject). QUEUEs are slightly different. To add a record to a QUEUE you set the values of the fields and then do an ADD(queuename). When you're done with the QUEUE you should issue a FREE(queuename) to release the memory taken by ADD. Procedure level QUEUEs will have their memory cleaned up when the procedure exits, but module and global QUEUEs will use up memory until the program exits. So clean up after yourself.

QUEUEs are quite powerful. You can sort on any field or combination of fields and retrieve matching records, and you can have QUEUEs of just about every type of data. I'll be coming back to QUEUEs later in this article.

CLASSes

The CLASS structure deserves a whole series of articles on its own, and is well beyond the scope of this article. If you'd like more information on how Clarion CLASS structures enable object-oriented development, see the ABCs of OOP series of articles.

Special Data Types

Clarion has a number of special data types, including reference variables (a sort of safe pointer), and the ANY data type, which can be used to store any numeric or string value or a reference to any simple data type. Feel free to read up on these subjects in the Language Reference - they won't be part of this series for a while yet.

Finally, An Example!

Here's an example, provided by Carl Barnes, that demonstrates some of the basics of Clarion data declarations and also introduces some of Clarion's program flow control statements. This is a little utility which cleans out Windows temporary files that are more than four days old.

Figure 1.  A utility program to delete old Windows temp files.
PROGRAM

   MAP
   END

DirQ    QUEUE(ff_:Queue),PRE(AnythingAtAll)
        END

Count    LONG
Idx      LONG
TempDir  STRING(255)

   CODE
   TempDir = 'C:\windows\Temp'
   !TempDir = 'C:\Temp'
   Count = 0
   DIRECTORY(DirQ,CLIP(TempDir)&'\*.*',ff_:Normal)
   LOOP Idx = 1 TO RECORDS(DirQ)
      GET(DirQ,Idx)
      IF DirQ.Date < TODAY() - 4
         REMOVE(CLIP(TempDir)&'\'&DirQ.Name)
         Count += 1
      END
   END
   MESSAGE('Done! ' & Count & ' file(s) removed')

The beginning of this program should look fairly familiar if you've read the first article in this series. But the first data declaration may be a bit of a puzzler.

DirQ      QUEUE(ff_:Queue),PRE(DirQ)
                       END

Clearly this is a QUEUE declaration, but it's of a slightly different form. Clarion allows you to build complex data types based on other data types. One way to do this is with the LIKE statement; the other, which applies to QUEUEs and GROUPs, is to use an existing QUEUE or GROUP as a parameter to the declaration. DirQ is based on something called ff_:Queue, and you can find out more about ff_:Queue in the Clarion help under Directory. Or you can just take it on faith that this is a QUEUE with some predefined fields.

So where is ff_:Queue declared? You may remember from the previous article that the MAP statement has the hidden benefit of automatically INCLUDEing EQUATES.CLW, which is in your LIBSRC\ directory, and that's where you can find ff_:Queue.

You can also, for the moment, ignore that strange PRE attribute. I'll come back to it shortly.

Following DirQ are some fairly normal looking variable declarations:

Count     LONG
Idx       LONG
TempDir   STRING(255)

Next is a CODE statement indicating the start of the executable part of the program. Several variables are initialized (you'll want to set TempDir to whichever directory your installation of Windows uses for its temporary files).

   CODE
   TempDir = 'C:\windows\Temp'
   !TempDir = 'C:\Temp'
   Count = 0

Now things get interesting. DIRECTORY is a library function that retrieves a directory file listing.

    DIRECTORY(DirQ,CLIP(TempDir)&'\*.*',ff_:Normal)

The first parameter is the QUEUE that will hold the file listing. This QUEUE has to be in a specific format, which is why it's based on the definition in EQUATES.CLW. The second parameter is the directory to retrieve, and the third parameter is a flag that tells the function what kind of files to retrieve. These flags are defined in EQUATES.CLW as well:

ff_:NORMAL        EQUATE(0)
ff_:READONLY      EQUATE(1)
ff_:HIDDEN        EQUATE(2)
ff_:SYSTEM        EQUATE(4)
ff_:DIRECTORY     EQUATE(10H)
ff_:ARCHIVE       EQUATE(20H)
ff_:LFN           EQUATE(80H)

EQUATE is a compiler directive which just means that a given label (such as ff_:ARCHIVE) should always be interpreted as the specified value. These flags area all defined so they can be used as bitwise flags, so they can be added if you want to combine features. For instance, to get a list of all of the normal and read-only files you would pass ff_:NORMAL + ff_:READONLY to the DIRECTORY procedure.

Although it may not look like it, the three parameters being passed to the DIRECTORY procedure aren't all handled the same way. Simple data types can be passed one of two ways; by value or by address. When a parameter is passed by value, the receiving procedure makes its own copy of that data. Any changes the receiving procedure makes to that data isn't reflected back in the calling procedure.

When data is passed by address, any changes the receiving procedure makes to the data are reflected in the calling procedure. For simple data types, a * is used in the prototype to indicate when something is passed by address.

The prototype for DIRECTORY, and all the other library functions, can be found in LIBSRC\BUILTINS.CLW:

DIRECTORY(ff_:queue result,STRING path,BYTE attrib),
    NAME('Cla$DIRECTORY')

You don't see any * in that prototype. Look a little further down the file however and you'll note that some prototypes do pass data by address:

WHAT(*GROUP,SIGNED),*?,NAME('Cla$WHAT')

So is DirQ passed by value? That's a trick question. Certain data types are defined as entity-parameters and are always passed by address. QUEUEs are one such data type. For more information see the help under Prototype Parameter Lists. This is one of the most vital pieces of documentation that comes with Clarion.

After DIRECTORY completes, DirQ will contain a directory listing which matches the attribute flags you specified. The QUEUE structure looks like this:

ff_:queue    QUEUE,PRE(ff_),TYPE
name           string(13)
date           long
time           long
size           long
attrib         byte
             END

(A digression: You'll notice that in the text of this article I'm using all caps for Clarion keywords, in keeping with stated Clarion practice. Yet the ff_:queue declaration is all lower case. That's what happens when you try to get programmers to be consistent. Personally I prefer lower case and mixed case to all caps, but for the sake of readability I've gone with the Clarion convention for this series of articles.)

This form of the QUEUE deals only with short file names - there is another for long file names. The fields include the file name, date and time stamps, the size, and the file attributes, corresponding to the equates listed above. In particular the date and time have been converted to Clarion standard date and time formats which makes an aging test very easy.

The code that deletes the files makes use of two program flow structures: LOOP and IF. The LOOP structure can have several forms, and in this case will loop continually beginning with the initial condition Idx = 1, and continuing until the condition Idx = RECORDS(DirQ) is met.

   LOOP Idx = 1 TO RECORDS(DirQ)
      GET(DirQ,Idx)
      IF DirQ.Date < TODAY() - 4
         REMOVE(CLIP(TempDir)&'\'&DirQ.Name)
         Count += 1
      END
   END

Idx is incremented on each loop iteration, and is used as an index to retrieve directory QUEUE records. When used this way QUEUEs are very similar to arrays, as each QUEUE record has an internal record number starting at 1.

Each file's date is tested to see if it is older than four days:

IF DirQ.Date < TODAY() - 4

Because the Date field is a member of DirQ it can (and I think should) be referenced as DirQ.Date. This "dot notation" is common to object-oriented languages, and Clarion is becoming more OO with each release.

The more traditional approach is to use a prefix, as indicated by the PRE(DirQ) attribute. When you specify a prefix the compiler automatically adds a colon to the end of the prefix text. You can then refer to the Date field in the QUEUE as DirQ:Date. The only difference in this example is the colon instead of the dot.

In fact, if you were working with a straight QUEUE declaration instead of one that was derived from ff_:Queue, you wouldn't need a prefix at all. But you do need one because of how the compiler handles the naming of GROUP and QUEUE fields internally. It's a little complication that only affects derived QUEUEs and GROUPs. Remove PRE(DirQ) and compile and you'll see what I mean.

Prefixes are also typically used on file layouts. And although they're a bit of a throwback, they do have two big advantages: they're short, and they ignore nesting. Say you have a GROUP called People with a prefix PEO, and it looks like this:

People      GROUP,PRE(PEO)
FirstName      STRING(30)
LastName       STRING(30)
Phone          GROUP
Area              SHORT
Number            LONG
               END
            END

You can use PEO:Area to refer to the area code. But if you want to stick with the dot syntax, you'll need to say People.Phone.Area which is a bit more of a hassle. Without the prefix you have to fully qualify the field names with any nested structures.

Back to the code. If the file in question is more than four days old, it's deleted using the REMOVE library function. Because this is a non-critical function there's no error checking; it isn't a big deal if a temporary file can't be deleted.

The last task is to display a message letting the reader know what's happened.

MESSAGE('Done! ' & Count & ' file(s) removed')

And that's it. You now have a handy utility program for removing unneeded temporary files. Thanks, Carl!

Summary

It's not difficult to write small utilities like this one, but then again there's a lot missing from this utility too. Almost all applications display windows and respond to events, behavior that is absent from this example. In the next article I'll introduce the Window structure and show how to add a user interface to the directory cleanup program.

Download the source code

David Harms is an independent software developer and the editor and publisher of Clarion Magazine. He is also co-author with Ross Santos of Developing Clarion for Windows Applications, published by SAMS (1995), and has written or co-written several Java books. David is a member of the American Society of Journalists and Authors (ASJA).

Article comments

Post a comment

You must be logged on to post comments.

Clarion Roadmap

Try the roadmap (beta)

Search ClarionMag

 

Advanced search

From the archives

External Business Rules with the In-Memory Driver

6/21/2006 12:00:00 AM

Towards the end of 2004 Nardus Swanevelder wrote a series of articles on Clarion's Business rules, and how they could be configured at runtime. In this update, Nardus shows how to use configurable business rules with the In-Memory Driver. SOURCE LINK UPDATED!