COBOL Textbook

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.

Published
Updated
Dialect
GnuCOBOL 3.x, free format
Status
Desk-checked, not compiled
Length
1,062 words, 5 min

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.
Listing 2.3-1. Lines 1 to 102 of cobol-occurs-subscripts-table.cob.

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

Table 2.3-1
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