9.9 Variables

Until now, we have only been printing what was stored in a data file without doing any calculations. Many reports require that some kind of calculation be performed: summarize information (totals), compute profit margins, compute averages, etc. For this reason, the Report Writer has the ability to define extra values for calculations. Each value, called a variable, must be declared in a VARIABLES declaration. These declarations must appear after all FIELD declarations and before any procedures (including MAIN).

One very common calculation done by reports is totaling a field. For this reason, the Report Writer has a special feature that provides automatic totaling. To use automatic totaling, you must declare a variable as the total of some other field or variable. Then, each time a value is stored in the item being totaled, the value is also added into the totaling variable. The following example helps to illustrate this.

We are going to write a report that shows the total number of subscribers to all magazines, as well as the normal detail for each magazine. This report is almost the same as problem 1 (for those of you who didn't take time to solve this problem or check at the end of this chapter for a sample solution, you might want to take time now to do so. For you other people, one gold star).

In order to generate a total, we must declare the variable as a total of something. What we need is a total of the subscribers field. The logical choice, therefore is to make the variable a total of subscribers. This is done through the declaration

VARIABLE IS

tot_subs TOTAL OF subscribers;

The only thing left is to print out the total at the end of the report. (Do I hear the call of another procedure?) Create a procedure which prints out the total of the subscribers (i.e., prints the variable tot_subs). You might be tempted to write this PRINT statement as:

PRINT TAB (20), "Total subscribers", TAB (55), tot_subs, NL;

The only problem with this kind of printing is that you would generate the following type of output:

rd Readers Digest 3

sd Science Digest 25

time Time Magazine 1,034

wsj Wall Street Journal 150

Total subscribers 1,212

The column of numbers are "left-justified", which means the numbers line up on the left side. Normally, numbers are "right-justified" which, as you might expect, means the numbers line up on the right side. In order to get things to line up on the right side, you have to know where the right side is. Or, stated in Report Writer terms, the left side is the current column when a number is printed, the right side is the current column plus a field width specified by the programmer (that's you). For example, if we assume that all magazines have less than 10,000 subscriptions then a field width of 5 covers things (didn't forget that comma, did you?). If we apply this to our example, the procedure details would be changed to

PROCEDURE details /* print one magazine */

BEGIN

PRINT magazine, TAB (16), title, TAB (55),

subscribers:5, NL;

END

Thus far, our output would look something like

rd Readers Digest 3

sd Science Digest 25

time Time Magazine 1,034

wsj Wall Street Journal 150

Note that the numbers now line up on the right side.

The last thing to consider is the total amount and how it appears. We have already seen how to make the amount line up on the right side. What we have been ignoring up till now is how the output looks in terms of ease of finding the total. With things the way they are, the total follows right after the column of numbers and might be mistaken as an additional entry. The way to avoid this is to somehow highlight the total. One method of doing this is to insert a blank line between the column of numbers and the total. Our output would then look like:

rd Readers Digest 3

sd Science Digest 25

time Time Magazine 1,034

wsj Wall Street Journal 150

Total subscribers 1,212

Now you can easily find the total, and it cannot be mistaken as part of the column of numbers. The procedure to print the total, which we chose to name summary, might look something like

PROCEDURE summary /* print out total subscribers */

BEGIN

PRINT NL, TAB (20), "Total subscribers",

TAB (55), tot_subs:5, NL;

END

The full program looks like:

/* list magazine subscriptions available */

FILE master IS "mag"

FIELDS IN master ARE

magazine, title,

subscribers;

VARIABLE IS

tot_subs TOTAL OF subscribers;

MAIN

BEGIN

AT TOP OF PAGE

DO headings;

SELECT FROM master

SORTED BY magazine;

FOR EACH master

DO details;

DO summary;

END

PROCEDURE headings /* print page headings */

BEGIN

PRINT TAB (15), "M a g a z i n e L i s t",

TAB (60), $today_date, TAB (70), "Page ",

$page, NL, NL;

PRINT "Magazine", TAB (16), "Magazine Title",

TAB (55), "Number Subscribers", NL, NL;

END

PROCEDURE details /* print one magazine */

BEGIN

PRINT magazine, TAB (16), title, TAB (55),

subscribers:5, NL;

END

PROCEDURE summary /* print total subscribers */

BEGIN

PRINT NL, TAB (20), "Total subscribers",

TAB (55), tot_subs:5, NL;

END

Totaling a field is very useful, but suppose what you needed was the total of some calculation? Assume you needed to write a report to show how much money is being generated annually by each magazine. This involves printing out the magazine master file (which we already know how to do), computing how much money each magazine makes, and then adding up all of the magazines and printing the total. We can do this by creating a variable that holds how much money one magazine makes annually. Then we can create another variable that totals all of the values stored into the previous variable. The declaration for this looks like:

VARIABLES ARE

annual_amt,

tot_annual_amt TOTAL OF annual_amt;

We still need to compute the amount of money generated annually by each magazine. The magazine file contains the fields year_rate and subscribers. If we multiply these two fields together the result is the annual money generated (for simplicity, we are assuming all subscriptions started at the beginning of the year and run for at least 1 year.) The statement:

annual_amt := year_rate * subscribers;

stores the product into the variable annual_amt, it also adds the product into the variable tot_annual_amt. The place for this statement is in the procedure which prints the detail for each magazine. This procedure might look like:

PROCEDURE details /* print one magazine */

BEGIN

annual_amt := year_rate * subscribers;

PRINT magazine, TAB (16), title, TAB (55),

subscribers:6, TAB(69), annual_amt:10,

NL;

END

Did you notice how the amount is being printed? Annual_amt is a money-type value from the calculation being done (money per subscription * #subscriptions = money). When you print a money value and don't specify how many decimal places you want to see, the Report Writer assumes you want to print two decimal places and tries to do so. If the value has fractions of pennies, more than two decimal places are shown.

In our example the annual_amt is not likely to have fractions of pennies. But if we wanted to calculate the amount for half a year by dividing the annual amount by 2, we could end up with fractions of pennies. One way of insuring that only two decimal places are displayed for a calculated money value is to use the ROUND function. The syntax for the ROUND function is:

ROUND (value)

The amount for half a year could be calculated as follows:

half_year_amt = ROUND (year_rate * subscribers / 2);

The ROUND function can also be used on real type values. In this case the REAL value is rounded to an integer or whole number.

Continuing with our example, the only remaining thing to do is to print out the total of all the magazines when the report is finished. This can be accomplished by adding a procedure, summary, to print out the total along with a description of the amount. This procedure might look like:

PROCEDURE summary /* print amount total */

BEGIN

PRINT NL, TAB (16), "Total amount generated",

TAB (67), tot_annual_amt:12, NL;

END

Notice again how the total is being printed. Since each item being totaled is a money value, the sum is also a money value. This means the same rules of printing apply. However, you might have noted that the field width is larger. The reason for this is that when you add up a column of numbers, it is quite reasonable for the sum to be more digits than the individual numbers. That is why we increase the field width, this also means that we have to decrease the TAB column by two to keep the right sides lined up.

This procedure needs to be called at the end of the report. This is done by placing the procedure call as the last statement of the MAIN procedure. The entire report looks like:

/* list magazine subscriptions available */

FILE master IS "mag"

FIELDS IN master ARE

magazine, title,

year_rate, subscribers;

VARIABLES ARE

annual_amt,

tot_annual_amt TOTAL OF annual_amt;

MAIN

BEGIN

AT TOP OF PAGE

DO headings;

SELECT FROM master

SORTED BY magazine;

FOR EACH master

DO details;

DO summary;

END

PROCEDURE headings /* print page headings */

BEGIN

PRINT TAB (15), "M a g a z i n e L i s t",

TAB (60), $todays_date:10, "Page ",

$page, NL;

PRINT "Magazine", TAB (16), "Magazine Title",

TAB (55), "Subscriptions", TAB (69),

"Annual Inc.", NL, NL;

END

PROCEDURE details /* print one magazine */

BEGIN

annual_amt := year_rate * subscribers;

PRINT magazine, TAB (16), title, TAB (55),

subscribers:6, TAB (69), annual_amt:10,

NL;

END

PROCEDURE summary /* print amount total */

BEGIN

PRINT NL, TAB (16), "Total amount generated",

TAB (67), tot_annual_amt:12, NL;

END

In our previous example, we were just generating a report total. Suppose you needed several totals and then a grand total for the report. An example of this might be report to show how much each subscriber pays annually for their magazines followed by a total for all subscribers. We will use as a model the report from the section on breaks that listed the magazine subscriptions by subscriber. We have to add the code to show the cost for a one year subscription for each magazine cost. This information is stored in the magazine file in the field year_rate, so we must also look up the magazine from the magazine code in the subscriptions file.

We will use the same variables as the previous example. In addition, we must also add a variable to accumulate a total for the report. With this added variable, our declaration looks like:

VARIABLES ARE

annual_amt,

tot_annual_amt TOTAL OF annual_amt,

grand_tot_amt TOTAL OF annual_amt;

Note two things here. First, two variables are being declared as the total of one variable. This is allowed, and each variable is a total of the variable annual_amt. Second, there is nothing in the declaration which says that grand_tot_amt is a grand total for the entire report. What determines this is when the variable is printed. Variables that are declared as TOTAL are reset to zero each time they are referenced (i.e., printed). Therefore, if we do not print out the value of grand_tot_amt until the end of the report, it is a report total. By setting a break for the end of subscribers, we can print out the total for each subscriber. When we do this, the total is automatically reset to zero for the next subscriber. Thus, tot_annual_amt becomes a subscriber total.

By now, you should be familiar with the fundamentals of the changes made to the report, so only the final version is presented here.

/* list subscriptions by subscriber */

FILE scripts IS "script" /* subscriptions file */

FILE sub IS "sub" /* subscriber file */

FILE mag IS "mag" /* magazine file */

FIELDS IN scripts ARE

subscriber, magazine,

started, issues;

FIELDS IN sub ARE

subscriber, name;

FIELDS IN mag ARE

magazine, year_rate;

VARIABLES ARE

annual_amt,

tot_annual_amt TOTAL OF annual_amt,

grand_tot_amt TOTAL OF annual_amt;

MAIN

BEGIN

AT TOP OF PAGE

DO headings;

CHECK scripts, AT START OF subscriber

DO new_subscriber;

CHECK scripts, AT END OF subscriber

DO end_subscribers;

SELECT FROM scripts

SORTED BY subscriber;

FOR EACH scripts

DO details;

DO report_summary;

END

PROCEDURE headings

BEGIN

PRINT TAB (20),

"S u b s c r i p t i o n L i s t",

TAB (60), $todays_date:10, "Page ",

$page, NL;

PRINT "Subscriber name", TAB (36),

"Magazine", TAB (52), "Date Started",

TAB (68), "Yearly Amt", NL, NL;

END

PROCEDURE new_subscriber

BEGIN

FIND IN sub

WHERE subscriber EQ scripts.subscriber;

PRINT NL, name;

END

PROCEDURE details

BEGIN

FIND IN mag

WHERE magazine EQ scripts.magazine;

annual_amt := year_rate;

PRINT TAB (36), scripts.magazine, TAB (52),

started, TAB (68), annual_amt:10, NL;

END

PROCEDURE end_subscriber

BEGIN

PRINT NL, TAB (10), "Total for: ", name,

TAB (66), tot_annual_amt:12, NL;

END

PROCEDURE report_summary

BEGIN

PRINT NL, TAB (10), "Grand Total",

TAB (66), grand_tot_amt:12, NL;

END