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 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:
Here is how the basic code would look:
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 t1avg;
In step 3 of the Doctor break, just before you print the total line,
calculate the average:
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:
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:
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
LATEST COMMENTS
MC Press Online