(c) Copyright 2001-2003 Foxtalk / Viafox, All rights reserved
This article was originally published in Foxtalk,
december 2001.
A Compact Cursor—Ideal for Displaying Multiple
Addresses
These days, it's common for people to have multiple
phone numbers and other kinds of addresses. In the old days, most people had
only one or two phone numbers, and exceptional people (the eccentrics) who had
more phone numbers weren't really handled within our databases. We thought we
couldn't, or we didn't want to, handle those eccentrics in the databases we
built for our customers. For example, we created databases with at most two
fields for phone numbers. People were happy with the fact that we could automate
some things, and customers didn't really complain about the restrictiveness for
a long time. But those days are over...
These days, not only do
people have an ever-increasing number of addresses (where and how they can be
reached), but the customer also wants to store and retrieve those multiple
addresses. We can no longer use multiple fields; rather, we put those addresses
in a separate table (table being the new word for the old database), and
each address takes up a separate record in that table. The Condes Address
Model that I developed is a clear example of such a structure (see my
related article in this issue, "Introduction to the Condes Address
Model"). That model not only describes the way the database should be
structured, but also the functionality that classes and forms should support
and the procedures that users, who have to enter and change addresses, should
adopt.
This article explains a
technique that can be used to display multiple addresses. For example,
how do you display all addresses of a certain type on screen? And how do you
display them on a report? It isn't easy! Anyone who's tried will know what I'm
talking about. Let's stick to the burden of displaying them on a report. The
problem is that there's only a single Detail Band available in a FoxPro report.
So, if you're lucky, you can use that Detail Band to display a person's
multiple street addresses. But if you're not lucky, you're also asked to
display the multiple phone numbers that this person has. Oops, that means a
shortage of one Detail Band already. What to do now?
Use my CompactCursor()
function! This function will produce a "compact" cursor, the solution
to the problem described in the preceding paragraph. And you can use it for
free.
What's a compact cursor? A
compact cursor is a cursor that takes as input another cursor or table, and
puts information that's scattered over one or more records in the original
cursor/table into one record in a newly created cursor—the "compact"
cursor. The gathering of several records into one record is determined by the
value of an ID field. All records in the original cursor/table that have the
same value in the ID field are the candidates for the gathering. Look at the
following scheme (assume it's a normal cursor):
|
ID |
StreetAddress |
|
00125 |
M. Emantsstr. 2 |
|
01023 |
Sesamestreet 32 |
|
01023 |
Wall Street 1 |
|
02318 |
Abbey Road 3 |
|
01023 |
42nd Street 21 |
This original cursor has three
records with the same ID 01023—let's say a person's unique key. So this person
has three addresses. In the compact cursor, there's only one record for this
person, and there will be one memo field besides the ID field:
|
ID |
StreetAddress |
|
00125 |
Memo |
|
01023 |
Memo |
|
02318 |
Memo |
What do you think is in the
memo field? Right—the street addresses, directly below each other! So the memo
of 01023 will read:
Sesamestreet 32
Wall Street 1
42nd Street 21
Do you get the picture?
Then it's only a small step to understanding that displaying this cursor no
longer requires multiple Detail Bands.
You might do the same with
phone numbers. Let's suppose there's another cursor with these phone numbers:
|
ID |
Phonenumber |
|
00125 |
02-233423445 |
|
01023 |
09-234230001 |
|
01023 |
004-45345545 begin_of_the_skype_highlighting 004-45345545 end_of_the_skype_highlighting |
|
02318 |
0292-22343444 |
|
01023 |
- |
CompactCursor() would
create the next cursor:
|
ID |
Phonenumber |
|
00125 |
Memo |
|
01023 |
Memo |
|
02318 |
Memo |
A normal Select-SQL, with some
outer join statements, is enough to create a cursor that has all of the
required info about the persons.
|
ID |
Surname |
StreetAddress |
Phonenumber |
|
00125 |
BeGood |
Memo |
Memo |
|
01023 |
Pietersen |
Memo |
Memo |
|
02318 |
Fa Leng |
Memo |
Memo |
Now the solution is
complete, and you'll even think of other possible usages. For example, it's
ideal for displaying multiple From-Thru dates, as you can see in Figure 1,
where the first person needs more lines.
Figure
1 - Displaying multiple From-Thru dates.
The funny thing is that the
memo fields and the name field are on the same line in the report. Make the
memo fields stretchable, and they'll display all dates perfectly.
Today the need for multiple
addresses is real. The function I wrote does a good and fast job. If I had my
druthers, I'd like the VFP development team to add this capability to the
native product—they can deliver unmatchable speed and ease of implementation. I
guess there could be a keyword added to the Select-SQL command, but there's an
international committee that will inhibit such "abuse." So I propose
a separate command inspired by the Select-SQL command. Here's an example that
demonstrates the capability I'd like:
COMPACT
SELECT ;
a.ID, ;
a.Surname, ;
b.StreetAddress, ;
c.Phonenumber ;
FROM person a, streetaddress b,
phonenumber c ;
WHERE a.ID = b.ID
AND
a.ID = c.ID ;
GROUP BY 1 ;
ORDER BY 1 ;
INTO CURSOR c_person
The GROUP BY expression is
the equivalent of the field that CompactCursor() regards as the ID field. And
no matter what datatype the other fields originally have, they'll be of
type Memo in the resulting cursor. (So that also accounts for Surname in the
preceding example.) Other solutions are certainly possible—this was only a
first move.
Reference
Okay, now let's see
how the CompactCursor() function works:
•
It takes its input from a table or cursor.
•
On exit, the new compact cursor will be the "currently" selected
cursor.
•
The From table or cursor typically has several records that share the same
value in an ID field in those same records.
•
The first field in the From table/cursor will be regarded as the ID field.
•
The n-found values found in an equal number of records in the From table/cursor
will be on an equal number of lines in the memo field, unless you use the sixth
parameter.
Here's the syntax:
lOK
= CompactCursor( cFrom, cIntoCursor, cDoFields, cFormatCodes, lTrim,
lNotIfEmpty, cThermo )
All parameters are optional
and are described here:
•
cFrom—This parameter should be an existing table or cursor. If it's not
yet open, it will be opened and stay open (just like a Select-SQL would do). It
defaults to the current ALIAS(). The first field in this table/cursor
will be regarded as the ID field.
•
cIntoCursor—This parameter should be a valid name for a compact cursor.
It defaults to the name of the From table/cursor with the addition of
"Compact."
•
cDoFields—This parameter should contain a comma-delimited list of the field
names that must be processed. It defaults to all fields (except the ID field,
of course).
•
cFormatCodes—The TRANSFORM() function is used for the conversion of the
fieldvalues so that they fit into the memo field. This parameter will be passed
to that function as FormatCodes, if specified. So, read the Help text on that
function to check what the parameter should look like in your case.
•
lTrim—Pass .T. if you want to trim leading and trailing spaces.
•
lNotIfEmpty—Pass .T. if you want to "skip" records that have
an empty field. Thus you'll be able to avoid empty lines in the memo. The
default is that such empty fields aren't skipped—a situation that's required if
there are, for instance, two fields in which even empty lines have meaning. An
example is a From Date memo field and a To Date memo field.
•
cThermo—Do you want to inform your users about the progress the routine
makes? Pass the name of your progress bar/thermometer function. It will be
executed with macro substitution for each record in the created compact cursor.
At that moment the total number of records will already be known, so you can
use both recno() and reccount() in your function.
The function does return a
Boolean value that indicates whether or not it was successful in creating a
compact cursor.
Following are some
important notes:
•
The parameter cFormatCodes is used for each processed field. This may pose a
problem if some of the fields actually need different formatting. In that case,
you should call the function two or more consecutive times, each time
processing only a subset of the fields, by specifying cDoFields. This enables
you to specify a different cFormatCodes for each subset. The compact cursor
will be created on the first call, and it will also receive all records in that
first call. The additional calls will only process the additionally declared
fields.
•
The routine hardly checks for error-causing conditions. You're supposed to
understand what the routine does (RThisFM) and thus be able to debug the cause
of an error.
•
The resulting compact cursor will have an index tag PRIMARY based on the ID
field.
Examples
And, finally, here
are some examples. The first one demonstrates the basic principle and the use
of format codes.
select
;
city as id, ;
padr(alltrim(address)+' '+number,50)
as address,;
amount ;
from address ;
where recno() < 6000 ;
into cursor c_adr nofilter
CompactCursor('c_adr','c_adrX','address')
CompactCursor('c_adr','c_adrX','amount','999,999.99')
browse
In the next, more
realistic, example, the street addresses of companies are in a separate table
StreetAddress, to allow for storing eventual extra street addresses. Each
record also has the ID of the owning company. The first statement creates a
cursor with a record for each full address. Then, CompactCursor() uses this
cursor to create a compact cursor. Finally, the info in the Company table is
put into a cursor that's extended with a memo field that contains all street
addresses of a company.
select
;
a.CompanyID, ;
alltrim(a.cStreet)+'
'+alltrim(a.cNumber)+CRLF+ ;
alltrim(a.cPostalCode)+' '+alltrim(a.cCity)+CRLF ;
as
mStreetAddresses ;
from StreetAddress a ;
into cursor c_StrAddress0
CompactCursor('c_StrAddress0','c_StrAddress',,,.t.)
select ;
a.*, ;
b.mStreetAddresses ;
from Company a left outer join
c_StrAddress b ;
on a.CompanyID = b.CompanyID ;
into cursor c_SelectedCompany
Okay, enough said. Go ahead
and download the function, which is packed together in the Download file
for my Condes Address Model article in this issue or available on my Web site,
and give it a try!