Writing Control Break Programs
May 12, 2004 Ted Holt
[The code for this article is available for download.]
Having worked on programs written by different programmers, I have noticed a lot of different techniques and programming styles. However, one thing is consistent: that most programmers do not design programs. When it comes time to write a program, they just sit down at their workstations and have at it. This approach to program development results in error-prone programs that are hard to modify. A saying I once read on a poster comes to mind: If builders built houses the way programmers write programs, the first woodpecker to come along would destroy civilization.
Control-break programs are good examples of this lack of design. It’s often obvious that the programmer of a control-break program did not design the program, but instead cobbled something together that produced the desired results.
Since I’m no longer allowed to use my beloved RPG cycle, I have developed a methodical way to write procedural control-break programs that not only produce the desired results but also are easy for me to read, understand, modify, and debug. Since I work in RPG, I will use that language to illustrate my method. However, I have used the same logic just as effectively in other procedural languages.
THE TEMPLATE
First let me present the template, cbtemplate.rpg, from which I begin. Tokens surrounded by angle brackets (< and >) are to be replaced with proper values, such as field and file names. The template has logic for three levels of control breaks. Level 10 is the major break field, level 20 is an intermediate break, and level 30 is the minor break. You may need to add or delete levels, but doing so is a matter of copying the code for a level and modifying it appropriately.
As I explain the principles behind control-break logic, I will illustrate with parts of the template.
Principle 1: You must save the values of control fields in order to check for control breaks. Keep in mind that a control break may have more than one control field. If your control fields can be null, you will have to save the null value setting, in addition to the field value. Here are the save fields from the template.
* control break save fields D cb10 s like(<major.break.field>) D cb10Save s like(cb10) D cb20 s like(<intermediate.break.field>) D cb20Save s like(cb20) D cb30 s like(<minor.break.field>) D cb30Save s like(cb30) D Null10 s 5i 0 D Null10Save s like(Null10) D Null20 s 5i 0 D Null20Save s like(Null20) D Null30 s 5i 0 D Null30Save s like(Null30)
This code makes two assumptions. The first is that the break fields can be null. If that is not the case, you can remove the null fields from this section of code and anywhere else they’re used. The second assumption is that there is only one control field per level. If there are two or more control fields on one level, duplicate the variable names for that level and modify them. I like to insert a letter after the level number. For example, if level 30 has two control fields, I would define variables CB30A, CB30ASAVE, CB30B, CB30BSAVE, and so on.
Principle 2: Load the comparison fields when you successfully read an input record. Here is part of the routine to read an input record. My template is set up to use SQL, but this logic works just as well with native I/O.
C Read begsr C C/exec SQL C+ Fetch c1 into <host variable list here> C+ C/end-exec C C eval bEOF = (SQLStt <> SQLNormal) C C if not bEOF C* copy input fields to to control break variables here C eval Null10 = <major.break.field.null.ind> C eval Null20 = <intermediate.break.field.null.ind> C eval Null30 = <minor.break.field.null.ind> C if Null10 >= *zero C eval cb10 = <major.break.field> C endif C if Null20 >= *zero C eval cb20 = <intermediate.break.field> C endif C if Null30 >= *zero C eval cb30 = <minor.break.field< C endif C endif C C endsr
If the program reads a record, the SQL status code is all zeros, which is equivalent to the constant SQLNormal, defined in the D-specs. SQL returns a status 02000 when it runs out of data, forcing variable BEOF to be true. If SQL finds a record, it updates the control-break variables with the field values and null indicators.
Principle 3: You must check for the control breaks in major to minor order before processing an input record. In the following code snippet, level 10, the major break, is tested first. If no control break has occurred, the next level is tested.
C select C when cb10 < > cb10Save C or Null10 <> Null10Save ... calcs for highest level break go here C when cb20 <> cb20Save C or Null20 <> Null20Save ... calcs for 2nd-highest level break go here C when cb30 <> cb30Save C or Null30 <> Null30Save ... calcs for lowest level break go here C endsl
Principle 4: When a control break occurs, end the preceding group before beginning the new one.
The following subroutine is a fleshed-out version of the previous code segment.
C CheckCtlBreak begsr C C select C when cb10 <> cb10Save C or Null10 <> Null10Save C exsr EndLvl10 C exsr BgnLvl10 C when cb20 <> cb20Save C or Null20 <> Null20Save C exsr EndLvl20 C exsr BgnLvl20 C when cb30 <> cb30Save C or Null30 <> Null30Save C exsr EndLvl30 C exsr BgnLvl30 C endsl C C endsr
The subroutine that ends a control group is called before beginning the next control group.
Principle 5: Define subroutines or subprocedures to process the beginning and end of each control break. Using routines makes it easy to ensure that everything is done at the proper time. Here is the template subroutine to begin a level.
C BgnLvl10 begsr C * save control break fields C eval cb10Save = cb10 C eval Null10Save = Null10 * print headings for this level * clear accumulators for this level * force breaks at lower levels C exsr BgnLvl20 C C endsr
Notice the comments. They tell you what needs to be done. All you have to do is add the proper code after each comment, if applicable. The method I use requires that I save control-break values, print headings for the group, clear accumulators and counters for the group, and force the next lower level.
Principle 6: Major breaks always force minor breaks. Suppose you are breaking on date within department number. If department number changes, it doesn’t matter whether date changes. You must force a date break if there is a break on department number.
Continuing with the template, here’s a subroutine to end a level.
C EndLvl10 begsr C * force breaks at lower levels C exsr EndLvl20 * accumulate to higher levels * print totals C C endsr
When ending a level, you must force the next lowest level before doing anything else. As with the beginning of a control group, fill in the necessary calculations after the comments. In the typical program, the end of a group is the best place to accumulate to higher levels and print group totals.
Principle 7: You must force the highest level control break before the first record and after the last record of the input dataset. Besides the routines for each control-break level, you will need routines for the beginning and end of the input. I refer to these as level 00. Their primary purpose is to force the highest level break, but you may put other beginning-of-file and end-of-file tasks in these subroutines.
Principle 8: In the detail routine, which processes each input record, accumulate to the lowest control break level. This principle could probably be omitted, since the detail processing is the only place to accumulate to the lowest level anyway.
THE REST IS DETAILS
That’s an overview of what it takes to handle control breaks. Using this method, you can easily handle any number of break levels. You will need to determine how you’re going to build output. I used to use externally described printer files, but lately I’ve been using the method Cletus the Codeslinger described in “An Alternative to Externally Described Printer Files.” Cletus’ method works better because it ensures that column headings, detail data, and control-level accumulators are aligned.
To help you see how it all fits together, here’s an example program from the template. The example is made up of five source members:
Member | Type |
CBEXAMPLE2 | SQLRPGLE |
CBPRINTF | PRTF |
INVDTL | PF |
INVHDR | PF |
PSDS | RPGLE |
For an idea of the output, view the report.txt file. You may have to modify bits and pieces of it. For example, there is a /COPY directive to put the PSDS member into the CBEXAMPLE2 member. Make sure it points to the proper location of PSDS.
THERE’S NO SUBSTITUTE FOR DESIGN
A program I had to work on recently had two level breaks, one was done with the L1 level indicator, the other by comparing to a save field. It was not an easy program to modify, but that’s what comes of a lack of design. Since control-break reports are so common in business programming, I encourage you to adopt a control-break design that everyone in your shop can use.