Usually, when writing a program with level-break
processing--be it a report, batch data maintenance, or whatever--the same
approach is used. That approach is to read the input file straight through and
compare the key field values to their values from the previous record. This
requires holding fields for the previous record's values. It also requires
knowing whether your current record is the first record (so as not to process
the previous key values) or the last record (so as not to process the next key
values). This process can be quite messy, especially if it involves more than
one file.
I would like to introduce a different method that I learned
several years ago. I have found that this approach makes level-break processing
much easier and more intuitive.
The approach involves breaking a level
break down into three distinct steps: 1) Prepare for the level break (by
clearing out the totals for the break, setting up the page to break, loading
header data, etc.). 2) Process the records for the break. 3) End the break (by
printing the total line for this break, adding those totals to the next higher
break's totals, etc.).
The key to the simplicity of this method is step
2. What is meant by "Process the records for the break"? This is where you
process the detail level of your report or process, or it is where you
perform the next lower-level break. So, in this approach, each level break
is nested in the next higher break. It is coded as a group of nested
DO loops.
If you write such a
control break program in RPG, using its native file access opcodes, you do not
even need hold fields.
For this example, suppose you have a veterinary
clinic that wishes to print a monthly invoice history. They wish to print the
details of each invoice and then the total quantity and billed amount for each
invoice. They want to group the invoices first by customer, then by doctor,
printing the total quantity and billed amount at each break, and finally ending
with the grand totals. So the level breaks for this report are: 1) Grand
Total 2) Doctor (DRNUM) 3) Customer (CSTNUM) 4) Invoice Number
(INVNUM)
Assume that this information is in a file INVHDRL1, which is
keyed by Doctor, Customer, and Invoice Number. A file INVDTL contains the
invoice details, keyed by unique Invoice Number.
The RPG program will of
course have these files coded, with an external print file P_INVHST using
indicator 70 as the overflow indicator. Assign *IN70 to the name NewPage
(either through INDDS or a DS pointing to *IN).
Now, to implement this
level-break approach, you first need a set of KLISTs, one for each break
level:
C K1 klist C kfld DRNUM C C K2 klist C kfld DRNUM C kfld CSTNUM C C K3 klist C kfld DRNUM C kfld CSTNUM C kfld INVNUM
Or, for V5R2:
D BreakLevel E DS EXTNAME(INVHDRL1:*KEY)
Here is how the basic code would look:
/free exsr $Main; *inlr = *on;
//================================================================= begsr $Main; // This would be the Grand Total Level //================================================================= // Step 1 clear t0qty; // Initialize Grand Total fields clear t0amt;
NewPage = *on; // Force page break
read INVHDRL1; // The traditional priming read
// Step 2 dow not %eof; // Doctor exsr $Level1; read INVHDRL1; enddo;
// Step 3 exsr $NewPage; write Total0; // Write Grand Total line
endsr; //================================================================= begsr $Level1; // This would be the Doctor Level //================================================================= // Step 1 clear t1qty; // Initialize Doctor Total fields clear t1amt;
chain(e) DRNUM DRMAST; // Let's get the Doctor's name and if %error; // put it on the header. DRNAME = '*** Dr. not found'; endif;
NewPage = *on; // Force page break
// Step 2 dow not %eof; // Customer exsr $Level2; reade K1 INVHDRL1; // V5R2: reade %kds(BreakLevel:1) INVHDRL1 enddo;
// Step 3 exsr $NewPage; write Total1; // Write Doctor Total line
t0qty = t0qty + t1qty; // Add to next higher break's totals t0amt = t0amt + t1amt; // V5R2: t0qty += t1qty; t0amt += t1amt;
setgt K1 INVHDRL1; // Point to next doctor for next execution // of this level break. THIS IS // IMPORTANT! // V5R2: setgt %kds(BreakLevel:1) INVHDRL1
endsr;
//================================================================= begsr $Level2; // This would be the Customer Level //================================================================= // Step 1 clear t2qty; // Initialize Customer Total fields clear t2amt;
chain(e) CSTNUM CSTMAST; // Let's get the Customer's name and if %error; // put it on the header. CSTRNAME = '*** Customer not found'; endif;
NewPage = *on; // Force page break
// Step 2 dow not %eof; // Invoice exsr $Level3; reade K2 INVHDRL1; // V5R2: reade %kds(BreakLevel:2) INVHDRL1 enddo;
// Step 3 exsr $NewPage; write Total2; // Write Customer Total line
t1qty = t1qty + t2qty; // Add to next higher break's totals t1amt = t1amt + t2amt; // V5R2: t1qty += t2qty; t1amt += t2amt;
setgt K2 INVHDRL1; // Point to next doctor/customer for // next execution of this level break. // THIS IS IMPORTANT! // V5R2: setgt %kds(BreakLevel:2) INVHDRL1
endsr;
//================================================================= begsr $Level3; // This would be the Invoice Level //================================================================= // Step 1 clear t3qty; // Initialize Invoice Total fields clear t3amt;
write Invheader; // We will write a special line to // head the invoice, showing invoice // number, date, etc. Don't need a // page break, though.
// Step 2 setll INVNUM INVDTL; // We will shift to a different file for reade INVNUM INVDTL; // the details. See how easy it is with // this level break approach? dow not %eof; // Details! exsr $Detail; reade INVNUM INVDTL; enddo;
// Step 3 exsr $NewPage; write Total3; // Write Invoice Total line
t2qty = t2qty + t3qty; // Add to next higher break's totals t2amt = t2amt + t3amt; // V5R2: t2qty += t3qty; t2amt += t3amt;
setgt K3 INVHDRL1; // Point to next doctor/customer/invoice // for next execution of this level break. // THIS IS IMPORTANT! // V5R2: setgt %kds(BreakLevel:3) INVHDRL1
endsr;
//================================================================= begsr $Detail; // This would be where the details are printed. Since this is not a // level break, we won't follow the Step 1/2/3. //================================================================= // Here is where we construct the detail report line
exsr $NewPage; write Detail; // Write Detail line
t3qty = t3qty + INVDQTY; // Add to next higher break's totals t3amt = t3amt + INVDAMT; // In this case, it is the lowest break // V5R2: t3qty += INVDQTY; // t3amt += INVDAMT;
// You have the option of adding the // detail to ALL of the levels' totals, // here in the detail processing, instead // of the appropriate Step 3s. There // may be cases where that is necessary, // but usually it's not, and it is more // processor intensive.
endsr;
//================================================================= begsr $NewPage; //================================================================= if NewPage; write Header; NewPage = *off; endif;
endsr;
You can see that this code is easy to follow and easy to maintain. Now,
suppose you want to add an Average Amount Billed per Invoice to the Doctor total
line. Easy enough.
In step 1 of the Doctor break, clear the
fields:
clear t1count; clear t1avg;
In step 3 of the Doctor break, just before you print the total line,
calculate the average:
monitor; t1avg = t1amt / t1count; on-error; t1avg = *zero; endmon;
Since you need to count invoices for the doctor, count each invoice at
step 3 of the Invoice break:
t1count = t1count + 1;
The intuitive nature of this level-break approach makes changes like this
easy.
As I said, using RPG's native file access opcodes in this approach
makes hold fields unnecessary. But if you get your data some other way, you will
need some simple hold fields. For example, what if you want to use this approach
with data from an SQL cursor? You wouldn't use KLISTs; you would do something
like this instead:
DName+++++++++++ETDsFrom+++To/L+++IDc.Keywords+++++++++++++++++++++++++ D SQLEOF S N D D SQLRow DS D KeyField1 a b t Key fields must be contiguous! D KeyFieldn c d t D DataFieldA ... D DataFieldB ... D DataFieldC ... D SQLPtr S * INZ(%ADDR(SQLRow)) D RowKeys DS BASED(SQLPtr) D K1 a b A This spans the first key D Kn a d A This spans the first n keys D HoldKeys DS D H1 LIKE(K1) D Hn LIKE(Kn)
The only changes to the code above are in the
$Main routine, the level break
routines, and the level break routine calling the detail
processing:
In the $Main
routine:
//================================================================= begsr $Main; //================================================================= // Step 1 . . .
exsr $Read; // The traditional priming read
// Step 2 dow not SQLEOF; // No READ here! exsr $Level1; enddo;
// Step 3 . . .
endsr;
In the level break routines:
//================================================================= begsr $Level1; //================================================================= // Step 1 . . .
// Step 2 H1 = K1; dow not SQLEOF and (K1 = H1); exsr $Level2; // No READE! enddo;
// Step 3 . . . // no SETGT!
endsr;
In the level break routine calling the detail processing:
//================================================================= begsr $Leveln; //================================================================= // Step 1 . . .
// Step 2 Hn = Kn; dow not SQLEOF and (Kn = Hn); // Details! exsr $Detail; exsr $Read; // This is the only other read! enddo;
// Step 3 . . . // No SETGT!
endsr;
//================================================================= begsr $Read; //================================================================= /exec sql fetch CsrName into :SQLRow /end-exec
// Because of the pointer, K1 through Kn are loaded automatically
SQLEOF = (SQLCOD <> *zero);
endsr;
As you can see, the code changes are minimal; hold field usage is
straightforward.
A similar approach can be used in COBOL. If you do not
or cannot use COBOL pointers, you can set up your "KLISTs" like
so:
01
K1. 05 KEY-FIELD-1
...
01
Kn. 05 KEY-FIELD-1
... . .
05 KEY-FIELD-n
...
01
HOLD-FIELDS. 05 H1 LIKE
K1. . .
05 Hn
LIKE Kn.
The read routine would then have one
MOVE CORR from the input record to each
Kx structure.
This alternative
way of coding level break processing splits a level break into three parts and
nests them in their proper hierarchy, which makes level breaks intuitive, simple
to code (faster, too), and easy to maintain and enhance. This method allows for
multiple files and does not require concern for the first or last record. Give
it a try!
Doug Eckersley is an iSeries
application developer in Columbus. He has a decade of experience with the AS/400
and is certified by IBM. He also co-authored Brainbench's
RPG IV certification exam. He can be reached by email at eckersley1@columbus.rr.com.
|