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...
In This Chapter
- Introduction
- 10.1 What Is a Table in COBOL?
- 10.2 The OCCURS Clause: Defining One-Dimensional Tables
- 10.3 Loading Tables: VALUE Clauses, REDEFINES, and File Input
- 10.4 Subscripts vs. Indexes: Syntax and Performance
- 10.5 Multi-Dimensional Tables
- 10.6 The SEARCH Statement: Sequential Table Lookup
- 10.7 The SEARCH ALL Statement: Binary Search
- 10.8 OCCURS DEPENDING ON: Variable-Length Tables
- 10.9 PERFORM VARYING with Tables
- 10.10 Common Table Patterns
- 10.11 Table SORT (COBOL 2002+ and Vendor Extensions)
- 10.12 Memory Considerations
- 10.13 Subscript Range Checking
- 10.14 Common Mistakes and How to Avoid Them
- 10.15 Fixed-Format vs. Free-Format Examples
- 10.16 Putting It All Together: A Tax Bracket Lookup Program
- Summary
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
OCCURSclause for defining one-dimensional tables - Subscripts and indexes for accessing table elements
- Multi-dimensional tables using nested
OCCURS - The
SEARCHstatement for sequential (linear) table lookup - The
SEARCH ALLstatement for binary search OCCURS DEPENDING ONfor variable-length tables- The
INDEXED BYclause andSETstatement - 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:
- Tables are defined in the DATA DIVISION, not the PROCEDURE DIVISION. Their size and structure are declared statically.
- Tables are 1-indexed, not 0-indexed. The first element is element 1.
- Tables can be multi-dimensional (up to 7 dimensions per the COBOL standard).
- Tables can contain complex structures, not just simple scalar values. A single table entry can include multiple fields of different types.
- Tables support built-in search operations through the
SEARCHandSEARCH ALLstatements.
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
OCCURScannot be specified at the 01 level in the WORKING-STORAGE SECTION. It can appear at levels 02 through 49.- The number after
OCCURSmust be a positive integer literal (or, withDEPENDING ON, a data-name). - The
OCCURSclause defines the maximum number of occurrences. - All occurrences share the same
PICclause 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:
- A hardcoded lookup table using
REDEFINESto convert month numbers to month names - A runtime-loaded table where employee data is populated during execution
- 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 BYclause. - You must
SETthe index to the starting position before eachSEARCH.
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
- The
SEARCHstatement checks if the index is already beyond the table (past the last occurrence). If so, theAT ENDclause executes immediately. - It evaluates the
WHENcondition(s) for the current index position. - If a
WHENcondition is true, the associated statements execute, and theSEARCHends. The index points to the found entry. - If no
WHENcondition 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.
Continuing a Search
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.
10.7 The SEARCH ALL Statement: Binary Search
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:
- The table must have an
ASCENDING KEY ISorDESCENDING KEY ISclause on itsOCCURS:
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).
-
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 ALLwill produce incorrect results silently. -
Only equality conditions (
=) are allowed in theWHENclause. You cannot use>,<,>=, or<=. -
Only one
WHENclause is permitted (unlikeSEARCH, which allows multiple). -
Compound keys require all key fields to appear in the
WHENclause, connected byAND.
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-COUNTin the example above). It determines how many occurrences are currently "active." - ODO object: The data item with the
OCCURS DEPENDING ONclause (WS-LINE-ITEM). Its effective size varies based on the ODO subject.
How It Works
- Memory is allocated for the maximum number of occurrences (50 in the example).
- The ODO subject (
WS-ITEM-COUNT) determines how many occurrences are logically valid at any given time. - When you set
WS-ITEM-COUNTto 5, only elements 1 through 5 are considered valid. FUNCTION LENGTHreturns the current logical length, not the maximum.
Rules and Restrictions
-
The ODO subject must be an integer numeric data item. It cannot be a table element or an item within the ODO object.
-
The ODO subject must NOT be part of the ODO object. The count field must be defined separately from the table it controls.
-
The value of the ODO subject must stay within the declared range. Setting
WS-ITEM-COUNTto 0 when the minimum is 1, or to 51 when the maximum is 50, causes undefined behavior. -
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. -
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.
-
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:
- Write the table to a temporary work file
- Use the
SORTverb to sort the file - 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 ONwhen 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.
Mistake 2: Forgetting to SET Index Before SEARCH
* 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:
- 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
-
State tax rates are stored in a sorted table with
ASCENDING KEY ISforSEARCH ALL(binary search). Each entry indicates whether the state has no income tax, a flat rate, or graduated rates. -
FICA taxes (Social Security and Medicare) use simple rate constants with wage base caps, demonstrating that not every calculation needs a table.
-
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.