Two-dimensional tables
Plenty of business data is a grid: sales by region and quarter, rates by
zone and weight band, premiums by age band and risk class. In COBOL
you build a grid by putting an OCCURS inside another OCCURS.
IDENTIFICATION DIVISION.
PROGRAM-ID. TWODIM.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-SALES-TABLE.
05 WS-REGION OCCURS 2 TIMES.
10 WS-QTR-SALES PIC 9(5) OCCURS 4 TIMES.
01 WS-R PIC 9.
01 WS-Q PIC 9.
01 WS-ROW-TOTAL PIC 9(6).
01 WS-COL-TOTAL PIC 9(6).
PROCEDURE DIVISION.
INITIALIZE WS-SALES-TABLE
MOVE 100 TO WS-QTR-SALES(1, 1)
MOVE 250 TO WS-QTR-SALES(1, 3)
MOVE 400 TO WS-QTR-SALES(2, 1)
MOVE 50 TO WS-QTR-SALES(2, 4)
DISPLAY "WHOLE TABLE: " WS-SALES-TABLE
PERFORM VARYING WS-R FROM 1 BY 1 UNTIL WS-R > 2
MOVE ZERO TO WS-ROW-TOTAL
PERFORM VARYING WS-Q FROM 1 BY 1 UNTIL WS-Q > 4
ADD WS-QTR-SALES(WS-R, WS-Q) TO WS-ROW-TOTAL
END-PERFORM
DISPLAY "REGION " WS-R " TOTAL " WS-ROW-TOTAL
END-PERFORM
MOVE ZERO TO WS-COL-TOTAL
PERFORM VARYING WS-R FROM 1 BY 1 UNTIL WS-R > 2
ADD WS-QTR-SALES(WS-R, 1) TO WS-COL-TOTAL
END-PERFORM
DISPLAY "Q1 TOTAL " WS-COL-TOTAL
STOP RUN.
Output:
WHOLE TABLE: 0010000000002500000000400000000000000050
REGION 1 TOTAL 000350
REGION 2 TOTAL 000450
Q1 TOTAL 000500
Reading the declaration
WS-REGIONoccurs 2 times. It is a group, one row per region.- Inside each row,
WS-QTR-SALESoccurs 4 times, one column per quarter. - A reference to
WS-QTR-SALESneeds two subscripts, outer first:WS-QTR-SALES(region, quarter). The comma is optional.(WS-R WS-Q)also works, but the comma makes it easier to read. WS-REGION(2)on its own is a whole row: 20 bytes.
How it sits in memory
The WHOLE TABLE line shows the layout: all four quarters of region 1,
then all four of region 2. This is called row-major order: the last
subscript varies fastest. It matters when you hard-code a grid with
REDEFINES: write the values one row at a time.
01 WS-RATE-VALUES.
05 FILLER PIC X(16) VALUE "0450060008501200".
05 FILLER PIC X(16) VALUE "0650087512001850".
01 WS-RATE-TABLE REDEFINES WS-RATE-VALUES.
05 WS-ZONE OCCURS 2 TIMES.
10 WS-RATE PIC 9(2)V99 OCCURS 4 TIMES.
Each FILLER is one zone's row of four 9(2)V99 rates, so WS-RATE(2, 3)
is 12.00.
Row totals and column totals
The example shows the two classic traversals:
- Row totals (per region): outer loop over rows, inner loop over columns, and reset the total at the start of each row.
- Column totals (per quarter): fix the column, loop over the rows.
PERFORM ... VARYING ... AFTER from the loops module does the "every cell"
walk in one statement, which is handy for initialising or printing a grid.
Two subscripts, two ranges
Each subscript must stay within its own OCCURS. With WS-R = 1 and
WS-Q = 5, WS-QTR-SALES(WS-R, WS-Q) does not fail. It silently reads
WS-QTR-SALES(2, 1), because that is the next 5 bytes in memory. Validate both values from input before
using them.
On the job
Deeply nested tables are allowed, but you will rarely see more than
three levels of OCCURS. When you meet a big
multi-level table in a copybook, sketch it as a grid on paper before
changing anything.
Your task
A courier charges by delivery zone and weight band:
| Zone | Up to 1 kg | Up to 5 kg | Up to 10 kg | Over 10 kg |
|---|---|---|---|---|
| 1 | 4.50 | 6.00 | 8.50 | 12.00 |
| 2 | 6.50 | 8.75 | 12.00 | 18.50 |
| 3 | 9.00 | 12.50 | 17.25 | 26.00 |
Hard-code this as a two-dimensional table (zone × band) using REDEFINES.
Read parcels, one per line, until END. Each line is a zone (9) then a
weight in kg (9(3)V9, 4 digits, one implied decimal): 20025 is zone 2,
2.5 kg. For each parcel, pick the band ("up to" includes the limit, so
exactly 1.0 kg is band 1), look up the charge and display:
ZONE 2 WEIGHT 2.5 CHARGE 8.75
A zone outside 1–3 gets INVALID ZONE instead of a charge and adds
nothing to the total:
ZONE 5 WEIGHT 1.0 INVALID ZONE
Finish with TOTAL CHARGES: and the total. The starter has the input
record, the edited output fields and the main loop.