The Novice's Corner: Understanding Clarion Code
Posted December 14 1999
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.
Article comments
Search ClarionMag
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!
