COBOL OCCURS and subscripts: a table of monthly totals
OCCURS builds a table; subscripts index it from 1. A worked program sums twelve monthly totals, finds the best month, fills a 3-by-4 grid and uses DEPENDING ON.
COBOL has no array type. It has the OCCURS clause, which says "this item repeats n times", and subscripts in parentheses to reach each repetition. This tutorial builds a twelve-entry table of monthly sales totals, walks it with PERFORM VARYING to print, sum and find the largest month, then adds a four-entry quarterly table computed with expression subscripts, a three-by-four grid to show two dimensions, and an OCCURS DEPENDING ON table to show the variable-length form. The listing was desk-checked against the GnuCOBOL 3.2 Programmer's Guide; cobc is not installed where we wrote this, so the output below is derived from the documented rules and the arithmetic was checked in Python. The loop mechanics come from the PERFORM tutorial and the record structure from group and elementary items.
The program
>>SOURCE FORMAT IS FREE
*> cobol-occurs-subscripts-table.cob — monthly totals in an OCCURS table
*> Dialect: GnuCOBOL 3.x, free format
*> Status : desk-checked against the GnuCOBOL 3.2 Programmer's Guide;
*> NOT compiled (no cobc on the authoring machine)
*> Inputs : none (the twelve monthly figures are VALUE clauses)
*> Outputs: DISPLAY lines on the console
*> Build : cobc -x cobol-occurs-subscripts-table.cob
*> Run : ./cobol-occurs-subscripts-table
*> (Windows: cobol-occurs-subscripts-table.exe)
IDENTIFICATION DIVISION.
PROGRAM-ID. MONTHLY-TOTALS.
ENVIRONMENT DIVISION.
DATA DIVISION.
WORKING-STORAGE SECTION.
*> Twelve figures given compile-time values, then viewed as a table
*> through REDEFINES (the technique the Programmer's Guide documents
*> under VALUE).
01 Month-Values.
05 FILLER PIC 9(7)V99 VALUE 125430.50.
05 FILLER PIC 9(7)V99 VALUE 98210.00.
05 FILLER PIC 9(7)V99 VALUE 143875.25.
05 FILLER PIC 9(7)V99 VALUE 110004.10.
05 FILLER PIC 9(7)V99 VALUE 156320.00.
05 FILLER PIC 9(7)V99 VALUE 171009.99.
05 FILLER PIC 9(7)V99 VALUE 132500.00.
05 FILLER PIC 9(7)V99 VALUE 99875.40.
05 FILLER PIC 9(7)V99 VALUE 148200.75.
05 FILLER PIC 9(7)V99 VALUE 160010.00.
05 FILLER PIC 9(7)V99 VALUE 189430.20.
05 FILLER PIC 9(7)V99 VALUE 210655.55.
01 Month-Table REDEFINES Month-Values.
05 Month-Total OCCURS 12 TIMES PIC 9(7)V99.
01 Month-Names-Values PIC X(36)
VALUE "JANFEBMARAPRMAYJUNJULAUGSEPOCTNOVDEC".
01 Month-Names REDEFINES Month-Names-Values.
05 Month-Name OCCURS 12 TIMES PIC X(3).
*> A table filled at run time.
01 Quarter-Table.
05 Quarter-Total OCCURS 4 TIMES PIC 9(8)V99.
*> A two-dimensional table: OCCURS inside OCCURS.
01 Grid.
05 Grid-Row OCCURS 3 TIMES.
10 Grid-Cell OCCURS 4 TIMES PIC 9(3).
*> A variable-length table: room for 12, but only Active-Months count.
77 Active-Months PIC 99 VALUE 9.
01 YTD-Table.
05 YTD-Total OCCURS 1 TO 12 TIMES
DEPENDING ON Active-Months
PIC 9(7)V99.
77 M PIC 99.
77 Q PIC 9.
77 R PIC 9.
77 C PIC 9.
77 Max-Month PIC 99 VALUE 1.
77 Grand-Total PIC 9(9)V99 VALUE 0.
77 Show-Amt PIC Z(6)9.99.
77 Show-Total PIC Z(8)9.99.
PROCEDURE DIVISION.
Main-Paragraph.
*> Walk the table once: print, sum, and remember the largest entry.
PERFORM VARYING M FROM 1 BY 1 UNTIL M > 12
MOVE Month-Total(M) TO Show-Amt
DISPLAY Month-Name(M) " " Show-Amt
ADD Month-Total(M) TO Grand-Total
IF Month-Total(M) > Month-Total(Max-Month)
MOVE M TO Max-Month
END-IF
END-PERFORM
MOVE Grand-Total TO Show-Total
DISPLAY "Year total " Show-Total
DISPLAY "Best month " Month-Name(Max-Month)
*> A subscript is a literal, an identifier, or an arithmetic expression.
DISPLAY "First month " Month-Name(1)
MOVE 6 TO M
DISPLAY "Month M + 1 " Month-Name(M + 1)
PERFORM VARYING Q FROM 1 BY 1 UNTIL Q > 4
COMPUTE Quarter-Total(Q) = Month-Total(3 * Q - 2)
+ Month-Total(3 * Q - 1)
+ Month-Total(3 * Q)
MOVE Quarter-Total(Q) TO Show-Total
DISPLAY "Quarter " Q " " Show-Total
END-PERFORM
*> Two subscripts for two dimensions; AFTER drives the inner loop.
PERFORM VARYING R FROM 1 BY 1 UNTIL R > 3
AFTER C FROM 1 BY 1 UNTIL C > 4
COMPUTE Grid-Cell(R, C) = R * 10 + C
END-PERFORM
PERFORM VARYING R FROM 1 BY 1 UNTIL R > 3
DISPLAY "Row " R ": " Grid-Cell(R, 1) " " Grid-Cell(R, 2)
" " Grid-Cell(R, 3) " " Grid-Cell(R, 4)
END-PERFORM
*> OCCURS DEPENDING ON: the storage is always the maximum, but the
*> table logically has Active-Months entries.
MOVE 0 TO Grand-Total
PERFORM VARYING M FROM 1 BY 1 UNTIL M > Active-Months
MOVE Month-Total(M) TO YTD-Total(M)
ADD YTD-Total(M) TO Grand-Total
END-PERFORM
MOVE Grand-Total TO Show-Total
DISPLAY "YTD through " Month-Name(Active-Months) " " Show-Total
STOP RUN.
What OCCURS declares
Section 6.9.31 of the Programmer's Guide describes OCCURS as the clause that "creates a data structure called a table, where entries in that structure repeat multiple times." 05 Month-Total OCCURS 12 TIMES PIC 9(7)V99. reserves twelve consecutive nine-byte fields, 108 bytes in all, and each one is reached as Month-Total(1) through Month-Total(12). Subscripts start at 1, not 0, and the guide is explicit that a subscript may be "a numeric literal, arithmetic expression or numeric identifier enclosed within parenthesis." The program uses all three: Month-Name(1), Month-Total(M), and Month-Total(3 * Q - 2).
The clause has one placement rule worth memorising: it "cannot be specified in a data description entry that has a level number of 01, 66, 77, or 88." A table always lives under a group, which is why every table here is a 05 under an 01. IBM's language reference states the same restriction for Enterprise COBOL.
Giving a table initial values
There is no array literal. The Programmer's Guide, under VALUE (6.9.51), lists the standard techniques, and the one used here is the portable one: write the values as a sequence of FILLER items with VALUE clauses, then REDEFINES that group as the table. Month-Values is twelve nine-byte numeric fields laid end to end, and Month-Table is the same 108 bytes read through an OCCURS. The month names use the same trick with a single 36-character literal sliced into twelve three-character entries. GnuCOBOL also accepts a VALUE clause directly on an OCCURS item, which the guide notes "many COBOL compilers do not allow"; the REDEFINES form compiles everywhere.
Walking the table
The first loop is the standard shape: PERFORM VARYING M FROM 1 BY 1 UNTIL M > 12. Section 7.8.29.3 describes the sequence: the FROM value is moved to M, the UNTIL condition is tested before each iteration (WITH TEST BEFORE is the default), the body runs, then BY 1 is added and the test repeats. Writing UNTIL M > 12 rather than UNTIL M = 12 is the habit the guide recommends, because with the default test-before behaviour an equality test stops one iteration short. Inside the body, each entry is printed through an edited picture, added to the running total, and compared with the entry at Max-Month, so the largest month is found in the same pass.
The quarterly loop shows an expression subscript doing real work. Quarter Q is the sum of months 3Q-2, 3Q-1 and 3Q, and COBOL evaluates each expression before using it as an index. The arithmetic is ordinary; what is unusual to newcomers is that it happens inside the parentheses of a data name.
The grid demonstrates nesting. Grid-Row OCCURS 3 TIMES contains Grid-Cell OCCURS 4 TIMES, so every cell needs two subscripts, row first: Grid-Cell(R, C). The nested loop is a single PERFORM with an AFTER clause; the guide's own example in section 7.8.29.3 uses exactly this form inline. One portability note: IBM's PERFORM documentation says that "if procedure-name-1 is omitted, the AFTER phrase must not be specified," so on Enterprise COBOL the AFTER form needs an out-of-line paragraph. GnuCOBOL accepts it inline.
OCCURS DEPENDING ON is the variable-length form. OCCURS 1 TO 12 TIMES DEPENDING ON Active-Months allocates the full twelve entries, and the guide states that "at execution time the value of identifier-1 will determine how many of the table elements are accessible." The loop runs to Active-Months, which is 9, so the year-to-date figure covers January to September. In file records this form matters because a group item containing the table changes its logical length with the count, which is how variable-length mainframe records are described.
Loop table
| Loop | Control | Iterations | Subscripts used |
|---|---|---|---|
| Print, sum, max | M from 1 to 12 | 12 | Month-Total(M), Month-Total(Max-Month) |
| Quarters | Q from 1 to 4 | 4 | Month-Total(3 * Q - 2) and two more |
| Grid fill | R 1 to 3, AFTER C 1 to 4 | 12 | Grid-Cell(R, C) |
| Grid print | R from 1 to 3 | 3 | Grid-Cell(R, 1) to Grid-Cell(R, 4) |
| Year to date | M from 1 to Active-Months | 9 | YTD-Total(M) |
Compile and run
cobc -x cobol-occurs-subscripts-table.cob
./cobol-occurs-subscripts-table
The listing starts with a >>SOURCE FORMAT IS FREE directive in column 8, so it compiles with or without the -free option.
Worked example
Expected output, with the twelve values, the quarter sums, the year total and the year-to-date figure checked in Python (the COBOL itself was not run):
JAN 125430.50
FEB 98210.00
MAR 143875.25
APR 110004.10
MAY 156320.00
JUN 171009.99
JUL 132500.00
AUG 99875.40
SEP 148200.75
OCT 160010.00
NOV 189430.20
DEC 210655.55
Year total 1745521.74
Best month DEC
First month JAN
Month M + 1 JUL
Quarter 1 367515.75
Quarter 2 437334.09
Quarter 3 380576.15
Quarter 4 560095.75
Row 1: 011 012 013 014
Row 2: 021 022 023 024
Row 3: 031 032 033 034
YTD through SEP 1185425.99
Show-Amt is ten characters wide and Show-Total twelve, which is why the amounts sit in aligned columns. The grid cells print with their leading zeros because PIC 9(3) is an unedited numeric field and DISPLAY shows it as stored; R and Q are single digits, so they print without padding.
Limitations
The program never checks a subscript against the table bounds, and neither does COBOL by default. Month-Total(13) would silently address the nine bytes after the table, which in this layout is the start of Month-Names-Values; on a mainframe that is the classic cause of corrupted totals that nobody can reproduce. GnuCOBOL can add run-time subscript checking through its debugging compiler options (the shipped GnuCOBOL Manual lists them under compiler options), and the practice lessons on this site compile with those checks during development. Two further points: the largest-month comparison keeps the first month on a tie, because the test is strictly greater; and the OCCURS DEPENDING ON example only reads and writes entries up to the current count, which is the rule, not a convenience. IBM Enterprise COBOL permits up to seven nested OCCURS levels according to its PERFORM documentation; this program uses two.
Index terms cobol, gnucobol, occurs, subscript, table