9.11 String Manipulation

The Report Writer usually works with fields as indivisible entities. However, it is sometimes necessary to split a string into parts or to combine several strings into one string. The most common example of this is creating string sequence numbers. A string sequence number typically has a constant prefix (inv, 89.012*, etc.) followed by one or more digits (0123). The next few problems show various ways to generate sequence numbers using different features of the Report Writer's string manipulation functions.

The first problem is to generate invoices for the demonstration system given an invoice key prefix and starting sequence number. The invoice key prefix is passed to the report as $PREFIX. The starting invoice number is passed as $FIRSTNUM. Rather than actually creating records in an invoice file, we discuss that process later in this chapter where file manipulation is discussed. Instead, this report must produce something like:

Invoice: inv-1101, Amount: $23.50

Invoice: inv-1102, Amount: $19.50

Invoice: inv-1103, Amount: $15.00

etc.

To begin with, this report must read two files: mag and script. The script file tells us which magazines to bill, the mag file tells us how much to charge. You should be familiar with the FILE and FIELD declarations - they won't be presented at this time. The main process of this report reads the subscriptions file, script, and for each subscription, finds the magazine in the magazine file, mag. This gives the amount of the subscription. Subscription amounts are collected by subscriber. The last step generates an invoice number and prints the results. Our MAIN procedure looks like:

MAIN

BEGIN

CHECK script, AT END OF subscriber

DO print_invoice;

SELECT FROM script

SORTED BY subscriber, magazine;

FOR EACH script

Do accumulate_subscription;

DO last_invoice;

END

The procedure accumulate_subscription is responsible for computing the amount of one subscription. This amount is totaled for the subscriber and printed at the end of the subscriptions for one subscriber (by the print_invoice procedure). You should be familiar with the basics of the accumulate_subscription procedure. It is presented in full at the end of this section.

Once we have accumulated all the amounts for one subscriber, we need to generate the invoice number. If you remember, the starting invoice number is passed to the report in two parameters: $PREFIX and $FIRSTNUM. We must combine these two strings into one string. The @ operator combines two strings into one larger string. Its syntax is:

expression1 @ expression2

If the results of either expression1 or expression2 are not strings, they are first converted into strings. The string from expression2 is concatenated to the string from expression1 creating a new, larger string. For example, if the variable animal contains the string "cow", the following expression

"Holy" @ animal @ "!"

has a result of "Holy cow!". The first part of the expression "Holy" @ animal is evaluated and results in "Holy cow". This string becomes the expression for the second @: "Holy cow" @ "!".

Getting back to our problem, we could use a statement like

key = $PREFIX @ $FIRSTNUM;

to build the string for the key, but this would only work for the first sequence number. After that, we would have to work something else out.

A better approach would be to save $FIRSTNUM in a variable. This variable could be incremented and concatenated to $PREFIX to make the sequence numbers.

Saving $FIRSTNUM should only be done once, and should be done early in the report. With only one initialization like this, it seems appropriate to place this near the beginning of the MAIN procedure:

MAIN

BEGIN

sequence = $FIRSTNUM +0;

/* adding zero makes sequence a real type */

CHECK script, AT END OF subscriber

DO print_invoice;

SELECT FROM script

SORTED BY subscriber, magazine;

FOR EACH script

DO accumulate_subscription;

Do last_invoice;

END

When incrementing the sequence portion of the number, it is important to remember that the value passed in $FIRSTNUM is the number of the first invoice. That is, we should not add 1 to the sequence variable before building the invoice key. This can be done by performing the string concatenation before incrementing the sequence variable:

PROCEDURE next_invoice

BEGIN

key = $PREFIX @ sequence;

sequence = sequence + 1;

END

The completed report looks like:

/* generate sequence numbers */

FILE mag IS "mag"

FILE script IS "script"

FIELDS IN mag ARE magazine, year_rate;

FIELDS IN script ARE subscriber, magazine;

VARIABLES ARE

sequence, /* sequence number counter */

key, /* next sequence key */

amount, /* amount of one subscription */

sub_total TOTAL OF amount,

grand_total TOTAL OF amount;

MAIN

BEGIN

sequence = $FIRSTNUM +0;

/* adding zero makes sequence a real type */

CHECK script, AT END OF subscriber

DO print_invoice;

SELECT FROM script

SORTED BY subscriber, magazine;

FOR EACH script

DO accumulate_subscription;

DO last_invoice;

END

/* compute charge for one subscription */

PROCEDURE accumulate_subscription

BEGIN

FIND IN mag

WHERE magazine EQ script.magazine;

IF ERROR (mag) THEN BEGIN

PRINT "No magazine master for: ",

script.magazine, NL;

RETURN;

END

/* charge one year for each subscription */

amount = year_rate;

END

/* print next invoice and subscriber amount */

PROCEDURE print_invoice

BEGIN

DO next_invoice;

PRINT "Invoice: ", key,

", Amount: ", sub_total, NL;

END

/* print final summary */

PROCEDURE last_invoice

BEGIN

DO next_invoice;

PRINT "Next invoice to use is: ",

key, NL;

END

/* generate next invoice number */

PROCEDURE next_invoice

BEGIN

key = $PREFIX @ sequence;

sequence = sequence + 1;

END

Although this report works as described, it has some quirks that should be eliminated. First, the sequence numbers are not justified correctly. Since these are string fields, that means they won't sort as expected (inv-3 sorts after inv-11). If you try to solve this by entering 0001, you still get 1 as the starting number. Also, when the sequence is over one thousand, commas are inserted into the numbers by the Report Writer (inv-1,302).

One simple way to solve these problems is to use the FORMAT Report Writer function. The syntax of this function is:

FORMAT (expression1, expression2)

The FORMAT function takes the value of expression1 and formats it into a string using expression2 as the formatting string. With the FORMAT function, you gain total control over how the sequence number is converted into a string.

To solve the above mentioned quirks, we can re-write the next_invoice procedure to use the FORMAT function. The value to be formatted is the sequence variable, sequence. To make the formatting string easy, we will apply a temporary restriction: our version will only handle sequence numbers of 6 digits. This restriction makes the FORMAT function simple:

FORMAT (sequence, "000000")

The string of 6 zeros, "000000", is the formatting string. Our revised next_invoice procedure looks like:

PROCEDURE next_invoice

BEGIN

key = $PREFIX @ FORMAT (sequence, "000000");

sequence = sequence + 1;

END

This change eliminates the quirks mentioned earlier, but now has the added restriction that all sequence numbers must end with 6 digits. Suppose we wanted to remove that restriction. Suppose further that we wanted this report to be able to match the number of digits supplied by $FIRSTNUM. How would you do it?

The answer to the question lies in two facts. One, the number of zeros in the FORMAT function formatting string controls the number of digits produced. Two, the number of digits in $FIRSTNUM is the length of the string in characters.

Finding the length of a string is easy. The STRLEN Report Writer function returns the number of characters in a string, or zero if the string is empty. Its syntax is:

STRLEN (expression)

The expression is evaluated and, if necessary, converted to a string. The number of characters in the string are counted and returned as an integer. The following table shows sample expressions and the corresponding result returned from STRLEN:

Example Result

"invoice" 7

"Hello, " @ "World" 12

"""Enough!"", he said." 19

To determine how many digits there are in $FIRSTNUM we would use the following statement:

digits = STRLEN ($FIRSTNUM);

Now that we know how many digits are in $FIRSTNUM, we need to produce a string of that many zeros. For this we need one more fact: the C/Base programs currently support at most 15 digits. The actual number of digits is machine specific, but the limit quoted above applies to all machines to date. To tie this fact into our problem, you need the STRLEFT Report Writer function.

The STRLEFT function returns the leftmost portion of a string. The length of the string is controlled by an argument to STRLEFT. The syntax of this function is:

STRLEFT (expression1, expression2)

A string of expression2 characters is created from the leftmost characters of the expression1 string. The table below shows some sample values for expression1, expression2 and the resulting string from STRLEFT:

expr1 expr2 Result

"Hello, world!" 8 Hello, w

"Fiddle, " @ "Faddle" 12 Fiddle, Fadd

"0000000000" 5 00000

The last example is the key to our answer. By starting with a string of zeros that is longer than any possible number of digits (16 zeros would be sufficient), we can use the STRLEFT function to generate a string of digits whose length matches that of the $FIRSTNUM string:

fmtString = STRLEFT ("0000000000000000", digits);

This makes our next_invoice procedure look like:

PROCEDURE next_invoice

BEGIN

digits = STRLEN ($FIRSTNUM);

fmtString = STRLEFT ("0000000000000000", digits);

key = $PREFIX @ FORMAT (sequence, fmtString);

sequence = sequence + 1;

END

The first two statements in our next_invoice procedure are constant. While the result may differ between different runs of the report, they never change during the report. This implies that we should move them to the MAIN procedure where they can be executed once at the start of the report. Our final version looks like

/* generate sequence number */

FILE mag IS "mag"

FILE script IS "script"

FIELDS IN mag ARE magazine, year_rate;

FIELDS IN script ARE subscriber, magazine;

VARIABLES ARE

digits, /* number of digits in $FIRSTNUM */

fmtString, /* string to format sequence #'s */

sequence, /* sequence number counter */

key, /* next sequence key */

amount, /* amount of one subscription */

sub_total TOTAL OF amount,

grand_total TOTAL OF amount;

MAIN

BEGIN

digits = STRLEN ($FIRSTNUM);

fmtString = STRLEFT ("0000000000000000", digits);

sequence = $FIRSTNUM +0;

/* adding zero makes sequence a real type */

CHECK script, AT END OF subscriber

DO print_invoice;

SELECT FROM script

SORTED BY subscriber, magazine;

FOR EACH script

DO accumulate_subscription;

DO last_invoice;

END

/* compute charge for one subscription */

PROCEDURE accumulate_subscription

BEGIN

FIND IN mag

WHERE magazine EQ script.magazine;

IF ERROR (mag) THEN BEGIN

PRINT "No magazine master for: ",

script.magazine, NL;

RETURN;

END

/* charge one year for each subscription */

amount = year_rate;

END

/* print next invoice and subscriber amount */

PROCEDURE print_invoice

BEGIN

DO next_invoice;

PRINT "Invoice: ", key,

", Amount: ", sub_total, NL;

END

/* print final summary */

PROCEDURE last_invoice

BEGIN

DO next_invoice;

PRINT "Next invoice to use is:,

key, NL;

END

/* generate next invoice number */

PROCEDURE next_invoice

BEGIN

key = $PREFIX @ FORMAT (sequence, fmtString);

sequence = sequence + 1;

END