22 min read

In nearly every non-trivial program, you will need to work with collections of related data: a list of monthly totals, a set of tax brackets, a roster of employees, or a grid of quarterly sales by region. In languages like C, Java, or Python, you...

Chapter 10: Tables and Arrays -- OCCURS, SEARCH, and Multi-Dimensional Data

Introduction

In nearly every non-trivial program, you will need to work with collections of related data: a list of monthly totals, a set of tax brackets, a roster of employees, or a grid of quarterly sales by region. In languages like C, Java, or Python, you would reach for arrays, lists, or dictionaries. In COBOL, the equivalent mechanism is the table -- a structured, fixed-layout collection of data elements defined using the OCCURS clause.

Tables are one of COBOL's most powerful and most frequently used features in production business systems. Payroll programs use tax bracket tables. Banking systems use account type lookup tables. Insurance applications use rate tables spanning dozens of dimensions. If you have worked through the earlier chapters on data definitions and control flow, you are ready to learn how COBOL organizes, accesses, searches, and manipulates collections of repeating data.

This chapter covers:

  • The OCCURS clause for defining one-dimensional tables
  • Subscripts and indexes for accessing table elements
  • Multi-dimensional tables using nested OCCURS
  • The SEARCH statement for sequential (linear) table lookup
  • The SEARCH ALL statement for binary search
  • OCCURS DEPENDING ON for variable-length tables
  • The INDEXED BY clause and SET statement
  • Common table patterns used in production COBOL systems
  • Memory considerations, range checking, and common mistakes

By the end of this chapter, you will be able to define tables of any shape, populate them from files or hardcoded values, search them efficiently, and apply them to solve real business problems.


10.1 What Is a Table in COBOL?

A table in COBOL is a contiguous block of memory containing multiple occurrences of the same data structure. If you are familiar with arrays in other languages, a COBOL table is conceptually similar -- but with some important differences:

  1. Tables are defined in the DATA DIVISION, not the PROCEDURE DIVISION. Their size and structure are declared statically.
  2. Tables are 1-indexed, not 0-indexed. The first element is element 1.
  3. Tables can be multi-dimensional (up to 7 dimensions per the COBOL standard).
  4. Tables can contain complex structures, not just simple scalar values. A single table entry can include multiple fields of different types.
  5. Tables support built-in search operations through the SEARCH and SEARCH ALL statements.

Why Tables Matter

Consider a program that calculates shipping costs. Without tables, you might write:

       IF WS-ZONE = 1
           MOVE 5.99 TO WS-SHIPPING-COST
       ELSE IF WS-ZONE = 2
           MOVE 8.99 TO WS-SHIPPING-COST
       ELSE IF WS-ZONE = 3
           MOVE 12.99 TO WS-SHIPPING-COST
       ...

With 50 zones, this becomes unmaintainable. With a table, you simply write:

       MOVE WS-ZONE-RATE(WS-ZONE) TO WS-SHIPPING-COST

Tables replace cascading conditionals with direct access, making code cleaner, shorter, and easier to maintain.


10.2 The OCCURS Clause: Defining One-Dimensional Tables

The OCCURS clause tells the COBOL compiler to allocate multiple occurrences of a data item. It is specified in the DATA DIVISION on the item you wish to repeat.

Basic Syntax

       01  WS-MONTHLY-TOTALS.
           05  WS-MONTH-TOTAL     PIC 9(7)V99
                                  OCCURS 12 TIMES.

This creates 12 separate WS-MONTH-TOTAL fields, each with a picture of 9(7)V99. They are stored contiguously in memory, occupying 12 x 9 = 108 bytes.

Rules for OCCURS

  1. OCCURS cannot be specified at the 01 level in the WORKING-STORAGE SECTION. It can appear at levels 02 through 49.
  2. The number after OCCURS must be a positive integer literal (or, with DEPENDING ON, a data-name).
  3. The OCCURS clause defines the maximum number of occurrences.
  4. All occurrences share the same PIC clause and subordinate structure.

Table with Structured Entries

Tables often contain multi-field entries:

       01  WS-EMPLOYEE-TABLE.
           05  WS-EMPLOYEE-ENTRY  OCCURS 100 TIMES.
               10  WS-EMP-ID      PIC 9(5).
               10  WS-EMP-NAME    PIC X(25).
               10  WS-EMP-SALARY  PIC 9(7)V99.

Here, each occurrence of WS-EMPLOYEE-ENTRY contains three fields. The table has 100 entries, each 39 bytes long (5 + 25 + 9), for a total of 3,900 bytes.

Accessing Table Elements with Subscripts

You access a specific occurrence by appending a subscript in parentheses:

       MOVE "ALICE JOHNSON" TO WS-EMP-NAME(1)
       MOVE 55000.00 TO WS-EMP-SALARY(3)
       DISPLAY WS-EMP-ID(WS-SUB)

The subscript can be: - An integer literal: WS-EMP-NAME(1) - A data-name containing an integer: WS-EMP-NAME(WS-SUB) - An arithmetic expression: WS-EMP-NAME(WS-SUB + 1) (COBOL 85+)

Important: Subscripts in COBOL start at 1, not 0. WS-EMP-NAME(0) is invalid and will cause a runtime error or unpredictable behavior.

Complete Example: Basic Table

See code/example-01-basic-table.cob for a full working program that demonstrates three common patterns:

  1. A hardcoded lookup table using REDEFINES to convert month numbers to month names
  2. A runtime-loaded table where employee data is populated during execution
  3. An accumulator table that aggregates monthly sales totals

The program uses PERFORM VARYING to iterate through table elements, which is the standard COBOL idiom for processing all elements of a table:

       PERFORM VARYING WS-SUBSCRIPT FROM 1 BY 1
           UNTIL WS-SUBSCRIPT > 12
           DISPLAY WS-MONTH-NAME(WS-SUBSCRIPT)
       END-PERFORM

10.3 Loading Tables: VALUE Clauses, REDEFINES, and File Input

One of the most common questions beginners have is: "How do I put data into a table?" COBOL provides several approaches, each suited to different situations.

Method 1: The REDEFINES Technique (Pre-COBOL 2002)

Before COBOL 2002, you could not specify VALUE clauses directly on items with OCCURS. The classic workaround uses REDEFINES:

       01  WS-MONTH-DATA.
           05  FILLER  PIC X(12) VALUE "01January   ".
           05  FILLER  PIC X(12) VALUE "02February  ".
           05  FILLER  PIC X(12) VALUE "03March     ".
           *> ... remaining months ...

       01  WS-MONTH-TABLE REDEFINES WS-MONTH-DATA.
           05  WS-MONTH-ENTRY  OCCURS 12 TIMES.
               10  WS-MONTH-NUM   PIC 99.
               10  WS-MONTH-NAME  PIC X(10).

The first 01 level defines the raw data as a series of FILLER items with VALUE clauses. The second 01 level REDEFINES the same memory as a table structure. This is the most widely used technique in legacy COBOL and works with every compiler.

Key points: - Both 01 items must be the same total length. - The REDEFINES item must immediately follow the item it redefines. - Data alignment is critical -- each FILLER entry must match the table entry size exactly.

Method 2: Inline VALUE with OCCURS (COBOL 2002+)

COBOL 2002 introduced the ability to specify VALUE clauses directly on items that have OCCURS:

       01  WS-DAY-NAMES.
           05  WS-DAY-NAME PIC X(9) OCCURS 7 TIMES
               VALUE "Sunday" "Monday" "Tuesday"
                     "Wednesday" "Thursday" "Friday"
                     "Saturday".

This is cleaner and less error-prone, but it is not supported by all compilers, particularly older mainframe COBOL compilers. If you are writing portable code or maintaining legacy systems, prefer the REDEFINES technique.

Method 3: Loading from a File

For large tables or tables whose data changes frequently, loading from a file is the best approach:

       OPEN INPUT RATE-FILE
       MOVE ZERO TO WS-RATE-COUNT

       PERFORM UNTIL WS-EOF = 'Y' OR WS-RATE-COUNT >= 500
           READ RATE-FILE INTO WS-RATE-RECORD
               AT END
                   MOVE 'Y' TO WS-EOF
               NOT AT END
                   ADD 1 TO WS-RATE-COUNT
                   MOVE WS-RR-CODE TO WS-TBL-CODE(WS-RATE-COUNT)
                   MOVE WS-RR-RATE TO WS-TBL-RATE(WS-RATE-COUNT)
           END-READ
       END-PERFORM

       CLOSE RATE-FILE

Method 4: INITIALIZE Statement

To set all occurrences to their default values:

       INITIALIZE WS-EMPLOYEE-TABLE

This sets all alphanumeric fields to spaces and all numeric fields to zeros throughout the entire table.

Method 5: Procedural Loading

Sometimes you compute table values at runtime:

       PERFORM VARYING WS-SUB FROM 1 BY 1
           UNTIL WS-SUB > 10
           COMPUTE WS-SQUARE(WS-SUB) = WS-SUB * WS-SUB
       END-PERFORM

10.4 Subscripts vs. Indexes: Syntax and Performance

COBOL provides two mechanisms for referencing table elements: subscripts and indexes. Understanding the difference is important for both correctness and performance.

Subscripts

A subscript is an ordinary integer data item (or literal) used in parentheses to identify a table occurrence:

       01  WS-SUB     PIC 99 VALUE ZERO.
       ...
       MOVE "HELLO" TO WS-TABLE-ITEM(WS-SUB)

Characteristics: - Defined as a regular data item (PIC 9, PIC 99, etc.) - Contains the occurrence number (1, 2, 3, ...) - Can be used in any arithmetic statement: ADD 1 TO WS-SUB - Can be displayed: DISPLAY WS-SUB - At runtime, the compiler must multiply: offset = (subscript - 1) * entry_size

Indexes

An index is a special data type defined with the INDEXED BY clause on the table itself:

       05  WS-TABLE-ENTRY  OCCURS 100 TIMES
                            INDEXED BY WS-TBL-IDX.

Characteristics: - Defined with the table, not as a separate data item - Internally stores a byte displacement (offset from the start of the table) - Manipulated only with SET, SEARCH, or PERFORM VARYING - Cannot be used in arithmetic (ADD, SUBTRACT, COMPUTE) - Cannot be directly displayed - No multiplication at runtime -- the displacement is already computed

Performance Comparison

When you use a subscript, every table access requires a multiplication:

offset = (subscript_value - 1) * entry_size_in_bytes

When you use an index, the byte offset is stored directly. Moving to the next element requires only addition:

SET WS-TBL-IDX UP BY 1   (adds entry_size to internal offset)

For a table with 10,000 entries accessed in a tight loop, the difference can be significant. On modern mainframe hardware with tight I/O loops processing millions of records, indexes can provide measurable performance improvements.

The SET Statement

Indexes are manipulated exclusively through the SET statement:

      * Set index to a specific occurrence number
        SET WS-TBL-IDX TO 1
        SET WS-TBL-IDX TO WS-SUB       *> from a data item

      * Increment / decrement index
        SET WS-TBL-IDX UP BY 1
        SET WS-TBL-IDX DOWN BY 3

      * Copy one index to another
        SET WS-TBL-IDX-2 TO WS-TBL-IDX

      * Extract occurrence number from index
        SET WS-SUB TO WS-TBL-IDX       *> integer := index

USAGE IS INDEX Data Items

You can define standalone index data items to save and restore index values:

       01  WS-SAVED-INDEX   USAGE IS INDEX.
       ...
       SET WS-SAVED-INDEX TO WS-TBL-IDX    *> save
       ...  (other processing that changes WS-TBL-IDX) ...
       SET WS-TBL-IDX TO WS-SAVED-INDEX    *> restore

See code/example-06-indexed-table.cob for a complete demonstration of index manipulation, including saving/restoring indexes, using indexes with two-dimensional tables, and a practical currency converter application.

When to Use Which

Situation Recommended
Performance-critical table loops Indexes
SEARCH or SEARCH ALL Indexes (required)
Need to display the occurrence number Subscripts
Need the value in calculations Subscripts
Simple, small tables Either works
Both access patterns needed Define both on the same table

You can define both subscript variables and indexes for the same table and use whichever is appropriate in each context.


10.5 Multi-Dimensional Tables

Real-world data often has more than one dimension. Sales data might be organized by region and quarter. A grade book has students and exams. COBOL supports multi-dimensional tables through nested OCCURS clauses -- an OCCURS within an OCCURS.

Two-Dimensional Tables

       01  WS-SALES-TABLE.
           05  WS-REGION        OCCURS 4 TIMES.
               10  WS-QUARTER-SALES  PIC 9(7)V99
                                     OCCURS 4 TIMES.

This creates a 4 x 4 grid: 4 regions, each with 4 quarters. Access uses two subscripts:

       MOVE 125000.00 TO WS-QUARTER-SALES(1, 1)   *> Region 1, Q1
       MOVE 138500.50 TO WS-QUARTER-SALES(1, 2)   *> Region 1, Q2
       DISPLAY WS-QUARTER-SALES(3, 4)              *> Region 3, Q4

The subscripts are listed outermost dimension first, left to right: (region, quarter).

Three-Dimensional Tables

       01  WS-3D-TABLE.
           05  WS-REGION         OCCURS 4 TIMES.
               10  WS-QUARTER    OCCURS 4 TIMES.
                   15  WS-PRODUCT-SALES  PIC 9(7)V99
                                         OCCURS 3 TIMES.

Access: WS-PRODUCT-SALES(region, quarter, product)

The Seven-Dimension Limit

The COBOL standard permits up to 7 levels of OCCURS nesting. In practice, tables beyond 3 dimensions are rare. If you find yourself needing more than 3 dimensions, consider whether a different data organization might be clearer.

Nested PERFORM VARYING for Multi-Dimensional Access

To process all elements of a multi-dimensional table, use nested PERFORM VARYING:

       PERFORM VARYING WS-REG FROM 1 BY 1
           UNTIL WS-REG > 4
           PERFORM VARYING WS-QTR FROM 1 BY 1
               UNTIL WS-QTR > 4
               DISPLAY WS-QUARTER-SALES(WS-REG, WS-QTR)
           END-PERFORM
       END-PERFORM

The outer PERFORM controls the first (leftmost) subscript; the inner PERFORM controls the second. For three dimensions, you add a third nested level.

Memory Layout

Multi-dimensional tables are stored in row-major order. For a 4 x 4 table, the memory layout is:

(1,1) (1,2) (1,3) (1,4) (2,1) (2,2) (2,3) (2,4) (3,1) ...

All elements of the first row are contiguous, followed by all elements of the second row, and so on. This is important for performance: accessing elements that vary in the rightmost subscript (walking across a row) is more cache-friendly than varying the leftmost subscript (walking down a column).

See code/example-02-multi-dim.cob for a complete program demonstrating two-dimensional and three-dimensional tables with quarterly sales data, nested PERFORM VARYING traversal, and cross-tabulation reports.


10.6 The SEARCH Statement: Sequential Table Lookup

The SEARCH statement provides a built-in mechanism for sequential (linear) search through a table. It scans entries one at a time, starting from the current index position, until a match is found or the end of the table is reached.

Requirements

  • The table must have an INDEXED BY clause.
  • You must SET the index to the starting position before each SEARCH.

Basic Syntax

       SET WS-STATE-IDX TO 1

       SEARCH WS-STATE-ENTRY
           AT END
               DISPLAY "Not found"
           WHEN WS-STATE-CODE(WS-STATE-IDX) = "TX"
               DISPLAY "Found Texas"
       END-SEARCH

How SEARCH Works

  1. The SEARCH statement checks if the index is already beyond the table (past the last occurrence). If so, the AT END clause executes immediately.
  2. It evaluates the WHEN condition(s) for the current index position.
  3. If a WHEN condition is true, the associated statements execute, and the SEARCH ends. The index points to the found entry.
  4. If no WHEN condition is true, the index is automatically incremented by 1, and the process repeats from step 1.

Multiple WHEN Clauses

You can specify multiple WHEN clauses. The first one that matches wins:

       SEARCH WS-STATE-ENTRY
           AT END
               DISPLAY "No NE or SE state found"
           WHEN WS-TAX-REGION(WS-STATE-IDX) = "NE"
               DISPLAY "Northeast: " WS-STATE-NAME(WS-STATE-IDX)
           WHEN WS-TAX-REGION(WS-STATE-IDX) = "SE"
               DISPLAY "Southeast: " WS-STATE-NAME(WS-STATE-IDX)
       END-SEARCH

All WHEN conditions are evaluated at each position. If the first entry has region "SE", the second WHEN fires even though the first WHEN was checked first.

The VARYING Clause

By default, SEARCH increments the index defined on the table being searched. You can use VARYING to simultaneously increment another index or subscript:

       SEARCH WS-CODE-ENTRY VARYING WS-DESC-IDX
           AT END
               DISPLAY "Not found"
           WHEN WS-CODE(WS-CODE-IDX) = WS-INPUT-CODE
               DISPLAY WS-DESCRIPTION(WS-DESC-IDX)
       END-SEARCH

This is useful when two parallel tables share the same layout and you need to find an entry in one table and access the corresponding entry in another.

SEARCH stops at the first match. To find all matching entries, you can loop:

       SET WS-STATE-IDX TO 1
       PERFORM UNTIL WS-STATE-IDX > 50
           SEARCH WS-STATE-ENTRY
               AT END
                   SET WS-STATE-IDX TO 51   *> exit loop
               WHEN WS-TAX-REGION(WS-STATE-IDX) = "NE"
                   ADD 1 TO WS-NE-COUNT
                   SET WS-STATE-IDX UP BY 1  *> advance past match
           END-SEARCH
       END-PERFORM

Performance

SEARCH performs a linear scan: O(n) in the worst case, where n is the number of table entries. For small tables (under 50 entries), this is perfectly adequate. For large tables (hundreds or thousands of entries), consider SEARCH ALL (binary search) instead.

See code/example-03-search.cob for a complete program demonstrating all aspects of the SEARCH statement, including a 50-state lookup table, multiple WHEN clauses, counting pattern, and interactive lookup.


The SEARCH ALL statement implements a binary search algorithm, which is dramatically faster than linear search for large tables. Instead of checking every entry, binary search repeatedly divides the search space in half.

Requirements for SEARCH ALL

Binary search imposes strict requirements:

  1. The table must have an ASCENDING KEY IS or DESCENDING KEY IS clause on its OCCURS:
       05  WS-ZIP-ENTRY  OCCURS 1000 TIMES
                          ASCENDING KEY IS WS-ZIP-CODE
                          INDEXED BY WS-ZIP-IDX.
           10  WS-ZIP-CODE   PIC X(5).
           10  WS-ZIP-CITY   PIC X(20).
  1. The table data must actually be sorted in the specified order. COBOL trusts you on this -- it does not verify the sort order. If the data is not sorted, SEARCH ALL will produce incorrect results silently.

  2. Only equality conditions (=) are allowed in the WHEN clause. You cannot use >, <, >=, or <=.

  3. Only one WHEN clause is permitted (unlike SEARCH, which allows multiple).

  4. Compound keys require all key fields to appear in the WHEN clause, connected by AND.

Basic Syntax

       SEARCH ALL WS-ZIP-ENTRY
           AT END
               DISPLAY "ZIP code not found"
           WHEN WS-ZIP-CODE(WS-ZIP-IDX) = "60601"
               DISPLAY "Found: " WS-ZIP-CITY(WS-ZIP-IDX)
       END-SEARCH

Note: You do not need to SET the index to 1 before SEARCH ALL. The binary search algorithm manages the index automatically.

Compound Keys

When a table has multiple ascending/descending keys, the WHEN clause must test all of them:

       05  WS-PROD-ENTRY  OCCURS 500 TIMES
                           ASCENDING KEY IS
                               WS-PROD-CATEGORY
                               WS-PROD-ID
                           INDEXED BY WS-PROD-IDX.
           10  WS-PROD-CATEGORY  PIC X(4).
           10  WS-PROD-ID        PIC X(4).
           10  WS-PROD-DESC      PIC X(20).

       SEARCH ALL WS-PROD-ENTRY
           AT END
               DISPLAY "Product not found"
           WHEN WS-PROD-CATEGORY(WS-PROD-IDX) = "ELEC"
            AND WS-PROD-ID(WS-PROD-IDX) = "1002"
               DISPLAY WS-PROD-DESC(WS-PROD-IDX)
       END-SEARCH

DESCENDING KEY

For tables sorted in descending order:

       05  WS-SCORE-ENTRY  OCCURS 100 TIMES
                            DESCENDING KEY IS WS-SCORE
                            INDEXED BY WS-SCORE-IDX.
           10  WS-SCORE       PIC 9(4).
           10  WS-SCORE-NAME  PIC X(20).

The data must be sorted from highest to lowest. SEARCH ALL still works correctly -- it just expects the opposite order.

Performance Comparison

Table Size SEARCH (linear) max comparisons SEARCH ALL (binary) max comparisons
10 10 4
100 100 7
1,000 1,000 10
10,000 10,000 14
100,000 100,000 17

Binary search is O(log2 n). For a table of 10,000 entries, that is at most 14 comparisons instead of 10,000. The difference is enormous in high-volume batch processing.

Common Mistake: Unsorted Data

The most dangerous mistake with SEARCH ALL is using it on data that is not properly sorted. The binary search algorithm assumes sorted order. With unsorted data, it may: - Report "not found" for entries that exist - Find the wrong entry - Appear to work for some values but fail for others

Always verify your data is sorted before using SEARCH ALL. If loading from a file, either sort the file first or sort the table after loading.

See code/example-04-search-all.cob for a complete program demonstrating binary search with ascending keys, descending keys, compound keys, and an interactive ZIP code lookup.


10.8 OCCURS DEPENDING ON: Variable-Length Tables

Sometimes you do not know how many table entries you will need at compile time. A customer order may have anywhere from 1 to 50 line items. An employee list might have 3 entries today and 200 tomorrow. The OCCURS DEPENDING ON (ODO) clause creates tables whose effective size varies at runtime.

Syntax

       01  WS-ITEM-COUNT        PIC 99    VALUE ZERO.

       01  WS-ORDER-TABLE.
           05  WS-LINE-ITEM     OCCURS 1 TO 50 TIMES
                                DEPENDING ON WS-ITEM-COUNT.
               10  WS-PROD-ID   PIC X(8).
               10  WS-QUANTITY  PIC 9(4).
               10  WS-PRICE     PIC 9(5)V99.

Terminology

  • ODO subject: The data item that controls the count (WS-ITEM-COUNT in the example above). It determines how many occurrences are currently "active."
  • ODO object: The data item with the OCCURS DEPENDING ON clause (WS-LINE-ITEM). Its effective size varies based on the ODO subject.

How It Works

  1. Memory is allocated for the maximum number of occurrences (50 in the example).
  2. The ODO subject (WS-ITEM-COUNT) determines how many occurrences are logically valid at any given time.
  3. When you set WS-ITEM-COUNT to 5, only elements 1 through 5 are considered valid.
  4. FUNCTION LENGTH returns the current logical length, not the maximum.

Rules and Restrictions

  1. The ODO subject must be an integer numeric data item. It cannot be a table element or an item within the ODO object.

  2. The ODO subject must NOT be part of the ODO object. The count field must be defined separately from the table it controls.

  3. The value of the ODO subject must stay within the declared range. Setting WS-ITEM-COUNT to 0 when the minimum is 1, or to 51 when the maximum is 50, causes undefined behavior.

  4. Only the last item at a given level can have OCCURS DEPENDING ON. Items defined after an ODO object in the same group have unpredictable positions because the preceding item's size is variable.

  5. Set the ODO subject before WRITE. When writing a variable-length record, the system uses the ODO subject to determine how many bytes to write.

  6. After READ, the ODO subject is set automatically based on the length of the record that was read.

Variable-Length Records in Files

ODO is commonly used with variable-length file records:

       FD  ORDER-FILE
           RECORD CONTAINS 20 TO 1020 CHARACTERS.

       01  ORDER-RECORD.
           05  OR-HEADER         PIC X(20).
           05  OR-ITEM-COUNT     PIC 99.
           05  OR-LINE-ITEM      OCCURS 1 TO 50 TIMES
                                 DEPENDING ON OR-ITEM-COUNT.
               10  OR-ITEM-DATA  PIC X(20).

When writing, set OR-ITEM-COUNT first -- the system writes only the header plus the indicated number of items. When reading, the system determines how many items were in the record and sets OR-ITEM-COUNT accordingly.

Common Pitfall: Accessing Beyond the Count

Even though memory is allocated for the maximum, accessing elements beyond the current ODO subject value is logically invalid:

       MOVE 3 TO WS-ITEM-COUNT
       MOVE "DATA" TO WS-PROD-ID(5)   *> INVALID! Count is only 3

Some compilers will allow this silently; others will raise a runtime error if subscript range checking is enabled. Either way, the behavior is undefined. Always respect the ODO subject value.

See code/example-05-variable-length.cob for a complete program demonstrating ODO with dynamic employee lists, variable-length order records, and record length calculations.


10.9 PERFORM VARYING with Tables

The PERFORM VARYING statement is the standard way to iterate through table elements. It initializes a subscript or index, tests a condition, executes a block of code, and increments the counter -- all in one statement.

Single-Dimension Iteration

       PERFORM VARYING WS-SUB FROM 1 BY 1
           UNTIL WS-SUB > 100
           ADD WS-AMOUNT(WS-SUB) TO WS-TOTAL
       END-PERFORM

With Indexes

       PERFORM VARYING WS-TBL-IDX FROM 1 BY 1
           UNTIL WS-TBL-IDX > 100
           ADD WS-AMOUNT(WS-TBL-IDX) TO WS-TOTAL
       END-PERFORM

When using indexes with PERFORM VARYING, COBOL handles the index manipulation automatically -- you do not need explicit SET statements.

Multi-Dimensional Iteration

For multi-dimensional tables, nest the PERFORM VARYING:

       PERFORM VARYING WS-ROW FROM 1 BY 1
           UNTIL WS-ROW > 10
           PERFORM VARYING WS-COL FROM 1 BY 1
               UNTIL WS-COL > 5
               DISPLAY WS-CELL(WS-ROW, WS-COL)
           END-PERFORM
       END-PERFORM

AFTER Clause for Compact Syntax

COBOL also supports the AFTER clause for multi-dimensional iteration in a single statement:

       PERFORM 2100-PROCESS-CELL
           VARYING WS-ROW FROM 1 BY 1
           UNTIL WS-ROW > 10
           AFTER WS-COL FROM 1 BY 1
           UNTIL WS-COL > 5

This is equivalent to the nested PERFORM VARYING above but more compact. The AFTER variable (inner loop) cycles through its full range for each value of the VARYING variable (outer loop).

Reverse Iteration

To process a table in reverse order:

       PERFORM VARYING WS-SUB FROM 100 BY -1
           UNTIL WS-SUB < 1
           DISPLAY WS-TABLE-ITEM(WS-SUB)
       END-PERFORM

10.10 Common Table Patterns

Production COBOL programs use tables in several recurring patterns. Understanding these patterns will help you recognize when and how to apply tables in your own programs.

Pattern 1: Lookup Tables

The most common use of tables is looking up a value based on a code:

      * State code -> state name lookup
       SET WS-STATE-IDX TO 1
       SEARCH WS-STATE-ENTRY
           AT END
               MOVE "UNKNOWN" TO WS-FULL-STATE-NAME
           WHEN WS-STATE-CODE(WS-STATE-IDX) = WS-INPUT-CODE
               MOVE WS-STATE-NAME(WS-STATE-IDX)
                   TO WS-FULL-STATE-NAME
       END-SEARCH

Common lookup tables include: - State/province codes to names - Error codes to messages - Product codes to descriptions - Currency codes to exchange rates - Tax bracket tables

Pattern 2: Accumulator Arrays

Accumulator tables collect running totals across categories:

       01  WS-DEPT-TOTALS.
           05  WS-DEPT-TOTAL   PIC 9(9)V99  OCCURS 20 TIMES
                                             VALUE ZERO.
       ...
      * During transaction processing:
       ADD WS-TRANS-AMOUNT TO WS-DEPT-TOTAL(WS-TRANS-DEPT)

At the end of processing, each element contains the sum for its department. This replaces what would otherwise require 20 separate accumulator variables.

Pattern 3: Transaction Buffers

When processing records in batches, tables serve as buffers:

       01  WS-BATCH-BUFFER.
           05  WS-BATCH-COUNT   PIC 999   VALUE ZERO.
           05  WS-BATCH-RECORD  OCCURS 1 TO 100 TIMES
                                DEPENDING ON WS-BATCH-COUNT.
               10  WS-BR-DATA   PIC X(80).

Read records into the buffer, process the batch, then clear and refill.

Pattern 4: Print Line Arrays

For generating formatted reports, tables can hold an entire page of output:

       01  WS-PAGE-BUFFER.
           05  WS-PAGE-LINE   PIC X(132)  OCCURS 60 TIMES.

Build the page in memory, then write all lines at once. This allows random access to any line on the page -- useful for reports that need data placed at specific positions.

Pattern 5: Direct-Access Tables

When the lookup key is a sequential integer, you can use it directly as a subscript -- no search needed:

      * Month number 1-12 maps directly to month name
       MOVE WS-MONTH-NAME(WS-MONTH-NUMBER) TO WS-OUTPUT

This is O(1) access -- the fastest possible lookup. Structure your data to take advantage of direct access whenever the key domain is small and sequential.


10.11 Table SORT (COBOL 2002+ and Vendor Extensions)

The COBOL 2002 standard introduced the ability to sort table data in place without using the SORT verb with files. The syntax varies by vendor, but the general form is:

       SORT WS-TABLE-ENTRY
           ON ASCENDING KEY WS-SORT-FIELD

Not all compilers support this. IBM Enterprise COBOL, for example, does not support in-place table sorting natively. In such cases, you would:

  1. Write the table to a temporary work file
  2. Use the SORT verb to sort the file
  3. Read the sorted file back into the table

Alternatively, you can implement a sorting algorithm (such as bubble sort or insertion sort) in COBOL:

      * Simple bubble sort
       PERFORM VARYING WS-I FROM 1 BY 1
           UNTIL WS-I >= WS-TABLE-SIZE
           PERFORM VARYING WS-J FROM 1 BY 1
               UNTIL WS-J > WS-TABLE-SIZE - WS-I
               IF WS-SORT-KEY(WS-J) > WS-SORT-KEY(WS-J + 1)
                   MOVE WS-TABLE-ENTRY(WS-J) TO WS-TEMP-ENTRY
                   MOVE WS-TABLE-ENTRY(WS-J + 1)
                       TO WS-TABLE-ENTRY(WS-J)
                   MOVE WS-TEMP-ENTRY
                       TO WS-TABLE-ENTRY(WS-J + 1)
               END-IF
           END-PERFORM
       END-PERFORM

For small tables (under 100 elements), bubble sort is adequate. For larger tables, consider more efficient algorithms or external sorting.


10.12 Memory Considerations

Tables consume memory proportional to their entry size multiplied by their occurrence count. In production systems, table sizing requires careful planning.

Calculating Table Size

Total bytes = entry_size * number_of_occurrences

For a multi-dimensional table:

Total bytes = entry_size * dim1 * dim2 * dim3 * ...

Example: A three-dimensional table with 50 regions x 12 months x 100 products, where each entry is 20 bytes:

50 * 12 * 100 * 20 = 1,200,000 bytes = ~1.2 MB

Mainframe Considerations

On IBM mainframes, WORKING-STORAGE resides in the program's address space. Very large tables may require: - Use of the LOCAL-STORAGE section (allocated per invocation) - External files instead of in-memory tables - Database lookups instead of table lookups - Region size increases in JCL

Efficient Sizing

  • Size tables for the expected maximum, not the theoretical maximum. If you will never have more than 500 products, do not allocate for 10,000.
  • Use OCCURS DEPENDING ON when the actual count varies significantly from the maximum.
  • For very large reference data, consider loading only the portion needed for the current run.

10.13 Subscript Range Checking

Accessing a table element with a subscript outside the valid range (less than 1 or greater than the OCCURS count) is one of the most common COBOL runtime errors. The consequences range from reading garbage data to corrupting adjacent memory to program abends.

Compiler Options for Range Checking

Most COBOL compilers offer a compile-time option to enable subscript range checking:

Compiler Option Effect
IBM Enterprise COBOL SSRANGE Runtime check on every subscript access
Micro Focus CHECK(2) or CHECK(3) Subscript bounds checking
GnuCOBOL -fcheck-subscripts Compile-time and runtime checks
ACUCOBOL -Za Array bounds checking

Use range checking during development and testing. It catches bugs that would otherwise produce subtle, hard-to-diagnose errors. In production, range checking is sometimes disabled for performance, but many shops leave it enabled as a safety measure.

Manual Range Checking

You can also check ranges explicitly in your code:

       IF WS-SUB >= 1 AND WS-SUB <= 100
           MOVE WS-TABLE-ITEM(WS-SUB) TO WS-OUTPUT
       ELSE
           DISPLAY "ERROR: Subscript out of range: " WS-SUB
           MOVE "INVALID" TO WS-OUTPUT
       END-IF

This is more verbose but makes the intent clear and works regardless of compiler options.


10.14 Common Mistakes and How to Avoid Them

Mistake 1: Subscript Out of Range

      * BUG: WS-SUB is 0 before the first iteration
       MOVE ZERO TO WS-SUB
       MOVE "DATA" TO WS-TABLE-ITEM(WS-SUB)   *> INVALID!

Fix: Always ensure subscripts are within bounds (1 to table size) before accessing table elements.

      * BUG: index might be beyond table end from previous SEARCH
       SEARCH WS-TABLE-ENTRY
           AT END ...
           WHEN ...
       END-SEARCH

Fix: Always SET WS-IDX TO 1 before each SEARCH statement (not needed for SEARCH ALL).

Mistake 3: Using SEARCH ALL on Unsorted Data

      * BUG: Data not sorted by WS-KEY -- SEARCH ALL gives wrong results
       SEARCH ALL WS-TABLE-ENTRY
           WHEN WS-KEY(WS-IDX) = "TARGET"
       END-SEARCH

Fix: Ensure data is sorted on the key field(s) in the order specified by ASCENDING/DESCENDING KEY IS. Verify after loading.

Mistake 4: Using Arithmetic on Indexes

      * BUG: Cannot ADD to an index
       ADD 1 TO WS-TBL-IDX   *> COMPILATION ERROR

Fix: Use SET WS-TBL-IDX UP BY 1 instead.

Mistake 5: ODO Subject Out of Range

       MOVE ZERO TO WS-ITEM-COUNT   *> Minimum is 1!
       MOVE "DATA" TO WS-ITEM-FIELD(1)  *> Undefined behavior

Fix: Keep the ODO subject within the declared n TO m range at all times.

Mistake 6: Misaligned REDEFINES Data

       01  WS-DATA.
           05  FILLER PIC X(10) VALUE "ABC       ".  *> 10 bytes
           05  FILLER PIC X(10) VALUE "DEF       ".  *> 10 bytes

       01  WS-TABLE REDEFINES WS-DATA.
           05  WS-ENTRY OCCURS 2 TIMES.
               10  WS-CODE  PIC X(3).
               10  WS-NAME  PIC X(8).   *> 3 + 8 = 11, not 10!

Fix: Ensure each FILLER entry is exactly the same size as the table entry structure. In this case, either make the FILLER items 11 bytes or adjust the table entry to 10 bytes.

Mistake 7: Multiple WHEN with SEARCH ALL

      * BUG: SEARCH ALL allows only ONE WHEN clause
       SEARCH ALL WS-ENTRY
           WHEN WS-CODE(WS-IDX) = "A"
               DISPLAY "Found A"
           WHEN WS-CODE(WS-IDX) = "B"    *> COMPILATION ERROR
               DISPLAY "Found B"
       END-SEARCH

Fix: Use separate SEARCH ALL statements for different search values, or use SEARCH (linear) if you need multiple conditions.


10.15 Fixed-Format vs. Free-Format Examples

All examples in this chapter use COBOL fixed-format (traditional column-based layout), which is the standard in most production environments. Here is how key constructs look in both formats for reference.

Fixed Format (Columns 7-72)

       01  WS-TABLE.
           05  WS-ENTRY        OCCURS 100 TIMES
                                INDEXED BY WS-IDX.
               10  WS-CODE     PIC X(5).
               10  WS-VALUE    PIC 9(7)V99.

       PROCEDURE DIVISION.
           SET WS-IDX TO 1
           SEARCH WS-ENTRY
               AT END
                   DISPLAY "Not found"
               WHEN WS-CODE(WS-IDX) = "ABC"
                   DISPLAY WS-VALUE(WS-IDX)
           END-SEARCH

Free Format (COBOL 2002+)

01 WS-TABLE.
   05 WS-ENTRY OCCURS 100 TIMES
               INDEXED BY WS-IDX.
      10 WS-CODE  PIC X(5).
      10 WS-VALUE PIC 9(7)V99.

PROCEDURE DIVISION.
    SET WS-IDX TO 1
    SEARCH WS-ENTRY
        AT END
            DISPLAY "Not found"
        WHEN WS-CODE(WS-IDX) = "ABC"
            DISPLAY WS-VALUE(WS-IDX)
    END-SEARCH

Free format removes the column restrictions, allowing code to start at any position. The syntax and semantics are identical; only the layout differs. Note that not all compilers support free format, and most existing production code uses fixed format.


10.16 Putting It All Together: A Tax Bracket Lookup Program

The case study program (code/case-study-code.cob) brings together all the table techniques covered in this chapter to implement a complete tax calculation system:

  1. Federal tax brackets are stored in a hardcoded table using REDEFINES. Each bracket contains a lower bound, upper bound, tax rate, and cumulative tax from lower brackets. The program searches through brackets from highest to lowest to find the applicable bracket, then calculates:

federal_tax = cumulative_tax + (income - bracket_lower) * rate

  1. State tax rates are stored in a sorted table with ASCENDING KEY IS for SEARCH ALL (binary search). Each entry indicates whether the state has no income tax, a flat rate, or graduated rates.

  2. FICA taxes (Social Security and Medicare) use simple rate constants with wage base caps, demonstrating that not every calculation needs a table.

  3. Employee records are stored in a table and processed in a batch loop, accumulating totals for a summary report.

This program demonstrates: - REDEFINES for hardcoded table initialization - SEARCH ALL with ASCENDING KEY IS for state lookup - Backward PERFORM VARYING for bracket search - Level-88 condition names on table fields - Accumulator pattern for batch totals - Formatted display of currency values


Summary

Tables are foundational to COBOL programming. This chapter covered the full spectrum of table functionality:

Topic Key Points
OCCURS clause Defines repeating data; cannot be at 01 level; creates fixed-size tables
Subscripts Regular data items (PIC 9); 1-based; require runtime multiplication
Indexes Defined with INDEXED BY; store byte displacement; faster; manipulated via SET
Multi-dimensional OCCURS within OCCURS; up to 7 levels; row-major memory layout
SEARCH Sequential scan; requires SET to 1 first; multiple WHEN allowed; O(n)
SEARCH ALL Binary search; requires sorted data and KEY IS clause; single WHEN; O(log n)
OCCURS DEPENDING ON Variable-length tables; ODO subject controls count; commonly used with files
REDEFINES Classic technique for hardcoded table initialization
Common patterns Lookup tables, accumulators, buffers, print arrays, direct-access tables

The key decisions when working with tables are: 1. How to populate the table: hardcoded (REDEFINES), file input, or computed 2. How to access elements: subscript (flexible) or index (faster) 3. How to search: direct access (O(1)), SEARCH (O(n)), or SEARCH ALL (O(log n)) 4. Fixed or variable length: OCCURS vs. OCCURS DEPENDING ON

Master these decisions and the corresponding syntax, and you will be able to handle any table-related requirement in production COBOL systems.


Next chapter: Chapter 11 explores string handling in COBOL -- the INSPECT, STRING, UNSTRING, and REFERENCE MODIFICATION statements that give you fine-grained control over text processing.