TechTip: An Easier Way to Process Level Breaks

RPG
Typography
  • Smaller Small Medium Big Bigger
  • Default Helvetica Segoe Georgia Times
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 This email address is being protected from spambots. You need JavaScript enabled to view it..

BLOG COMMENTS POWERED BY DISQUS

LATEST COMMENTS

Support MC Press Online

$0.00 Raised:
$